Android Spinner selection

2019-01-13 15:59发布

问题:

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?

回答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.



回答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 ;-)



回答3:

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.



回答4:

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 :)



回答5:

I also looked for a good solution on the internet but didn't find any that satisfied my needs. So I've written this extension on the Spinner class so you can set a simple OnItemClickListener, which has the same behaviour as a ListView.

Only when an item gets 'selected', the onItemClickListener is called.

Have fun with it!

 public class MySpinner extends Spinner
    {
        private OnItemClickListener onItemClickListener;


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

        public MySpinner(Context context, AttributeSet attrs)
        {
            super(context, attrs);
        }

        public MySpinner(Context context, AttributeSet attrs, int defStyle)
        {
            super(context, attrs, defStyle);
        }

        @Override
        public void setOnItemClickListener(android.widget.AdapterView.OnItemClickListener inOnItemClickListener)
        {
            this.onItemClickListener = inOnItemClickListener;
        }

        @Override
        public void onClick(DialogInterface dialog, int which)
        {
            super.onClick(dialog, which);

            if (this.onItemClickListener != null)
            {
                this.onItemClickListener.onItemClick(this, this.getSelectedView(), which, this.getSelectedItemId());
            }
        }
    }


回答6:

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.



回答7:

I did some logging and discovered that it only ever gets called on initialize, which is annoying. Can't see the need for all this code, I just created an instance variable that was initialised to a guard value, and then set it after the method had been called the first time.

I logged when the onItemSelected method was being called it was otherwise only being called once.

I had a problem where it was creating two of something and realised it was because I was calling add() on my custom adapter, which already had a reference to the list I was referencing and had added to outside the adapter. After I realised this and removed the add method the problem went away.

Are you guys sure you need all this code?



回答8:

I know this is pretty late, but I came up with a very simple solution to this. It is based on Arhimed's answer, it's exactly the same. It's very easy to implement too. Refer the accepted answer:

Undesired onItemSelected calls