Android Spinner selection

26,979

Solution 1

To workaround you need to remember the last selected position. Then inside of your spinner listener compare the last selected position with the new one. If they are different, then process the event and also update the last selected position with new position value, else just skip the event processing.

If somewhere within the code you are going to programatically change spinner selected position and you don't want the listener to process the event, then just reset the last selected position to the one you're going to set.

Yes, Spinner in Android is painful. I'd even say pain starts from its name - "Spinner". Isn't it a bit misleading? :) As far as we're talking about it you should also be aware there's a bug - Spinner may not restore (not always) its state (on device rotation), so make sure you handle Spinner's state manually.

Solution 2

Hard to believe that a year and a half later, the problem still exists and continues to boggle people...

Thought I'd share the workaround I came up with after reading Arhimed's most useful post (thanks, and I agree about spinners being painful!). What I've been doing to avoid these false positives is to use a simple wrapper class:

import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;

public class OnItemSelectedListenerWrapper implements OnItemSelectedListener {

    private int lastPosition;
    private OnItemSelectedListener listener;

    public OnItemSelectedListenerWrapper(OnItemSelectedListener aListener) {
        lastPosition = 0;
        listener = aListener;
    }

    @Override
    public void onItemSelected(AdapterView<?> aParentView, View aView, int aPosition, long anId) {
        if (lastPosition == aPosition) {
            Log.d(getClass().getName(), "Ignoring onItemSelected for same position: " + aPosition);
        } else {
            Log.d(getClass().getName(), "Passing on onItemSelected for different position: " + aPosition);
            listener.onItemSelected(aParentView, aView, aPosition, anId);
        }
        lastPosition = aPosition;
    }

    @Override
    public void onNothingSelected(AdapterView<?> aParentView) {
        listener.onNothingSelected(aParentView);
    }
}

All it does is trap item selected events for the same position that was already selected (e.g. the initial automatically triggered selection for position 0), and pass on other events to the wrapped listener. To use it, all you have to do is modify the line in your code that calls the listener to include the wrapper (and add the closing bracket of course), so instead of, say:

mySpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
    ...
});

you'd have this:

mySpinner.setOnItemSelectedListener(new OnItemSelectedListenerWrapper(new OnItemSelectedListener() {
    ...
}));

Obviously once you've tested it, you could get rid of the Log calls, and you could add the ability to reset the last position if required (you'd have to keep a reference to the instance, of course, rather than declaring on-the-fly) as Arhimed said.

Hope this can help someone from being driven crazy by this strange behaviour ;-)

Solution 3

I had this situation lately when using spinners and the internet didn't came up with a suitable solution.

My application scenario:

X spinners (dynamically, 2 for each cpu, min & max) for setting & viewing the CPU-Frequency. They are filled when the application starts and they also get the current max/min freq of the cpu set. A thread runs in the background and checks for changes every second and updates the spinners accordingly. If a new frequency inside the spinner is set by the user the new frequency is set.

The issue was that the thread accessed setSelection to update the current frequency which in turn called my listener and I had no way of knowing if it was the user or the thread that changed the value. If it was the thread I didn't want the listener to be called since there would have been no need to change the frequency.

I came up with a solution that suits my needs perfectly and works around the listener on your call :) (and I think this solution gives you maximal control)

I extended Spinner:

import android.content.Context;
import android.widget.Spinner;

public class MySpinner extends Spinner {
    private boolean call_listener = true;

    public MySpinner(Context context) {
        super(context);
    }

    public boolean getCallListener() {
        return call_listener;
    }

    public void setCallListener(boolean b) {
        call_listener = b;
    }

    @Override
    public void setSelection(int position, boolean lswitch) {
        super.setSelection(position);
        call_listener = lswitch;
    }
}

and created my own OnItemSelectedListener:

import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;

public class SpinnerOnItemSelectedListener implements OnItemSelectedListener {
      public void onItemSelected(AdapterView<?> parent, View view, int pos,long id) {
          MySpinner spin = (MySpinner) parent.findViewById(parent.getId());
          if (!spin.getCallListener()) {
              Log.w("yourapptaghere", "Machine call!");
              spin.setCallListener(true);
          } else {
              Log.w("yourapptaghere", "UserCall!");
          }
      }

      @Override
      public void onNothingSelected(AdapterView<?> arg0) {
        // TODO Auto-generated method stub
      }
}

If you now create a MySpinner you can use this to set the selection:

setSelection(position, callListener);

Where callListener is either true or false. True will call the listener and is default, which is why user interactions are getting identified, false will also call the listener but uses code you want for this special case, exempli gratia in my case: Nothing.

I hope that someone else finds this useful and is spared a long journey to look if something like this already exists :)

Solution 4

In the past I've done things like this to distinguish

internal++; // 'internal' is an integer field initialized to 0
textBox.setValue("...."); // listener should not act on this internal setting
internal--;

Then in textBox's listener

if (internal == 0) {
  // ... Act on user change action
}

I use ++ and -- rather than setting a boolean value to 'true' so that there is no worry when methods nest other methods that might also set the internal change indicator.

Solution 5

Just to expand on aaamos's post above, since I don't have the 50 rep points to comment, I am creating a new answer here.

Basically, his code works for the case when the initial Spinner selection is 0. But to generalize it, I amended his code the following way:

@Override
public void setOnItemSelectedListener(final OnItemSelectedListener listener)
{
    if (listener != null)
        super.setOnItemSelectedListener(new OnItemSelectedListener()
        {
            private static final int NO_POSITION  = -1;

            private int lastPosition = NO_POSITION;


            @Override
            public void onItemSelected(AdapterView<?> parent, View view, int position, long id)
            {
                if ((lastPosition != NO_POSITION) && (lastPosition != position))
                    listener.onItemSelected(parent, view, position, id);

                lastPosition = position;
            }


            @Override
            public void onNothingSelected(AdapterView<?> parent)
            {
                listener.onNothingSelected(parent);
            }
        });
    else
        super.setOnItemSelectedListener(null);
}

Basically, this code will ignore the very first firing of onItemSelected(), and then all subsequent "same position" calls.

Of course, the requirement here is that the selection is set programatically, but that should be the case anyhow if the default position is not 0.

Share:
26,979
Arutha
Author by

Arutha

Updated on July 18, 2022

Comments

  • Arutha
    Arutha almost 2 years

    The OnItemSelectedListener event handler gets called both when a spinner selection is changed programmatically, and when a user physically clicks the spinner control. Is is possible to determine if an event was triggered by a user selection somehow?

    Or is there another way to handle spinner user selections?