Monday 5 May 2014

Spinner with "re-select" functionality

This is an advanced tutorial: I'll cover custom views, the Android source code and Java reflection.

A simple Spinner

As you probably already know, when the user selects an item from a Spinner the event onItemSelected is triggered.
To define the selection event handler for a Spinner, you have to implement the AdapterView.OnItemSelectedListener interface and the corresponding onItemSelected() callback method:
public class SpinnerActivity extends Activity implements OnItemSelectedListener {
 //...
 
    public void onItemSelected(AdapterView<?> parent, View view,
            int pos, long id) {
        // An item was selected. You can retrieve the selected item using
        // parent.getItemAtPosition(pos)
    }

    public void onNothingSelected(AdapterView<?> parent) {
        // Another interface callback
    }
}

However, if the Spinner is set to a specific item and the user re-selects the same item, no callback method is invoked. The reason is that the default implementation of the Spinner class uses an internal variable to keep track of the currently selected item: if the user re-selects the same item, no callback method is triggered.
To be more precise, to understand exactly what happens we have to look at the source code of the Spinner class, using AndroidXRef:

By examining the source code you can discover that when the user selects an item the method setSelection is invoked (check the methods onClick and show) . The method setSelection is defined in the superclass AbsSpinner (Spinner infact extends AbsSpinner).


As you can see the method setSelection calls setSelectionInt, which checks if the currently selected item is equal to the item already selected: in this case no event is triggered.
From the source code of setSelectionInt we discover that the class uses the variable mOldSelectedPosition to keep track of the already selected item of the Spinner. However, the AbsSpinner class contains no definition of this variable, so we have to check the superclasses.
After a brief search you can discover that mOldSelectedPosition is defined in the class AdapterView (superclass of AbsSpinner):


Now we have all the elements to create our custom Spinner with "re-select" functionality:
import java.lang.reflect.Field;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.Spinner;


public class SpinnerReselect extends Spinner {
 
 public SpinnerReselect(Context context) {
     super(context);
     // TODO Auto-generated constructor stub
 }
 
 public SpinnerReselect(Context context, AttributeSet attrs) {
     super(context, attrs);
     // TODO Auto-generated constructor stub
 }
 
 public SpinnerReselect(Context context, AttributeSet attrs, int defStyle) {
     super(context, attrs, defStyle);
     // TODO Auto-generated constructor stub
 }

  @Override
 public void setSelection(int position, boolean animate) {
     ignoreOldSelectionByReflection();
     super.setSelection(position, animate);
 }

  private void ignoreOldSelectionByReflection() {
     try {
         Class<?> c = this.getClass().getSuperclass().getSuperclass().getSuperclass();
         Field reqField = c.getDeclaredField("mOldSelectedPosition");
         reqField.setAccessible(true);
         reqField.setInt(this, -1);
     } catch (Exception e) {
         Log.d("Exception Private", "ex", e);
         // TODO: handle exception
     }
 }

  @Override
 public void setSelection(int position) {
     ignoreOldSelectionByReflection();
     super.setSelection(position);
 }
}



By implementing our custom Spinner we have to override the default implementation of the setSelection methods. To do so, we simply call the method ignoreOldSelectionByReflection before calling the default methods (defined in the class AbsSpinner: that's why we use super.setSelection...).

In the method ignoreOldSelectionByReflection we make use of Java reflectionReflection is commonly used by programs which require the ability to examine or modify the runtime behavior of applications running in the Java virtual machine. This is a relatively advanced feature and should be used only by developers who have a strong grasp of the fundamentals of the language. With that caveat in mind, reflection is a powerful technique and can enable applications to perform operations which would otherwise be impossible.

Let's examine the code:

Class<?> c = this.getClass().getSuperclass().getSuperclass().getSuperclass();

As we already saw the variable mOldSelectedPosition is defined the class AdapterView (superclass of AbsSpinner). So we have to get a reference to this class by calling getClass and getSuperclass respectively.


Field reqField = c.getDeclaredField("mOldSelectedPosition");
reqField.setAccessible(true);

The secondo step is to get access to the field mOldSelectedPosition. By calling reqField.setAccessible(true) we make sure that the reflected object should suppress Java language access checking when it is used.

reqField.setInt(this, -1);

The trick is quite clear: we set the value of mOldSelectedPosition to -1 to make sure that the currently selected item of the Spinner is always different from the previously selected item, even if the user re-selectes the same item.
This way the method setSelection is always triggered!


4 comments:

  1. "this" is taking my applications context.
    I am getting exception like "expected receiver of type android.widget.AdapterView, but got com.example.spinnertest.MainActivity$1" this.
    Plz help me to resolve this.

    ReplyDelete
    Replies
    1. In which line are you getting this exception?

      Delete