I have had the same problem and I wanted to detecting when the the pop-up is closed, no matter if it was clicked outside or element was selected. I have no idea why Google don't wants to add simple listeners that we can use to detect such a crucial things. It is 2021 and there is still no good way of detecting it, really Google???
Obviously the solution is to use Reflection and getting access to the private variables. As @Kanth suggested we need to get access to the OnDismissListener(). But his answer is little outdated especially if you intend on using AppCompatSpinner
On further inspection we can see see that the AppCompatSpinner contains the private object 'mPopup' and it is from the interface SpinnerPopup type.
private SpinnerPopup mPopup;
This interface is then used by the class DropdownPopup and is implementing its method and we need to look more closely at the implemented method show(). If we go down we can see that it sets the OnDismissListener(). So the listener is then used to remove the global layout listener using the method removeGlobalOnLayoutListener(). So we cannot directly change the setOnDismissListener because, the global layout listener that was previously added needs to be removed.
Now we need to find where exactly is the listener stored and then we need to get that value and keep it. Then set the new OnDismissListener where we can detect the closing of the pop-up. And finally it is very important to call the original OnDismissListener, so it can remove the global layout listener. So calling the method setOnDismissListener() is going inside the ListPopupWindow class and it is calling the same method from its 'mPopup' object.
And finally we get to the end method and the class that stores the listener reference object. The object is called mOnDismissListener and we need to hold reference to it when we set the new listener using the method setOnDismissListener().
So we need to override the spinner class and somehow override the OnDismissListener and to do that we need to go 3 parent classes down the rabbit hole.
CustomSpinner
├── AppCompatSpinner (mPopup)
│ ├── ListPopupWindow (mPopup)
| | ├── PopupWindow (mOnDismissListener) finally!!!
| | |
|────────
- We create custom class CustomSpinner that implements the AppCompatSpinner class
- We need to get access to the private object mPopup from the AppCompatSpinner
- Then we need to get the private object mPopup from the ListPopupWindow class
- Then we need to get the private object mOnDismissListener from the PopupWindow class
Now we need to find a method that is called after the method show() from the DropdownPopup class that is declared inside the AppCompatSpinner class. But this method should be triggered before the original OnDismissListener is called. And this specials method is performClick(), which is called when the user clicks on the spinner and then the show() method is triggered that then attaches the original OnDismissListener.
So here are the steps that we need to do inside the performClick() method:
- Store the reference to the original OnDismissListener
- Set the new OnDismissListener that is called when the pop-up is closed
- Call a custom listener onPopUpClosedListener that we can use
- Finally the most important invoke the original OnDismissListener using the reference we stored previously in order to remove the global layout listener
So here is the final source code for our custom Spinner class
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
init {
try {
// get private property and make it accessible
val listPopupWindowField = androidx.appcompat.widget.AppCompatSpinner::class.java.getDeclaredField("mPopup")
listPopupWindowField.isAccessible = true
// get the list popup window
listPopupWindow = listPopupWindowField.get(this) as ListPopupWindow
listPopupWindow.isModal = false
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun performClick(): Boolean {
val returnValue = super.performClick()
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 {
// here we detect when the drop down is dismissed and call the listener
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 we can simply use the onPopUpClosedListener listener and detect when the pop-up is closed.
val customSpinner: CustomSpinner = findViewById(R.id.mySpinner)
customSpinner.onPopUpClosedListener = {
// here we detect when the pop-up from our custom spinner is closed
}
setOnItemSelectedListener()
? – Diahann