Spinner: get state or get notified when opens
Asked Answered
P

8

58

Is it possible to know whether a Spinner is open or closed? It would even be better if there was some sort of onOpenListener for Spinners.

I've tried using an OnItemSelectedListener like this:

spinnerType.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {

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

        }

        @Override
        public void onNothingSelected(AdapterView<?> parent) {
            Log.d("nothing" , "selected");  
        }

    });

I can know that the window will close if something is selected (in executeSomething()). But I don't get notified if I click outside of the Dialog, which also closes the spinner

Phylys answered 26/8, 2013 at 14:55 Comment(11)
We can user onClickListner for spinner. Since spinner open when we click(tap) on it. And to chcek where the spinner is open or not we can use onItemSelectedListner callbacks. Tell me if you want further code example.Flagstad
that's not completely correct, because the items are displayed in a Dialog. If you click outside of the dialog, it closesPhylys
There is another method that you can use to handle when nothing is selected that is onNothingSelected with onItemSelectedListner. If you will ask further help i will write complete logic for you.Flagstad
I've just tried this by adding a Log to the onNothingSelected() method, but it does not work. It does not give me the log if I click outside of itPhylys
Can you please post the code that you are trying? If you will post that i can see that what you are doing wrong. I will be easy for me otherwise i have write the complete code.Flagstad
I think @Nachi has made answer for that and it will work for you... Cheers...Flagstad
I'm afraid the answer isn't correctPhylys
don't worry! so the following code did'nt solved your problem? can you explain why?Flagstad
Spinner is an AdapterView, not a View, so It does not have an OnClickListenerPhylys
Why do you want to know when the Spinner is opened or closed?Underpay
I need to change the layout of the Spinner 'header'. I've tried using a Selector with different states as background, but there's no way to know whether it's open or closedPhylys
U
112

Another option to watch for those events is to extend the Spinner class and use one of its methods(performClick() which will trigger its dialog/popup) followed by monitoring the focus of the window holding this custom Spinner. This should provide you with the wanted closed event for all the possible finishing possibilities(for either the dialog or dropdown mode).

The custom Spinner class:

public class CustomSpinner extends Spinner {

   /**
    * An interface which a client of this Spinner could use to receive
    * open/closed events for this Spinner. 
    */
    public interface OnSpinnerEventsListener {

        /**
         * Callback triggered when the spinner was opened.
         */
         void onSpinnerOpened(Spinner spinner);

        /**
         * Callback triggered when the spinner was closed.
         */
         void onSpinnerClosed(Spinner spinner);

    }

    private OnSpinnerEventsListener mListener;
    private boolean mOpenInitiated = false;

    // implement the Spinner constructors that you need

    @Override
    public boolean performClick() {
        // register that the Spinner was opened so we have a status
        // indicator for when the container holding this Spinner may lose focus
        mOpenInitiated = true;
        if (mListener != null) {
            mListener.onSpinnerOpened(this);
        }
        return super.performClick();
    }

    @Override
    public void onWindowFocusChanged (boolean hasFocus) {
        if (hasBeenOpened() && hasFocus) {
            performClosedEvent();
        }
    }

    /**
    * Register the listener which will listen for events.
    */
    public void setSpinnerEventsListener(
            OnSpinnerEventsListener onSpinnerEventsListener) {
        mListener = onSpinnerEventsListener;
    }

    /**
     * Propagate the closed Spinner event to the listener from outside if needed.
     */
    public void performClosedEvent() {
        mOpenInitiated = false;
        if (mListener != null) {
            mListener.onSpinnerClosed(this);
        }
    }

    /**
     * A boolean flag indicating that the Spinner triggered an open event.
     * 
     * @return true for opened Spinner 
     */
    public boolean hasBeenOpened() {
        return mOpenInitiated;
    }

}
Underpay answered 5/9, 2013 at 12:30 Comment(4)
Professional solutionLooming
The onWindowFocusChanged method can (and should) go inside the custom spinner class.Tenrec
If I click outside none of these methods is trigeredRare
Can use method in kotlin: spinner.setSpinnerEventsListener(object : CustomSpinner.OnSpinnerEventsListener { override fun onSpinnerOpened() { } override fun onSpinnerClosed() { } })Hyperbolize
C
41

based on @Luksprog wonderful solution,i just want to add a small change which will be very helpful in case someone is using the CustomSpinner inside a fragment. instead of using the Activity.onWindowFocusChanged function, we override the View.onWindowFocusChanged function. thus the whole CustomSpinner class become

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

public class CustomSpinner extends Spinner {
    private static final String TAG = "CustomSpinner";
    private OnSpinnerEventsListener mListener;
    private boolean mOpenInitiated = false;

    public CustomSpinner(Context context, AttributeSet attrs, int defStyleAttr, int mode) {
        super(context, attrs, defStyleAttr, mode);
    }

    public CustomSpinner(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

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

    public CustomSpinner(Context context, int mode) {
        super(context, mode);
    }

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

    public interface OnSpinnerEventsListener {

        void onSpinnerOpened();

        void onSpinnerClosed();

    }

    @Override
    public boolean performClick() {
        // register that the Spinner was opened so we have a status
        // indicator for the activity(which may lose focus for some other
        // reasons)
        mOpenInitiated = true;
        if (mListener != null) {
            mListener.onSpinnerOpened();
        }
        return super.performClick();
    }

    public void setSpinnerEventsListener(OnSpinnerEventsListener onSpinnerEventsListener) {
        mListener = onSpinnerEventsListener;
    }

    /**
     * Propagate the closed Spinner event to the listener from outside.
     */
    public void performClosedEvent() {
        mOpenInitiated = false;
        if (mListener != null) {
            mListener.onSpinnerClosed();
        }
    }

    /**
     * A boolean flag indicating that the Spinner triggered an open event.
     * 
     * @return true for opened Spinner
     */
    public boolean hasBeenOpened() {
        return mOpenInitiated;
    }

    @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        android.util.Log.d(TAG, "onWindowFocusChanged");
        super.onWindowFocusChanged(hasWindowFocus);
        if (hasBeenOpened() && hasWindowFocus) {
            android.util.Log.i(TAG, "closing popup");
            performClosedEvent();
        }
    }
}
Carbon answered 29/12, 2014 at 16:39 Comment(2)
I have multiple spinners and I want to identify which spinner dropdown is opened/closed. How can I do it ?Slab
@Slab You can change the listener to pass back the spinner as a parameter. Like so: void onSpinnerOpened(Spinner spinner); Then when you call it, mListener.onSpinnerOpened(this);. So then in the listener code just check which of the spinners it is.Lemal
C
13

Hi friends I am struggling on this issue from last two days and finally I got following solution which done my job. I tried and it worked perfectly. Thanks

 mSpinner.setOnTouchListener(new OnTouchListener(){

                @Override
                public boolean onTouch(View v, MotionEvent event) {
                   if(event.getAction() == MotionEvent.ACTION_DOWN){
                       Toast.makeText(MapActivity.this,"down",Toast.LENGTH_LONG).show();
                    // Load your spinner here
                   }
                    return false;
                }

            });
Concinnate answered 20/7, 2015 at 9:51 Comment(2)
This doesn't tell you whether spinner is expanded or collapsed.Shive
Much faster solution. ThanksKilloran
T
4

After spending the day looking at all the solutions, here is my easy fix for detecting the opening and closing of Spinner and also how focusing outside spinner closes the spinner.

Step 1: Adding a addOnWindowFocusChangeListener to your Spinner in Fragment or Activity.

 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    val spinner = spinner_view

    val arrayAdapter = ArrayAdapter<RestoreManager.ConnectionType>(context!!, R.layout.layout_backup_spinner)
    arrayAdapter.setDropDownViewResource(R.layout.spinner_item)

    spinner?.let {
        val spinnerAdapter = SpinnerAdapter(activity!!)
        it.adapter = spinnerAdapter 
        it.setSelection(0)

        it.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
            override fun onItemSelected(parentView: AdapterView<*>, selectedItemView: View?, position: Int, id: Long) {}

            override fun onNothingSelected(parentView: AdapterView<*>) {}
        }

        it.viewTreeObserver?.addOnWindowFocusChangeListener { hasFocus -> //This updates the arrow icon up/down depending on Spinner opening/closing
            spinnerAdapter .spinnerOpen = hasFocus
            spinnerAdapter .notifyDataSetChanged()
        }
    }
}

addOnWindowFocusChangeListener is called every time the spinner opens or closes. It also get triggered when spinner is open and user tap outside the spinner to close the spinner. In this method you can update the UI of your SpinnerAdapter.

For my use case, i wanted to show the arrow icon up and down when the spinner opens and closes. So i set the flag spinnerAdapter.spinnerOpen in my Spinner Adapter.

Step 2: In your SpinnerAdapter override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {} is called every time spinner open or close. Here is the code in SpinnerAdapter:

override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
    val spinView = if (convertView == null) {
        val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
        inflater.inflate(R.layout.layout_backup_spinner, null)
    } else {
        convertView
    }

    var arrowIcon = spinView.findViewById<ImageView>(R.id.arrow_icon)
    if (spinnerOpen) arrowIcon.setImageResource(R.drawable.arrow_down)
    else arrowIcon.setImageResource(R.drawable.arrow_up)

    return spinView
}
Tabulate answered 31/8, 2020 at 8:32 Comment(4)
Thanks, you saved my day. working good. But how do you know that spinner is opened or closed since it's firing the event for both.Pepin
hasFocus boolean will tell us if the spinner is open or closeTabulate
yeah, got that but how do you set the "hasFocus" ? I don't see the full code to understandPepin
hasFocus is just a variable you can use any name, and we are not settings this variable, when focus change listener is triggered it will tell if it has the focus or not. And that is the full code sample hereTabulate
E
2

There's no built in function but it's pretty easy to do with an OnTouchListener and OnItemSelectedListener.

abstract class OnOpenListener implements OnTouchListener, OnItemSelectedListener {

    public OnOpenListener(Spinner spinner) {
        spinner.setOnTouchListener(this);
        spinner.setOnItemSelectedListener(this);
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_UP) {
            onOpen();
        }
        return false;
    }

    @Override
    public void onItemSelected(AdapterView<?> arg0, View arg1, int arg2, long arg3) {
        onClose();
    }

    @Override
    public void onNothingSelected(AdapterView<?> arg0) {
        onClose();
    }

    abstract public void onOpen();

    abstract public void onClose();
}

And then assign the appropriate listeners:

    OnOpenListener onOpenListener = new OnOpenListener(mySpinner) {

        @Override
        public void onOpen() {
            // spinner was opened
        }

        @Override
        public void onClose() {
            // spinner was closed
        }
    };
Enforce answered 2/9, 2013 at 8:16 Comment(5)
I'm getting an error: "Don't call setOnClickListener for an AdapterView. You probably want setOnItemClickListener instead"Phylys
@dumazy: You're right, looks like the OnClickListener throws a runtime exception. I've edited the code to use OnTouchListeners instead.Enforce
I've also did this, it does not solve the problem. If you click outside of the dialog, it closes without being notified. I've looked at the code of Spinner on Grepcode and saw that the Dialog is created in performClick() but is never kept (in 1.5, I looked at 4.0.1 and there the Dialog is kept in the class, but is private). If you could access this Dialog, you could check if it's showing or not. And add a method for onTouchEvent() where the event is MotionEvent.ACTION_OUTSIDE, etc...Phylys
Is there a reason why you are using a Spinner inside a Dialog? You could instead use an AlertDialog with setSingleChoiceItems() and be able to handle a dismiss on touch outside much more easily.Enforce
I'm not using a Spinner in a Dialog. The list with all the options of a Spinner is a DialogPhylys
W
1

I think the best way to find when it got opened and closed is this way:

  1. If it was closed, and now it calls "getDropDownView" in the adapter, it can be assumed that it got opened.

  2. If "onItemSelected" or "onNothingSelected" are called, now it got closed.


EDIT: here's a sample code

public class MainActivity extends AppCompatActivity {
    boolean isSpinnerClosed = true;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        AppCompatSpinner spinner2 = (AppCompatSpinner) findViewById(R.id.spinner2);
        List<String> list = new ArrayList<String>();
        list.add("list 1");
        list.add("list 2");
        list.add("list 3");
        Log.d("AppLog", "started");
//spinner2.setondi
        ArrayAdapter<String> dataAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, list) {
            @Override
            public View getDropDownView(final int position, @Nullable final View convertView, @NonNull final ViewGroup parent) {
                if (isSpinnerClosed) {
                    Log.d("AppLog", "closed->open");
                    isSpinnerClosed = false;
                }
                return super.getDropDownView(position, convertView, parent);
            }
        };
        spinner2.setOnItemSelectedListener(new OnItemSelectedListener() {
            @Override
            public void onItemSelected(final AdapterView<?> adapterView, final View view, final int i, final long l) {
                Log.d("AppLog", "onItemSelected");
                if (!isSpinnerClosed) {
                    Log.d("AppLog", "open->closed");
                    isSpinnerClosed = true;
                }
            }

            @Override
            public void onNothingSelected(final AdapterView<?> adapterView) {
                Log.d("AppLog", "onNothingSelected");
                if (!isSpinnerClosed) {
                    Log.d("AppLog", "open->closed");
                    isSpinnerClosed = true;
                }
            }
        });
        dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
        spinner2.setAdapter(dataAdapter);
    }

    @Override
    public void onWindowFocusChanged(final boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (hasFocus && isSpinnerClosed) {
            Log.d("AppLog", "open->closed");
            isSpinnerClosed = true;
        }
    }
}
Woebegone answered 27/10, 2015 at 8:2 Comment(17)
getDropDownView is calling and continue the call after the spinner is being closed.Please provide any other solutionChangeless
What do you mean exactly?Woebegone
Means just put toast in getDropDownView() method and when spinner got opened by user toast show repeatedly.I tried to put logs in the method getDropDownView() and guess what lot of logs once user opened the spinner.That shows getDropDownView() method called number of time.Anyway going with the answered provided by @CarbonChangeless
@Changeless Can't confirm this issue. Created a sample and updated answer, if you wish to try. The only issue I've found, is when the user clicked outside of the spinner (or pressed the back key), when it's opened, which causes it to close without calling "onNothingSelected".Woebegone
@Changeless OK, found a workaround for this too. In your activity, override the function onWindowFocusChanged. When you know the spinner got opened, and this function is called with "hasFocus" set to true, it means the popup of the spinner was closed. Updated the sample codeWoebegone
Since this is quite a basic thing to have, and those workarounds are too weird, I decided to create a request for this feature. the post also includes the sample code. Please consider starring it : issuetracker.google.com/issues/66836611Woebegone
convertView is null when this method is used.Nuts
@Nuts So?Woebegone
Nevermind. I found another method to achieve this behavior based on an OnTouchListener. I was confused though why you are passing a null convertView to the super method.Nuts
@Nuts Where exactly do I pass null convertView to the super method ? I see this: "return super.getDropDownView(position, convertView, parent);" . And why the downvote? Can you please share your solution as well?Woebegone
You can disregard my comment about the null convertView. I downvoted because your solution didn't work for me. It might work with some modifications, but as I said, I went with another solution. By the way, ´isSpinnerClosed´ is not declared in your code sample.Nuts
@Nuts Are you sure you tried the sample? It has "isSpinnerClosed" in it. Please check again. Here's the link: issuetracker.google.com/issues/66836611 . I will now update the code here as well. But why I still see downvote ?Woebegone
Yes, I see it now. Maybe when I tried the first time I made a copy-paste error. Sorry for that.Nuts
@Nuts But it works for you or not? And, can you please show an alternative code ? Maybe what you wrote is better...Woebegone
My solution is very similar to the one provided by @Amol Suryawanshi (https://mcmap.net/q/328964/-spinner-get-state-or-get-notified-when-opens). I don't know if it is better or not than yours, but for me it works and was a bit simpler. I don't need to distinguish between open and closed events. I took away my downvote of your answer because I realized it was based on my own mistake.Nuts
@Nuts Well I did want to know when it gets opened or closed.Woebegone
fair enough. The original question is a bit ambiguous, in my opinion.Nuts
O
0

I could not find a way to get this behaviour with the spinner so the only thing that worked for me was to use the spinner (custom) adapter instead:

public interface SpinnerListener {

    void onSpinnerExpanded();   

    void onSpinnerCollapsed();
}

Then a custom adapter can be written that just grabs the “spinner expanded” view and adds a listener to it to listen for “expand” and “collapse” events. The custom adapter I used is:

public class ListeningArrayAdapter<T> extends ArrayAdapter<T> {
        private ViewGroup itemParent;
        private final Collection<SpinnerListener> spinnerListeners = new ArrayList<SpinnerListener>();

    public ListeningArrayAdapter(Context context, int resource, T[] objects) {
        super(context, resource, objects);
    }

    // Add the rest of the constructors here ...


    // Just grab the spinner view (parent of the spinner item view) and add a listener to it.
    @Override
    public View getDropDownView(int position, View convertView, ViewGroup parent) {
        if (isParentTheListView(parent)) {
            itemParent = parent;
            addFocusListenerAsExpansionListener();
        }

        return super.getDropDownView(position, convertView, parent);
    }

    // Assumes the item view parent is a ListView (which it is when a Spinner class is used)
    private boolean isParentTheListView(ViewGroup parent) {
        return (parent != itemParent && parent != null && ListView.class.isAssignableFrom(parent.getClass()));      
    }

    // Add a focus listener to listen to spinner expansion and collapse events.
    private void addFocusListenerAsExpansionListener() {
        final View.OnFocusChangeListener listenerWrapper = new OnFocusChangeListenerWrapper(itemParent.getOnFocusChangeListener(), spinnerListeners);
        itemParent.setOnFocusChangeListener(listenerWrapper);       
    }

    // Utility method.
    public boolean isExpanded() {
        return (itemParent != null && itemParent.hasFocus());
    }

    public void addSpinnerListener(SpinnerListener spinnerListener) {
        spinnerListeners.add(spinnerListener);
    }

    public boolean removeSpinnerListener(SpinnerListener spinnerListener) {
        return spinnerListeners.remove(spinnerListener);    
    }

    // Listener that listens for 'expand' and 'collapse' events.
    private static class OnFocusChangeListenerWrapper implements View.OnFocusChangeListener {
        private final Collection<SpinnerListener> spinnerListeners;
        private final View.OnFocusChangeListener originalFocusListener;

        private OnFocusChangeListenerWrapper(View.OnFocusChangeListener originalFocusListener, Collection<SpinnerListener> spinnerListeners) {
            this.spinnerListeners = spinnerListeners;
            this.originalFocusListener = originalFocusListener;
        }

        @Override
        public void onFocusChange(View view, boolean hasFocus) {
            if (originalFocusListener != null) {
                originalFocusListener.onFocusChange(view, hasFocus); // Preserve the pre-existing focus listener (if any).
            }

            callSpinnerListeners(hasFocus);
        }

        private void callSpinnerListeners(boolean hasFocus) {
            for (SpinnerListener spinnerListener : spinnerListeners) {
                if (spinnerListener != null) {
                    callSpinnerListener(hasFocus, spinnerListener);
                }
            }           
        }

        private void callSpinnerListener(boolean hasFocus, SpinnerListener spinnerListener) {
            if (hasFocus) {
                spinnerListener.onSpinnerExpanded();
            }
            else {
                spinnerListener.onSpinnerCollapsed();
            }           
        }
    }
}

Then when I use a spinner in my activity or fragment all I had to do was to set the spinner adapter to the above custom adapter:

private ListeningArrayAdapter<CharSequence> adapter;

private Spinner buildSpinner() {
    final CharSequence[] items = {"One", "Two", "Three"};
    final Spinner spinner = (Spinner)getActivity().getLayoutInflater().inflate(R.layout.item_spinner, null);            
    adapter = new ListeningArrayAdapter<CharSequence>(getActivity(), R.layout.item_spinner_item, items);
    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    adapter.addSpinnerListener(new TestSpinnerListener(getActivity())); // Add your own spinner listener implementation here.
    spinner.setAdapter(adapter);

    return spinner;
}

I know that this is a bit of a hack and a a bit brittle but it worked for me. It would be much better if the Spinner class had all this functionality build in and allowed you to set an expand-collapse listener. For the time being I will have to do with this hack.

Ostraw answered 5/8, 2014 at 11:31 Comment(0)
G
0

You need to use Reflection and get access to the private field 'mPopup' and then set the method setOnDismissListener(), which is triggered when the pop-up is closed no matter it the user clicks on the empty area or selects new item. You can learn more about how it works here: https://mcmap.net/q/331491/-catching-an-event-when-spinner-drop-down-is-dismissed

Here is the full source code for the custom Spinner

open class CustomSpinner: androidx.appcompat.widget.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)

    lateinit var listPopupWindow: ListPopupWindow
    lateinit var onPopUpClosedListener: (dropDownMenu: DropDownMenu) -> Unit
    lateinit var onPopUpOpenedListener: (dropDownMenu: DropDownMenu) -> Unit

    init {

        try {

            // get the listPopupWindow
            val listPopupWindowField = androidx.appcompat.widget.AppCompatSpinner::class.java.getDeclaredField("mPopup")
            listPopupWindowField.isAccessible = true
            listPopupWindow = listPopupWindowField.get(this) as ListPopupWindow
            listPopupWindow.isModal = false

        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    override fun performClick(): Boolean {
        val returnValue = super.performClick()

        // indicate that the pop-up was opened
        if (::onPopUpOpenedListener.isInitialized) {
            onPopUpOpenedListener.invoke(this)
        }

        try {

            // get the popupWindow
            val popupWindowField = ListPopupWindow::class.java.getDeclaredField("mPopup")
            popupWindowField.isAccessible = true
            val popupWindow = popupWindowField.get(listPopupWindow) as PopupWindow

            // get the original onDismissListener
            val onDismissListenerField = PopupWindow::class.java.getDeclaredField("mOnDismissListener")
            onDismissListenerField.isAccessible = true
            val onDismissListener = onDismissListenerField.get(popupWindow) as PopupWindow.OnDismissListener

            // now override the original OnDismissListener
            listPopupWindow.setOnDismissListener {

                // indicate that the pop-up was closed
                if (::onPopUpClosedListener.isInitialized) {
                    onPopUpClosedListener.invoke(this)
                }

                // now we need to call the original listener that will remove the global OnLayoutListener
                onDismissListener.onDismiss()
            }

        } catch (e: Exception) {
            e.printStackTrace()
        }

        return returnValue
    }
}

And then simply attach listeners to your custom spinner

val customSpinner = findViewById<CustomSpinner>(R.id.mySpinner)
customSpinner.onPopUpClosedListener = { spinner: CustomSpinner ->
    // called when the pop-up is closed
}

customSpinner.onPopUpOpenedListener = { spinner: CustomSpinner ->
    // called when the pop-up is opened      
}
Ganesha answered 13/9, 2021 at 2:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.