Android Spinner OnItemSelected ONLY on User Interaction

2.1k views Asked by At

I know that this question has been answered multiple times, but I have not found any of them satisfactory and certainly not elegant.

The issue is that OnItemSelected gets fired not only when the user selects an item, but also when the selection is programmatically set.

Some answers propose setting a flag for times when the programmer is setting a value for the spinner. However sometimes other android code will set the value outside of your code.

A common place for android to set the value is upon instantiation of the spinner. Some answers address that particular issue. However, there are numerous places where Android will break down and reinstantiate a spinner. It is not elegant to track down all of them.

So the question is: how does one attach their OnItemSelectedListener code ONLY to selections made by user interaction with the UI?

  • Spinner.setOnItemClickListener seems like it would answer this issue perfectly, but Google has it disabled
  • AdapterView.setOnClickListener also seems like a natural candidate but it also generates a runtime error

Next place is to extend Spinner and start overriding methods but that of course means messing with the APIs (which I'd rather not do, or I'd at least like to have other users working on it with me)

2

There are 2 answers

0
NewEndian On

So I am posting an answer, but any criticisms, improvements, or other more elegant answers are welcome.

The key is overriding onClick to set a flag which ties the onItemSelectedListener to user interaction and fires the onItemClickedListener.

If you don't feel comfortable using the API setOnItemClickedListener (for future compatibility perhaps), you can of course substitute your own method. I just felt like onItemClickedListener should have been implemented to this effect the whole time, so that is my subtle protest.

Also, if anyone can think of a reason that the spinnerTouched flag gets short circuited (stays true for longer than it should), please let us know so that it can be addressed. It seems to work pretty well so far though.

public class OnItemClickSpinner extends Spinner implements AdapterView.OnItemSelectedListener {

    boolean spinnerTouched = false;
    OnItemClickListener onItemClickListener = null;
    OnItemSelectedListener onItemSelectedListener = null;

    public OnItemClickSpinner(Context context) {
        super(context);
        super.setOnItemSelectedListener(this);
    }

    public OnItemClickSpinner(Context context, AttributeSet attrs) {
        super(context, attrs);
        super.setOnItemSelectedListener(this);
    }

    public OnItemClickSpinner(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        super.setOnItemSelectedListener(this);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        spinnerTouched = true;
        return super.onTouchEvent(event);
    }

    @Override
    public void setOnItemClickListener(OnItemClickListener listener) {
        onItemClickListener = listener;
    }

    @Override
    public void setOnItemSelectedListener(OnItemSelectedListener onItemSelectedListener) {
        this.onItemSelectedListener = onItemSelectedListener;
        super.setOnItemSelectedListener(this);
    }

    @Override
    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
        if(spinnerTouched && this.onItemClickListener!=null) this.onItemClickListener.onItemClick(parent,view,position,id);
        if(this.onItemSelectedListener!=null) this.onItemSelectedListener.onItemSelected(parent,view,position,id);
        spinnerTouched=false;
    }

    @Override
    public void onNothingSelected(AdapterView<?> parent) {
        if(this.onItemSelectedListener!=null) this.onItemSelectedListener.onNothingSelected(parent);
        spinnerTouched=false;
    }
}
0
funct7 On

Had a similar issue with some UI component in iOS. I decided to use a similar hack.

So somewhere in the object that owns the spinner--maybe an activity--declare a member variable: private var isSelectionFromTouch: Boolean = false

The rest of the relevant code:

init {
    spinner.onItemSelectedListener = this
    spinner.setOnTouchListener { _, _ ->
        this.isSelectionFromTouch = true
        false
    }
}

// On Item Selected

override
fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
    if (!isSelectionFromTouch) { return }
    Log.d(TAG, "item selected. do something.")
    isSelectionFromTouch = false
}

override
fun onNothingSelected(parent: AdapterView<*>?) {
    isSelectionFromTouch = false
}