简体   繁体   中英

How can I get an event in Android Spinner when the current selected item is selected again?

I have written an setOnItemSelectedListener for spinner to respond when the spinner item is changed. My requirement is when I clicks again the currently selected item, a toast should display. How to get this event? When the currently selected item is clicked again, spinner is not responding. `

    StorageSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener(){

        @Override
        public void onItemSelected(AdapterView adapter, View v, int i, long lng) {              
            Toast.makeText(getApplicationContext(), (CharSequence) StorageSpinner.getSelectedItem(), Toast.LENGTH_SHORT).show();

        }

        @Override
        public void onNothingSelected(AdapterView arg0) {
            Toast.makeText(getApplicationContext(), "Nothing selected", Toast.LENGTH_SHORT).show();

        }
    });  

I spent a good few hours trying to get something to solve this problem. I ended up with the following. I'm not certain if it works in all cases, but it seems to work for me. It's just an extension of the Spinner class which checks the selection and calls the listener if the selection is set to the same value.

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


/** Spinner extension that calls onItemSelected even when the selection is the same as its previous value */
public class NDSpinner extends Spinner {

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

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

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

    @Override 
    public void setSelection(int position, boolean animate) {
        boolean sameSelected = position == getSelectedItemPosition();
        super.setSelection(position, animate);
        if (sameSelected) {
            // Spinner does not call the OnItemSelectedListener if the same item is selected, so do it manually now
            getOnItemSelectedListener().onItemSelected(this, getSelectedView(), position, getSelectedItemId());
        }
    } 

    @Override
    public void setSelection(int position) {
        boolean sameSelected = position == getSelectedItemPosition();
        super.setSelection(position);
        if (sameSelected) {
            // Spinner does not call the OnItemSelectedListener if the same item is selected, so do it manually now
            getOnItemSelectedListener().onItemSelected(this, getSelectedView(), position, getSelectedItemId());
        }
    }

}

try this

public class MySpinner extends Spinner{

OnItemSelectedListener listener;

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

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

        if (position == getSelectedItemPosition())
        {
            listener.onItemSelected(null, null, position, 0);
        }       
    }

    public void setOnItemSelectedListener(OnItemSelectedListener listener)
    {
        this.listener = listener;
    }
}

This spinner will always tell you that selection has changed:

package com.mitosoft.ui.widgets;

import java.lang.reflect.Method;
import android.content.Context;
import android.content.DialogInterface;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.AdapterView;
import android.widget.Spinner;

//com.mitosoft.ui.widgets.NoDefaultSpinner
public class NoDefaultSpinner extends Spinner {

    private int lastSelected = 0;
    private static Method s_pSelectionChangedMethod = null;


    static {        
        try {
            Class noparams[] = {};
            Class targetClass = AdapterView.class;

            s_pSelectionChangedMethod = targetClass.getDeclaredMethod("selectionChanged", noparams);            
            if (s_pSelectionChangedMethod != null) {
                s_pSelectionChangedMethod.setAccessible(true);              
            }

        } catch( Exception e ) {
            Log.e("Custom spinner, reflection bug:", e.getMessage());
            throw new RuntimeException(e);
        }
    }

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

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

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

    public void testReflectionForSelectionChanged() {
        try {
            Class noparams[] = {};          
            s_pSelectionChangedMethod.invoke(this, noparams);
        } catch (Exception e) {
            Log.e("Custom spinner, reflection bug: ", e.getMessage());
            e.printStackTrace();                
        }
    } 




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

            lastSelected = which;
    }
}

I figured I'd leave an updated answer for those working on newer Android versions.

I compiled together a function from the above answers that will work for at least 4.1.2 and 4.3 (the devices I tested on). This function doesn't use reflection, but instead tracks the last selected index itself, so should be safe to use even if the SDK changes how the classes extend each other.

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

public class SelectAgainSpinner extends Spinner {

    private int lastSelected = 0;

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

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

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

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if(this.lastSelected == this.getSelectedItemPosition() && getOnItemSelectedListener() != null)
            getOnItemSelectedListener().onItemSelected(this, getSelectedView(), this.getSelectedItemPosition(), getSelectedItemId());
        if(!changed)
            lastSelected = this.getSelectedItemPosition();

        super.onLayout(changed, l, t, r, b);
    } 
}

kotlin , hope it helps

import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatSpinner

class MoreSpinner : AppCompatSpinner {

    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    override fun setSelection(position: Int, animate: Boolean) {
        val sameSelected = position == selectedItemPosition
        super.setSelection(position, animate)
        if (sameSelected) {
            onItemSelectedListener?.onItemSelected(
                this,
                selectedView,
                position,
                selectedItemId
            )
        }
    }

    override fun setSelection(position: Int) {
        val sameSelected = position == selectedItemPosition
        super.setSelection(position)
        if (sameSelected) {
            onItemSelectedListener?.onItemSelected(
                this,
                selectedView,
                position,
                selectedItemId
            )
        }
    }
}

for newer platforms try to add this to Dimitar's solution. I think it works :)

(you have to override onLayout and to remove onClick method)

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

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    if(this.lastSelected == this.getSelectedItemPosition())
        testReflectionForSelectionChanged();
    if(!changed)
        lastSelected = this.getSelectedItemPosition();

    super.onLayout(changed, l, t, r, b);
} 

What I found is, OnItemSelectedListener won't be called if the same item is selected again in a spinner. When i click on spinner and again select the same value, then OnItemSelectedListener method is not called. People do not expect something to happen if they click on a selection that is already active as per the UI design.

This is not full solution but it works if you only want to call this when you're getting back to your fragment/activity from wherever.

Considering mSpinner is your Spinner view, we call its listener like this:

@Override public void onResume() {
        if (mSpinner.getCount() > 0) {
            mSpinner.getOnItemSelectedListener()
                    .onItemSelected( mSpinner, null, mSpinner.getSelectedItemPosition(), 0 );
        }
        super.onResume();
    }

Continue from this answer https://stackoverflow.com/a/17099104/7392507

In your xml file, create a spinner. Replace the spinner tag <Spinner> with <yourdomain.yourprojectname.yourpackagename.spinnerclassname> for example <com.company.appname.utility.MySpinner> .

In your java file, define the spinner variable as below: - Spinnerclassname varName = findViewById(R.id.spinnerId);

For example: - MySpinner varSpinner = findViewById(R.id.spinnerId);

@Dimitar. WOW, brilliant work. Thanks for that. I can't upvote your solution (not enough points) but the NoDefaultSpinner class WORKS. Only this one thing was a problem: because you call super.onClick and then testReflectionForSelectionChanged() inside "OnClick", you will get the onItemSelected handler for the spinner being called twice if the selection DOES actually change (while functionality is correct if the same item is re-selected). I solved this by hacking around it. I added an onTouchEvent override that recorded which item was touched, then checked if this had changed in "onClick":

private Object ob=null; //class level variable
 @Override
public boolean onTouchEvent(MotionEvent m)
{
    if (m.getAction()==MotionEvent.ACTION_DOWN)
    {
        ob=this.getSelectedItem();
    }
    return super.onTouchEvent(m);
}
@Override
public void onClick(DialogInterface dialog, int which) {    
    super.onClick(dialog, which);
    if (this.getSelectedItem().equals(ob))
        testReflectionForSelectionChanged();
}

My solution is Based on MySpinner by benoffi7. Fixes passing nulls on same item selection by saving the last selected parent and view.

public class MySpinner extends Spinner {

    private OnItemSelectedListener listener;
    private AdapterView<?> lastParent;
    private View lastView;
    private long lastId;

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

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

    private void initInternalListener() {
        super.setOnItemSelectedListener(new OnItemSelectedListener() {

            @Override
            public void onItemSelected(AdapterView<?> parent, View view,
                int position, long id) {
                lastParent = parent;
                lastView = view;
                lastId = id;
                if (listener != null) {
                    listener.onItemSelected(parent, view, position, id);
                }
            }

            @Override
            public void onNothingSelected(AdapterView<?> parent) {
                //lastParent = parent; // do we need it?
                if (listener != null) {
                    listener.onNothingSelected(parent);
                }
            }
        });
    }

    @Override
    public void setSelection(int position) {
        if (position == getSelectedItemPosition() && listener != null) {
            listener.onItemSelected(lastParent, lastView, position, lastId);
        } else {
            super.setSelection(position);
        }
    }

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

The Spinner behaviour is not expected for our requeriments. My solution is not work with Spinners, make it in one similar way, with one ListView inside one BaseFragment to research the functionality we are expected.

The beneficts are:

  1. No more headaches extending Spinner defaults.
  2. Easy implementation and customization.
  3. Full compatibility along all Android APIs.
  4. No face against first OnItemSelectedListener.onItemSelected call.

The main idea, is do something like this:

The BaseFragment layout could looks similar to:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:background="@null"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical"
              android:gravity="center">

    <ListView
            android:id="@+id/fragment_spinnerList"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
</LinearLayout>

The code looks something like this:

public class SpinnerListFragment extends android.support.v4.app.DialogFragment {

    static SpinnerListFragment newInstance(List<String> items) {

        SpinnerListFragment spinnerListFragment = new SpinnerListFragment();
        Bundle args = new Bundle();

        args.putCharSequenceArrayList("items", (ArrayList) items);
        spinnerListFragment.setArguments(args);

        return spinnerListFragment;
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {

        Dialog dialog = new Dialog(getActivity(), R.style.dialog);
        final View view = getActivity().getLayoutInflater().inflate(R.layout.dialog_spinner_list, null);

        dialog.getWindow().setContentView(view);
        dialog.setCanceledOnTouchOutside(true);

        // CUSTOMIZATION...

        final List items = (ArrayList) getArguments().getCharSequenceArrayList("items");

        final ListView spinnerList = (ListView) view.findViewById(R.id.fragment_spinnerList);

        ArrayAdapter<String> arrayAdapter =
                new ArrayAdapter<String>(
                        getActivity(),
                        R.layout.search_spinner_list_item,
                        items);

        spinnerList.setAdapter(arrayAdapter);

        spinnerList.setOnItemClickListener(new AdapterView.OnItemClickListener() {

            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {

                // DO SOMETHING...

                SpinnerListFragment.this.dismiss();
            }
        });

        return dialog;
    }

}
class MySpinner extends Spinner {


    public MySpinner(Context context) {
        super(context);
        // TODO Auto-generated constructor stub
    }
    public MySpinner(Context context, AttributeSet attrs)
    {
        super(context, attrs);
    }
    @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);
    }

}

This has an easy solution because it is possible to set the "selected item" programmatically based on "onTouch", like this:

spinnerobject.setOnTouchListener(new AdapterView.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {

            final int MAKE_A_SELECTION = 1; //whatever index that is the normal starting point of the spinner.
            spinnerObject.setSelection(MAKE_A_SELECTION);
            return false;
        }
    });

There is a tiny penalty in that (1) the spinner text will change back to the default just prior to the spinner rows being displayed, and (2), this default item will be part of the list. Perhaps someone can add how to disable a particular item for a spinner?

Generally speaking, since a spinner selection may execute a repeatable event (like starting a search), the lack of possibility to reselect an item in a spinner is really a missing feature/bug in the Spinner class, a bug that Android developers should correct ASAP.

Hi man, this worked for me:

ArrayAdapter<String> adaptador1 = new ArrayAdapter<String>( Ed_Central.this, 
                                                            android.R.layout.simple_spinner_item,
                                                            datos1
                                                            );
        lista1.setAdapter( adaptador1 );

        lista1.setOnItemSelectedListener( new AdapterView.OnItemSelectedListener() {
            @Override
            public void onItemSelected( AdapterView<?> parent, View view, int position, long id ) {

                lista1.setSelection( 0 );

                switch ( position ) {
                    case 1:
                        Toast.makeText( getApplicationContext(), "msg1", Toast.LENGTH_SHORT ).show();
                        break;
                    case 2:
                        Toast.makeText( getApplicationContext(), "msg2", Toast.LENGTH_SHORT ).show();
                        break;
                    case 3:
                        Toast.makeText( getApplicationContext(), "msg3", Toast.LENGTH_SHORT ).show();
                        break;

                    default:
                        break;
                }
            }

Always the adapter is going to be at position "0", and you can show your toast.

package customclasses;

/**
 * Created by Deepak on 7/1/2015.
 */

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

/**
 * Spinner extension that calls onItemSelected even when the selection is the same as its previous value
 */
public class NDSpinner extends Spinner {
    public boolean isDropDownMenuShown=false;

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

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

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


    @Override
    public void
    setSelection(int position, boolean animate) {
        boolean sameSelected = position == getSelectedItemPosition();
        super.setSelection(position, animate);
        if (sameSelected) {
            // Spinner does not call the OnItemSelectedListener if the same item is selected, so do it manually now
            getOnItemSelectedListener().onItemSelected(this, getSelectedView(), position, getSelectedItemId());
        }
    }

    @Override
    public boolean performClick() {
        this.isDropDownMenuShown = true; //Flag to indicate the spinner menu is shown
        return super.performClick();
    }
    public boolean isDropDownMenuShown(){
        return isDropDownMenuShown;
    }
    public void setDropDownMenuShown(boolean isDropDownMenuShown){
        this.isDropDownMenuShown=isDropDownMenuShown;
    }
    @Override
    public void
    setSelection(int position) {
        boolean sameSelected = position == getSelectedItemPosition();
        super.setSelection(position);
        if (sameSelected) {
            // Spinner does not call the OnItemSelectedListener if the same item is selected, so do it manually now
            getOnItemSelectedListener().onItemSelected(this, getSelectedView(), position, getSelectedItemId());
        }
    }
    @Override
    public void onDetachedFromWindow() {
        super.onDetachedFromWindow();
    }
}

Custom spinner with same item selection callback in Kotlin :

class StorageSpinner : AppCompatSpinner {

    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    interface StorageSpinnerSelectionCallback {
        fun onItemSelected(position: Int)
    }

    private var selectionCallback: StorageSpinnerSelectionCallback? = null

    fun setSelectionCallback(selectionCallback: StorageSpinnerSelectionCallback) {
        this.selectionCallback = selectionCallback
    }

    fun removeSelectionCallback() {
        selectionCallback = null
    }

    override fun setSelection(position: Int) {
        super.setSelection(position)

        if (position == selectedItemPosition) selectionCallback?.onItemSelected(position)
    }
}

Try this, this might be not the proper solution but is the best working solution available right now, if not making a custom spinner.

what you have to do is to reset spinner adapter on every item click,

 @Override
    public void onItemSelected(AdapterView adapter, View v, int position, long lng) {              

    if (position == getSelectedItemPosition())
    {
        //do your thing
        //then at end of statement reset adapter like

       spinner.setAdapter(adapter);
    } 

    }

I hope it helped you solve your problem

Hello & thanks @Dimitar for a creative answer to the problem. I've tried it and it works well on older Android versions like 2.x, but unfortunately it doesn't work on version 3.0 and later (tried 3.2 and 4.0.3). For some reason the onClick method is never called on newer platforms. Someone has written a bug report for this here: http://code.google.com/p/android/issues/detail?id=16245

Not functioning on newer platforms means I needed a different solution. In my application it was sufficient to simulate an unselected spinner with a hidden "dummy" entry at the start. Then every item clicked will result in a callback if the "hidden" item is set as the selection. A drawback for some may be that nothing will appear selected, but that could be fixed using Spinner class override tricks.

See How to hide one item in an Android Spinner

if u really want to do this task in your XML when your spinner display add one edit text and set visibility gone attribute; and create costume adapter for spinner and in costume adapter set on view.onclicklisner and when clickevent fired EditText.setText("0"); and in activity set edittext textWatcher Event and in event block you add your onSppinerItem Event Block code; Your Problem Solved

您必须执行以下步骤: 1. 为微调器创建一个 Adapter Customer 2. 在覆盖 fun getDropDownView(...) 中,您必须放置一个 view.setOnClickListener {"interface"} 3. 创建一个接口

There is no need to extend the spinner. I use a SpinnerAdapter in the following manner:

    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle  savedInstanceState) {
    //....
    //Create an ArrayAdapter as usual
    myArrayAdapter = new ArrayAdapter(mParentView.getContext(),R.layout.myarrayadapter_item);
    //Create a Spinner as usual
    mySpinner = (Spinner)mParentView.findViewById(R.id.myspinner);

    //create a spinneradapter
    //selecting the same item twice will not throw an onselectionchange event
    //therefore add an OnTouchlistener to the DropDownView items
    SpinnerAdapter o_SpinnerAdapter = new SpinnerAdapter() {
            private ArrayAdapter m_ArrayAdapter = myArrayAdapter;
            private View.OnTouchListener m_OnTouchListener = new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                    //do something
                   return false; //return false, don't consume the event
            };
            @Override
            public View getDropDownView(int position, View convertView, ViewGroup parent) {
                    TextView o_TextView; //usual the view is a TextView
                    o_TextView = (TextView)m_ArrayAdapter.getView(position,convertView,parent);
                    o_TextView.setOnTouchListener(m_OnTouchListener); //Add the ontouchlistener
                    return o_TextView;
            }

            @Override
            public void registerDataSetObserver(DataSetObserver observer) {
                    m_ArrayAdapter.registerDataSetObserver(observer);
            }
            @Override
            public void unregisterDataSetObserver(DataSetObserver observer) {
                    m_ArrayAdapter.unregisterDataSetObserver(observer);
            }
            @Override
            public int getCount() {
                    return m_ArrayAdapter.getCount();
            }
            @Override
            public Object getItem(int position) {
                    return m_ArrayAdapter.getItem(position);
            }
            @Override
            public long getItemId(int position) {
                    return m_ArrayAdapter.getItemId(position);
            }
            @Override
            public boolean hasStableIds() {
                    return m_ArrayAdapter.hasStableIds();
            }
            @Override
            public View getView(int position, View convertView, ViewGroup parent) {
                    return m_ArrayAdapter.getView(position, convertView, parent);
            }
            @Override
            public int getItemViewType(int position) {
                    return m_ArrayAdapter.getItemViewType(position);
            }
            @Override
            public int getViewTypeCount() {
                    return m_ArrayAdapter.getViewTypeCount();
            }
            @Override
            public boolean isEmpty() {
                    return m_ArrayAdapter.isEmpty();
            }
    };
    //Set the SpinnerAdapter instead of myArrayAdapter
    m_SpinnerDMXDeviceGroups.setAdapter(o_SpinnerAdapter);

    //.......
    }

Hope it helps

This threat was very helpful for me. There are two remarks which might help someone:

First:

You should check if the OnItemSelecListener is already set. In my case I called setSelection() before I set the OnItemSelecListener, which lead to a crash.

@Override
public void setSelection(int position) {
    boolean sameSelected = position == getSelectedItemPosition();
    super.setSelection(position);
    if (sameSelected) {
        // Spinner does not call the OnItemSelectedListener if the same item is selected, so do it manually now
        AdapterView.OnItemSelectedListener selLis = getOnItemSelectedListener();
        if (selLis != null) {
            selLis.onItemSelected(this, getSelectedView(), position, getSelectedItemId());
        }
    }
}

Second:

Android Studio asks me not to derive from Spinner but from AppCompatSpinner. This looks like this:

import androidx.appcompat.widget.AppCompatSpinner;

public class SpinnerReclickable  extends AppCompatSpinner {
...

Just try out with this.

@Override
public void onItemSelected(AdapterView adapter, View v, int i, long lng) {  

    if(v.hasFocus() {            
        Toast.makeText(getApplicationContext(), (CharSequence) StorageSpinner.getSelectedItem(), Toast.LENGTH_SHORT).show();
    }    
}

Hope it may work.

When you clicks again the currently selected item, then it can not fire any event. So you can not catch setOnItemSelectedListener for spinner to respond.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM