Unfortunately no chances to catch the touch outside event using setOnDismissListener
or dismissDropDown()
; the latter gets called when item is selected.
But that would be possible by registering OnDismissListener
on the instance of the inner popup window of the AutoCompleteTextView
which is of type ListPopupWindow
through reflections:
private fun getPopup(): ListPopupWindow? {
try {
val field = AutoCompleteTextView::class.java.getDeclaredField("mPopup")
field.isAccessible = true
return field.get(this) as ListPopupWindow
} catch (e: NoSuchFieldException) {
e.printStackTrace()
} catch (e: IllegalAccessException) {
e.printStackTrace()
}
return null
}
Hopefully, the documentation can solve this to avoid this anti-pattern.
The OnDismissListener
callback gets called on any type of dismissal of the menu; either clicking on items, touching outside the AutoCompleteTextView
or hitting the soft keyboard back button. This can be distinguished by tagging the ACTT
with the appropriate flag for each event; an enum
is used for that in the below customized class):
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.util.AttributeSet
import android.util.Log
import android.view.KeyEvent
import android.view.View
import android.view.ViewTreeObserver
import android.widget.AdapterView
import android.widget.AutoCompleteTextView
import android.widget.ListPopupWindow
import androidx.appcompat.widget.AppCompatAutoCompleteTextView
class TouchOutsideAutoCompleteTextView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : AppCompatAutoCompleteTextView(context, attrs), AdapterView.OnItemClickListener {
init {
super.setOnItemClickListener(this)
viewTreeObserver.addOnGlobalLayoutListener(object :
ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
viewTreeObserver
.removeOnGlobalLayoutListener(this)
else
viewTreeObserver
.removeGlobalOnLayoutListener(this)
// Registering the mPopup window OnDismissListener
adjustTouchOutside()
}
})
}
private var consumerListener: AdapterView.OnItemClickListener? = null
private enum class DismissEvent {
ON_ITEM_CLICK, // Should be set to enable the next touch dispatch event whenever the dismiss is due to a click on the menu item.
ON_TOUCH_OUTSIDE, // Should be set to disable the next touch dispatch event whenever the dismiss is due to a touch outside the menu.
ON_BACK_PRESSED // Should be set to enable the next touch dispatch event whenever the dismiss is due to the software keyboard back button pressed.
}
/*
* Called globally on any touch on the screen to consume the event if it returns true
* */
fun isDismissByTouchOutside() = tag == DismissEvent.ON_TOUCH_OUTSIDE
private fun isDismissByItemClickOrBackPressed() =
tag == DismissEvent.ON_ITEM_CLICK || tag == DismissEvent.ON_BACK_PRESSED
private fun setDismissToItemClick() {
tag = DismissEvent.ON_ITEM_CLICK
}
private fun setDismissToTouchOutside() {
tag = DismissEvent.ON_TOUCH_OUTSIDE
}
private fun setDismissToBackPressed() {
tag = DismissEvent.ON_BACK_PRESSED
}
fun clearDismissEvent() {
tag = null
}
@SuppressLint("DiscouragedPrivateApi")
private fun getPopup(): ListPopupWindow? {
try {
val field = AutoCompleteTextView::class.java.getDeclaredField("mPopup")
field.isAccessible = true
return field.get(this) as ListPopupWindow
} catch (e: NoSuchFieldException) {
e.printStackTrace()
} catch (e: IllegalAccessException) {
e.printStackTrace()
}
return null
}
private fun adjustTouchOutside() {
getPopup()?.let {
it.setOnDismissListener {
if (isDismissByItemClickOrBackPressed()) {// Menu dismissal Event of clicking on the menu item or hitting the software back button
clearDismissEvent() // Neutralize the enum to allow the next touch dispatch event & for adding a chance of next dismissal decision
} else { // Menu dismissal Event of touching outside the menu
// Don't allow the next touch dispatch event
setDismissToTouchOutside()
}
}
}
}
override fun onItemClick(p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long) {
setDismissToItemClick()
consumerListener?.onItemClick(p0, p1, p2, p3)
}
override fun setOnItemClickListener(l: AdapterView.OnItemClickListener?) {
// DO NOT CALL SUPER HERE
// super.setOnItemClickListener(l)
consumerListener = l
}
override fun onKeyPreIme(keyCode: Int, event: KeyEvent?): Boolean {
if (keyCode == KeyEvent.KEYCODE_BACK && isPopupShowing)
setDismissToBackPressed()
return super.onKeyPreIme(keyCode, event)
}
}
The usage
Overriding dispatchTouchEvent()
in the activity is required to consume the event for touching outside by checking isTouchOutsideDisabled()
; and neutralize the dismissal event:
If the AutoCompleteTextView
is in the activity then:
lateinit var autoCTV: TouchOutsideAutoCompleteTextView
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
if (autoCTV.isDismissByTouchOutside()) {
autoCTV.clearDismissEvent()
return true
}
return super.dispatchTouchEvent(ev)
}
If it is in some fragment, then the dispatchTouchEvent()
need to have access to it:
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
val fragment = supportFragmentManager.findFragmentByTag("MyFragmentTag") as MyFragment
if (fragment.autoCTV.isDismissByTouchOutside()) {
fragment.autoCTV.clearDismissEvent()
return true
}
return super.dispatchTouchEvent(ev)
}