Reputation: 2511
I have code that runs OnItemSelectedListener
event of spinner. So when I am in the method:
public void onItemSelected(AdapterView<?> parentView, View selectedItemView, int position, long id) {
// I want to do something here if it's a user who changed the the selected item
}
...how can I know whether the item selection was done programmatically or by a user action through the UI?
Upvotes: 16
Views: 5838
Reputation: 161
Unlike a SeekBar
a Spinner
does not have built-in support for detecting whether the change was programmatic or by the user, so I suggest that, never use a spinner for any kind of recursive programmatic tasks. I had very bad experience when I tried to implement a MediaPlayer
with recursive connection to a SeekBar
and a Spinner
. The result was full of disappointment. So you can attempt only if you like unhappiness and disappointment.
Note:
I solved my issue by adding an apply Button
to my spinner selection. Doing a work around is not a good practice rather re-implement the Spinner
to have an our own expected behavior.
Upvotes: 1
Reputation: 3504
I used a workaround based on the focusable in touch mode scenario.
Set the spinner view as focusable in touch mode.
Set an on focus change listener of the spinner to call the spinner.performClick() when focused.
in the onItemSelected listener of the spinner,return the focus to the parent view of the layout(or which ever view you find suitable)
User input can be identified by checking if the spinner has focus as programmatic changes will not requestfocus.
PS: when you set the focusableintouchmode for the spinner in onCreate, make sure you return the focus to the parent view immediately in case you lack any other focusable views.
Upvotes: 0
Reputation: 3135
I made a mashup of @ban-geoengineering's and @vedant's. Available as a gist at https://gist.github.com/paulpv/9f6f1cd81945a3029ee3343a3543fe1c
I do not like the accepted isUserAction
answer.
@ban-geoengineering's answer is cute for one Spinner, but not when you have more than one.
I did not like needing to have a special changeSpinnerSelectionProgrammatically
method in @vedant's answer.
I see this as a reasonable best of both worlds:
package com.prometheanworld.audiotest
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Resources
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.View.OnTouchListener
import android.widget.AdapterView
import androidx.appcompat.widget.AppCompatSpinner
/**
* A subclass of AppCompatSpinner that adds `userTouched` detection
*/
@SuppressLint("ClickableViewAccessibility")
class MySpinner : AppCompatSpinner {
companion object {
private const val MODE_THEME = -1
}
/**
* A clone of AdapterView.OnItemSelectedListener that adds a `userTouched: Boolean` parameter to each method.
*/
interface OnItemSelectedListener {
/**
*
* Callback method to be invoked when an item in this view has been
* selected. This callback is invoked only when the newly selected
* position is different from the previously selected position or if
* there was no selected item.
*
* Implementers can call getItemAtPosition(position) if they need to access the
* data associated with the selected item.
*
* @param parent The AdapterView where the selection happened
* @param view The view within the AdapterView that was clicked
* @param position The position of the view in the adapter
* @param id The row id of the item that is selected
* @param userTouched true if the user touched the view, otherwise false
*/
fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long, userTouched: Boolean)
/**
* Callback method to be invoked when the selection disappears from this
* view. The selection can disappear for instance when touch is activated
* or when the adapter becomes empty.
*
* @param parent The AdapterView that now contains no selected item.
* @param userTouched true if the user touched the view, otherwise false
*/
fun onNothingSelected(parent: AdapterView<*>?, userTouched: Boolean)
}
private var userTouched = false
private var externalOnTouchListener: OnTouchListener? = null
private var internalOnTouchListener = OnTouchListener { v, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> userTouched = true
}
externalOnTouchListener?.onTouch(v, event) ?: false
}
private var externalOnItemSelectedListener: OnItemSelectedListener? = null
private val internalOnItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
externalOnItemSelectedListener?.onItemSelected(parent, view, position, id, userTouched)
userTouched = false
}
override fun onNothingSelected(parent: AdapterView<*>?) {
externalOnItemSelectedListener?.onNothingSelected(parent, userTouched)
userTouched = false
}
}
constructor(context: Context) : this(context, null)
@Suppress("unused")
constructor(context: Context, mode: Int) : this(context, null, R.attr.spinnerStyle, mode)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, R.attr.spinnerStyle)
constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : this(context, attrs, defStyle, MODE_THEME)
constructor(context: Context, attrs: AttributeSet?, defStyle: Int, mode: Int) : this(context, attrs, defStyle, mode, null)
constructor(context: Context, attrs: AttributeSet?, defStyle: Int, mode: Int, popupTheme: Resources.Theme?) :
super(context, attrs, defStyle, mode, popupTheme) {
super.setOnTouchListener(internalOnTouchListener)
super.setOnItemSelectedListener(internalOnItemSelectedListener)
}
override fun setOnTouchListener(listener: OnTouchListener?) {
externalOnTouchListener = listener
}
override fun setOnItemSelectedListener(listener: AdapterView.OnItemSelectedListener?) {
throw UnsupportedOperationException("Use setOnItemSelectedListener(listener: MySpinner.OnItemSelectedListener?) instead")
}
fun setOnItemSelectedListener(listener: OnItemSelectedListener?) {
externalOnItemSelectedListener = listener
}
}
Upvotes: 0
Reputation: 18871
You can achieve the desired result fairly simply by using Spinner's setOnTouchListener()
method:
// Instance variables
boolean spinnerTouched = false;
Spinner spinner;
// onCreate() / onCreateView() / etc. method..
spinner = ...;
spinner.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
spinnerTouched = true; // User DID touched the spinner!
}
return false;
}
});
spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parentView, View selectedItemView, int position, long id) {
if (spinnerTouched) {
// Do something
}
else {
// Do something else
}
}
@Override
public void onNothingSelected(AdapterView<?> parentView) {
}
});
// Your method that you use the change the spinner selection programmatically...
private void changeSpinnerSelectionProgrammatically(int pos) {
stateSpinnerTouched = false; // User DIDN'T touch the spinner
boolean useAnimation = false;
spinner.setSelection(pos, useAnimation); // Calls onItemSelected()
}
Upvotes: 12
Reputation: 18819
I made a new Spinner class encapsulating the above mentioned principles. But even then you have to make sure to call the correct method and not setSelection
Same thing in a gist
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.AdapterView;
/**
* Used this to differentiate between user selected and prorammatically selected
* Call {@link Spinner#programmaticallySetPosition} to use this feature.
* Created by vedant on 6/1/15.
*/
public class Spinner extends android.widget.Spinner implements AdapterView.OnItemSelectedListener {
OnItemSelectedListener mListener;
/**
* used to ascertain whether the user selected an item on spinner (and not programmatically)
*/
private boolean mUserActionOnSpinner = true;
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (mListener != null) {
mListener.onItemSelected(parent, view, position, id, mUserActionOnSpinner);
}
// reset variable, so that it will always be true unless tampered with
mUserActionOnSpinner = true;
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
if (mListener != null)
mListener.onNothingSelected(parent);
}
public interface OnItemSelectedListener {
/**
* <p>Callback method to be invoked when an item in this view has been
* selected. This callback is invoked only when the newly selected
* position is different from the previously selected position or if
* there was no selected item.</p>
*
* Impelmenters can call getItemAtPosition(position) if they need to access the
* data associated with the selected item.
*
* @param parent The AdapterView where the selection happened
* @param view The view within the AdapterView that was clicked
* @param position The position of the view in the adapter
* @param id The row id of the item that is selected
*/
void onItemSelected(AdapterView<?> parent, View view, int position, long id, boolean userSelected);
/**
* Callback method to be invoked when the selection disappears from this
* view. The selection can disappear for instance when touch is activated
* or when the adapter becomes empty.
*
* @param parent The AdapterView that now contains no selected item.
*/
void onNothingSelected(AdapterView<?> parent);
}
public void programmaticallySetPosition(int pos, boolean animate) {
mUserActionOnSpinner = false;
setSelection(pos, animate);
}
public void setOnItemSelectedListener (OnItemSelectedListener listener) {
mListener = listener;
}
public Spinner(Context context) {
super(context);
super.setOnItemSelectedListener(this);
}
public Spinner(Context context, int mode) {
super(context, mode);
super.setOnItemSelectedListener(this);
}
public Spinner(Context context, AttributeSet attrs) {
super(context, attrs);
super.setOnItemSelectedListener(this);
}
public Spinner(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
super.setOnItemSelectedListener(this);
}
public Spinner(Context context, AttributeSet attrs, int defStyle, int mode) {
super(context, attrs, defStyle, mode);
super.setOnItemSelectedListener(this);
}
}
Upvotes: 8
Reputation: 2108
I have come up with a workaround that is simple and generic. Refer the accepted answer to this question:
Undesired onItemSelected calls
So, if position
is not equal to spin.getTag(R.id.pos)
, you know that the callback was due to the user making a change, because whenever you yourself are making the change, you are setting the tag as spin.setTag(R.id.pos, pos)
where pos is the value you set. If you are using this approach, make sure to set the tag in onItemSelected
after you finish your work!
Upvotes: 1
Reputation: 129792
I don't know that this can be distinguished from within the method. Indeed, it is a problem that a lot of people are facing, that onItemSelected
is fired when the spinner is initiated. It seems that currently, the only workaround is to use an external variable for this.
private Boolean isUserAction = false;
...
public void onItemSelected( ... ) {
if( isUserAction ) {
// code for user initiated selection
} else {
// code for programmatic selection
// also triggers on init (hence the default false)
}
// reset variable, so that it will always be true unless tampered with
isUserAction = true;
}
public void myButtonClick( ... ) {
isUserAction = false;
mySpinner.setSelectedItem ( ... );
}
Upvotes: 10