Java Question Spinner change Adapter

warwound

Expert
Licensed User
Longtime User
I'm trying to take a Spinner created in the Designer and change it's Adaptor to an Adaptor i've created in a library.
Look at a typical exception:
java.lang.IndexOutOfBoundsException: Invalid index 6, size is 0
at java.util.ArrayList.throwIndexOutOfBoundsException(ArrayList.java:255)
at java.util.ArrayList.get(ArrayList.java:308)
at anywheresoftware.b4a.objects.SpinnerWrapper$B4ASpinnerAdapter.getItem(SpinnerWrapper.java:257)
at anywheresoftware.b4a.objects.SpinnerWrapper$B4ASpinner.setSelection(SpinnerWrapper.java:212)
at android.widget.Spinner$DropdownPopup$1.onItemClick(Spinner.java:1042)
at android.widget.AdapterView.performItemClick(AdapterView.java:299)
at android.widget.AbsListView.performItemClick(AbsListView.java:1113)
at android.widget.AbsListView$PerformClick.run(AbsListView.java:2904)
at android.widget.AbsListView$3.run(AbsListView.java:3638)
at android.os.Handler.handleCallback(Handler.java:733)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:136)
at android.app.ActivityThread.main(ActivityThread.java:5102)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:515)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:785)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:601)
at dalvik.system.NativeStart.main(Native Method)

I loaded the layout file containing the Spinner and then passed it to my library to have my Adapter passed to it's setAdapter() method.
I added 64 strings as spinner items to my adapter, and clicked on item 6.

My Spinner seems to have two Adapters set - the B4ASpinnerAdapter and my CustomSpinnerAdapter.
The Spinner shows my 64 items so my CustomSpinnerAdapter has obviously been set as my Spinner's Adapter.
But a click on an item causes the exception - the B4ASpinnerAdapter looks for item number 6 but has no items - so the original B4ASpinnerAdapter is still set.

I can successfully set a custom Adapter as an Adapter for a B4A ListView using a similar technique, the B4A ListView's original Adapter is no longer set, my call to setAdapter() overwrites the previous Adapter with a ListView.
BUT with a Spinner the call to setAdapter() seems to addAdapter.

I've googled a bit but found no info for such a simple question - how can i change a Spinner's Adapter?

Here's my code:

B4X:
Sub Process_Globals
End Sub

Sub Globals
	Private ListView1 As ListView
	Private Spinner1 As Spinner
End Sub

Sub Activity_Create(FirstTime As Boolean)
	Activity.LoadLayout("Main")
	
	Dim CustomSpinnerAdapter1 As CustomSpinnerAdapter
	CustomSpinnerAdapter1.Initialize("CustomSpinnerAdapter1")	'	raises no events yet
	CustomSpinnerAdapter1.SetSpinner(Spinner1)
	For i=0 To 63
		CustomSpinnerAdapter1.AddItem("Item: "&i, Null)
	Next
End Sub

Sub Activity_Resume
End Sub

Sub Activity_Pause (UserClosed As Boolean)
End Sub

B4X:
public class CustomSpinnerAdapter extends BaseAdapter implements SpinnerAdapter {
	
	@ShortName("CustomSpinnerAdapterItem")
	public static final class CustomSpinnerAdapterItem{
		protected final Object mTag;
		protected final String mText;
		
		public CustomSpinnerAdapterItem(String pText, Object pTag){
			mTag=pTag;
			mText=pText;
		}
		
		/**
		 * Returns the item Tag property.
		 */
		public Object getTag(){
			return mTag;
		}
		
		/**
		 * Returns the item Tag property.
		 */
		public String getText(){
			return mText;
		}
	}
	
	private BA mBA;
	private String mEventName;
	private String mGetViewEventName;
	private LayoutInflater mLayoutInflater;
	private List<CustomSpinnerAdapterItem> mItems;
	
	public CustomSpinnerAdapter(BA pBA, String pEventName){
		mBA=pBA;
		mEventName=pEventName;
		mGetViewEventName=(pEventName+"_GetView").toLowerCase(BA.cul);
		if(!mBA.subExists(mGetViewEventName)){
			BA.LogError("CustomSpinnerAdapter callback not found "+pEventName+"_GetView");
		}
		mLayoutInflater=LayoutInflater.from(mBA.context);
		mItems=new ArrayList<CustomSpinnerAdapterItem>();
		
	}
	
	public synchronized void AddItem(String pText, Object pTag){
		mItems.add(new CustomSpinnerAdapterItem(pText, pTag));
		this.notifyDataSetChanged();
	}
	
	public synchronized void Clear(){
		mItems.clear();
		this.notifyDataSetChanged();
	}
	
	public BA getBA(){
		return mBA;
	}
	
	@Override
	public synchronized int getCount() {
		return mItems.size();
	}
	
	@Override
	public View getDropDownView(int position, View convertView, ViewGroup parent) {
		if(convertView==null){
			convertView=mLayoutInflater.inflate(R.layout.simple_spinner_dropdown_item, parent, false);
		}
		((TextView) convertView).setText(mItems.get(position).getText());
		return convertView;
	}

	public String getEventName(){
		return mEventName;
	}
	
	@Override
	public synchronized Object getItem(int pPosition) {
		return mItems.get(pPosition);
	}

	@Override
	public long getItemId(int position) {
		// TODO Auto-generated method stub
		return 0;
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		if(convertView==null){
			convertView=mLayoutInflater.inflate(R.layout.simple_spinner_item, parent, false);
		}
		((TextView) convertView).setText("debug: "+mItems.get(position).getText());
		return convertView;
	}
}
Library and demo project attached.
(Ignore the logged message 'CustomSpinnerAdapter callback not found CustomSpinnerAdapter1_GetView' it's not relevant).
Thanks.

Martin.
 

Attachments

  • Adapter_problem.zip
    19.3 KB · Views: 648

warwound

Expert
Licensed User
Longtime User
Another thing that had me stumped was that i couldn't see where in the b4a Spinner class (in Core.jar) the Spinner ItemClick event was being raised.
I thought that'd lead me to some listener that was added in the b4a Spinner initialization but as i said, could find the raiseEvent code that raised that event.

Martin.
 

warwound

Expert
Licensed User
Longtime User
Searching the SpinnerWrapper.java source i only find this event being raised:

B4X:
    public void setSelection(int position)
    {
      super.setSelection(position);
      selectedItem = position;
      if ((ba != null) && (!disallowItemClick))
        ba.raiseEventFromUI(this, eventName + "_itemclick", new Object[] { 
          Integer.valueOf(selectedItem), adapter.getItem(selectedItem) });
    }

Which seems to manually select an item and then manually raise the required 'itemclick' event.

SpinnerWrapper extends ViewWrapper and ViewWrapper will try to set both OnClickListener and onLongClickListener if corresponding event subs exist.
But i have no subs which are of the form mySpinnerEventName_Click or mySpinnerEventName_LongClick so am assuming that neither of these listeners have been added.

I was expecting to find code such as:

B4X:
setOnItemSelectedListener(new OnItemSelectedListener(){
	
					@Override
					public void onItemSelected(AdapterView<?> pParent, View pView, int pPosition, long pId) {
						// raise an event
					}
	
					@Override
					public void onNothingSelected(AdapterView<?> pParent) {}
				});

Martin.
 

thedesolatesoul

Expert
Licensed User
Longtime User
I agree.
But it seems like setSelected is being used to trigger onItemSelectedListener indirectly. It never happens though as there is no listener, but it is assumed that setSelected will be the only method to be able to raise that.

But either way, your original problem is because of this being returned:
B4X:
adapter.getItem(selectedItem)
 

warwound

Expert
Licensed User
Longtime User
Yep:
at anywheresoftware.b4a.objects.SpinnerWrapper$B4ASpinnerAdapter.getItem(SpinnerWrapper.java:257)
But why is the original adapter still executing it's methods after i have set a new adapter - that's the problem.

Martin.
 

thedesolatesoul

Expert
Licensed User
Longtime User
Because that is how it is coded.
B4X:
this.adapter.getItem(this.selectedItem)
It should be coded like this:
B4X:
this.getObject().getAdapter.getItem(this.selectedItem)
This would have fetched whichever the current adapter was, especially if it was changed afterwards.

EDIT:
Also this is not a method of the original adapter, this is the method of the subclassed spinner B4ASpinner.
 

warwound

Expert
Licensed User
Longtime User
Gotcha - didn't notice that!

I seems to remember that the class's 'adaptor' member was public.
If so i can set that member as well as calling setAdaptor().

I'll be back...
 

Erel

B4X founder
Staff member
Licensed User
Longtime User

warwound

Expert
Licensed User
Longtime User
Success!

I have to update a project with many layout files containing many Spinners.
I didn't want to create a custom view as it'd involve too much modification of all those layout files.
Instead i updated my library, this is from the BaseAdapter class:

B4X:
	/**
	 * Set this adapter as the adapter for AdapterView1.
	 * AdapterView1 will be a View such as a ListView or a Spinner.
	 */
	public void SetAdapterView(BA pBA, AdapterView<android.widget.BaseAdapter> AdapterView1){
		if(B4ASpinner.class.isInstance(AdapterView1)){
			BA.LogInfo("B4ASpinner being deactivated");
			View view=(View) AdapterView1;
			B4ASpinner b4aSpinner=(B4ASpinner) view;
			b4aSpinner.ba=null;
			B4ASpinnerAdapter adapter=b4aSpinner.adapter;
			adapter.items=null;
			adapter=new SpinnerWrapper.B4ASpinnerAdapter(pBA.context);
			b4aSpinner.adapter=adapter;
		}
		// rest of method snipped

My method accepts an android AdapterView - a ListView or a Spinner.
If the AdapterView is an instance of B4ASpinner then i set it's 'ba' member to null.
Now the B4ASpinner setSelection method doesn't execute anything that raises a NullPointerException:

B4X:
   public void setSelection(int position)
    {
      super.setSelection(position);
      selectedItem = position;
      if ((ba != null) && (!disallowItemClick))
        ba.raiseEventFromUI(this, eventName + "_itemclick", new Object[] { 
          Integer.valueOf(selectedItem), adapter.getItem(selectedItem) });
    }

Setting it's 'items' member to null and then setting it's 'adapter' member to a new B4ASpinnerAdapter should hopefully release any references.

Now i have an Adaptor object that can be used with a ListView and/or a Spinner, a single instance of the Adaptor can be simultaneously set as a ListView adaptor or Spinner adapter.

Thanks.

Martin.
 

warwound

Expert
Licensed User
Longtime User
The workflow is:
  • Initialize an instance of my BaseAdapter.
  • Set the instance as the adapter of a ListView or Spinner.
  • When setting this adapter, the type of object that it is being added to is established:
    • A ListView has OnItemClickListener and OnItemLongClickListener added.
    • A Spinner has an OnItemSelectedListener added.
  • An item in either ListView or Spinner is an instance of my AdaptorViewItem class.
  • AdaptorViewItem is inflated from xml layout files, it's not a native b4a View.
  • The BaseAdapter raises these events:
    • GetView(ItemIndex As Int, AdaptorViewItem1 As AdaptorViewItem) As AdaptorViewItem
      Return an AdapterViewItem.
      AdapterViewItem may or may not be initialized, if it is initialized then re-cycle it otherwise initialize it.
    • ItemClick(ItemIndex As Int, AdaptorViewItem1 As AdaptorViewItem)
      Not raised by a Spinner.
    • ItemLongClick(ItemIndex As Int, AdaptorViewItem1 As AdaptorViewItem)
      Not raised by a Spinner.
    • ItemSelected(ItemIndex As Int, AdaptorViewItem1 As AdaptorViewItem)
      Not raised by a ListView.
  • AdaptorViewItem is a type of View and therefore has a Tag property.

So my customised Spinner/BaseAdapter will raise the event:

ItemSelected(ItemIndex As Int, AdaptorViewItem1 As AdaptorViewItem)

AdaptorViewItem1's Tag property will contain a value that i can use to establish the selected item.
(Much as a standard b4a ListView item can pass an arbitrary object to the subs that handle it's item clicks and long clicks).
The standard Spinner behaviour passes only the clicked or long clicked item's text property - my Spinner needs to contain localised text for different languages and so it's text property varies depending on language.
Setting a unique integer Tag property on each AdapterViewItem makes it easier to identify the selected item.

That's why i created the library.
I extended the android BaseAdapter class here and a BaseAdaptor can be used for ListViews and Spinners - that wasn't important but is a nice feature.

Might release it when the code has been polished, though if you're interested post again and i'll upload a demo.

Martin.
 

warwound

Expert
Licensed User
Longtime User
@tds or indeed anyone that's interested.
My CustomAdapters library and demo projects are attached.

I expect to update the library's CustomSpinnerAdapter this week as i put it to use in a project.
Rather than start a new library thread i'll just attach the files to this post for now.
If there's any interest i'll start a new thread.

I'm particularly pleased with the GeographBaseAdapterSpinner example - here we have a b4a Spinner which displays one of two different Views for each Spinner item.

One View is the default single TextView - a plain and boring default!
This View is displayed as the selected item - when the Spinner is collapsed.

The other View is more complex - a RelativeLayout containing an ImageView and 4 TextViews all nicely aligned.
This View is displayed when the Spinner is expanded - a list of these Views is selectable by the user.

Seems like an occasional NullPointerException is thrown if an Activity is paused while the Adapter is requesting a View from the b4a code.
I'll hopefully fix that next week.

Martin.
 

Attachments

  • CustomAdapters_demos_and_library.zip
    60 KB · Views: 742

warwound

Expert
Licensed User
Longtime User
Interesting, I think I could use this for a spinner which needs right aligment?

http://stackoverflow.com/questions/7511049/set-view-text-align-at-center-in-spinner-in-android

Not sure how to implement this however.

Yes you'd set the gravity attribute of a TextView in the XML that defines your Spinner items layout - set it to 'right' i'd guess.
Can you get my custom adapter example project to work?
If so look at the resources located at 'res/layout/' - the simple_spinner_????.xml files.
You want to add the attribute android:gravity="right" to the TextView and CheckedTextView elements in each layout file.

Now recompile and see if that has worked...

Martin.
 

bluedude

Well-Known Member
Licensed User
Longtime User
Hi warhound, I got something working. However, I would want to have a spinner which would inherit from the default spinner and only overwite the alignment. Is that actually possible? Could I still use stuff like color settings in B4A when I use this?

I got the demo's working for the rest.
 
Top