AutoCompleteTextView: detecting when dropdown is dismissed and item is NOT selected - android

I need to know when the user taps outside of the AutoCompleteTextView dropdown in order to dismiss it (i.e. they dismiss the popup with selecting an item in the list). I've setup the setOnDismissListener() as shown here:
mAutoView.setOnDismissListener(new AutoCompleteTextView.OnDismissListener() {
#Override
public void onDismiss() {
CharSequence msg = "isPerformingCompletion = " + mAutoView.isPerformingCompletion() +
" Item selected at = " + mAutoView.getListSelection();
Toast.makeText(getContext(), msg, Toast.LENGTH_LONG).show();
}
});
And an OnItemClickListener like this:
private AdapterView.OnItemClickListener mAutocompleteClickListener
= new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
// get selected item and pass it to result callback
}
};
The onDismiss event fires before the onItemClick event, and unfortunately, neither the "isPerformingCompletion()" nor the "getListSelection()" methods return a value until the onItemClick event fires.
Can anyone suggest an approach to detecting a dismiss without a list selection?

Below piece of code will detect that if user dismiss the dropdown of autocompletetextview, Then onDismiss it will check the selected input using Geocode API, If it is having a result then selected input is valid otherwise input is not valid, So In that case, Entered text will be vanish in bit seconds.
mAutoView.setOnDismissListener {
try {
val fromLocationName = Geocoder(context).getFromLocationName(mAutoView.getText().toString(), 1)
if (fromLocationName != null && fromLocationName.isNotEmpty()) {
Log.d(TAG, "Address valid")
} else {
mAutoView.setText("")
Log.d(TAG, "Address not valid")
}
} catch (e: Exception) {
mAutoView.setText("")
Log.d(TAG, "Address not valid with Exception")
}
}
Means you need a kind of validator in OnDismiss, That will check the input text is valid or not, Based on validation you can indicate to user that entered input is valid or not.

AutoCompleteTextView: detecting when dropdown is dismissed and item is NOT selected
If I understood you well, you need to catch the event of dismissing the draopDown by touching outside of it instead of choosing an item.
For some reason, the setOnDismissListener doesn't work for me. I couldn't find a clue without touching the inner ListPopupWindow which get called on either event (item click or touch outside).
The event is differentiated by registering an inner OnItemClickListener listener to set the tag of the view to some value that indicates that BY_ITEM_CLICK ; if the tag is anything else; it will be considered a touch outside event.
Here is a custom AutoCompleteTextView that can be used for that:
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.KeyEvent
import android.view.View
import android.widget.AdapterView
import android.widget.AutoCompleteTextView
import android.widget.ListPopupWindow
import androidx.appcompat.widget.AppCompatAutoCompleteTextView
class OnDismissAutoCompleteTextView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : AppCompatAutoCompleteTextView(context, attrs), AdapterView.OnItemClickListener {
interface OnMenuDismissListener {
fun onTouchOutside()
fun onItemClick()
fun onBackPressed()
}
companion object {
// set the tag to this value when an item is clicked to dismiss the menu
const val BY_ITEM_CLICK = "BY_ITEM_CLICK"
// set the tag to this value when the back is pressed to dismiss the menu
const val BY_BACK_PRESSED = "BY_BACK_PRESSED"
}
init {
super.setOnItemClickListener(this)
}
var onMenuDismissListener: OnMenuDismissListener? = null
private var consumerListener: AdapterView.OnItemClickListener? = 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 setupDismissListener() {
getPopup()?.let {
it.setOnDismissListener {
when (tag) {
BY_ITEM_CLICK -> onMenuDismissListener?.onItemClick() // Menu dismissal Event of clicking on the menu item
BY_BACK_PRESSED -> onMenuDismissListener?.onBackPressed()
else -> onMenuDismissListener?.onTouchOutside() // Menu dismissal Event of touching outside the menu
}
// reset the tag for the next dismissal
tag = null
}
}
}
override fun onPreDraw(): Boolean {
// Registering the mPopup window OnDismissListener
setupDismissListener()
return super.onPreDraw()
}
override fun onItemClick(p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long) {
tag = BY_ITEM_CLICK
if (consumerListener != null) {
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)
tag = BY_BACK_PRESSED
return super.onKeyPreIme(keyCode, event)
}
}
Usage:
myAutoCompleteTV.onMenuDismissListener = object :
OnDismissAutoCompleteTextView.OnMenuDismissListener {
override fun onTouchOutside() {
// Menu dismiss is due to touch outside event
Toast.makeText(context, "touch outside", Toast.LENGTH_SHORT).show()
}
override fun onItemClick() {
// Menu dismiss is due to clicking on an item
Toast.makeText(context, "item clicked", Toast.LENGTH_SHORT).show()
}
override fun onBackPressed() {
Toast.makeText(context, "back pressed", Toast.LENGTH_SHORT).show()
}
}

Related

Android Spinner - Differentiate manually and programmatically selection

I am looking for a way to differentiate selection type in OnItemSelectedListener of Spinner between manual selection by tapping and by setSelection(pos)
i tried by this, but no success
binding.spinner.onItemSelectedListener = object : OnItemSelectedListener {
override fun onItemSelected(
adapterView: AdapterView<*>,
view: View?,
position: Int,
id: Long
) {
if (adapterView.isPressed) {
Toast.makeText(this#SplashActivity, "Manually", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this#SplashActivity, "Programmatically", Toast.LENGTH_SHORT)
.show()
}
}
For reference, similar functionality is already provided by Checkbox, and it is working perfectly for checkboxes
binding.checkbox.setOnCheckedChangeListener { compoundButton, bool ->
if (compoundButton.isPressed) {
//manually
Toast.makeText(this, "Manually", Toast.LENGTH_SHORT).show()
} else {
//Programmatically
Toast.makeText(this, "Programmatically", Toast.LENGTH_SHORT).show()
}
}

Material Spinner (AutoCompleteTextView) - catch outside clicks

When using a spinner, it's the default behaviour that clicks outside of the spinner close the spinner but are NOT handled by the view underneath the click.
When switching to TextInputLayout + AutoCompleteTextView this behaviour is different, clicking on something outside the "spinner" closes the spinner AND the view underneath the touch does get the click event as well - this is very annoying and imho unexpected.
Can I somehow disable this behaviour to get the same behaviour as I get when using an old spinner?
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)
}

How can we detect which keyboard is open, Numeric or Alpha Numeric in android Webview?

Hi i am new here i need to know how can i detect which keyboard type is open in android webview here is my try
KeyboardUtils.addKeyboardToggleListener(this, new KeyboardUtils.SoftKeyboardToggleListener()
{
#Override
public void onToggleSoftKeyboard(boolean isVisible)
{
if(isVisible){
InputMethodManager imm =(InputMethodManager)MainActivity.this.getSystemService(Context.INPUT_METHOD_SERVICE);
Log.d(TAG, "onToggleSoftKeyboard: "+imm.getCurrentInputMethodSubtype().getMode());
//
}
Log.d("keyboard", "keyboard visible: "+isVisible);
}
});
You can use below code to check the type
private fun getKeyboardType(){
val devicesIds = InputDevice.getDeviceIds()
for (deviceId in devicesIds) {
val device = InputDevice.getDevice(deviceId)
device.keyboardType //returns an integer
// Now compare this integer with predefined constant values : https://developer.android.com/reference/android/text/InputType
}
}
Compare the integer values with constants here
You can check when keyboard opens/closes using a listener.
val listener = object : ViewTreeObserver.OnGlobalLayoutListener {
// Keep a reference to the last state of the keyboard
private var lastState: Boolean = activity.isKeyboardOpen()
override fun onGlobalLayout() {
val isOpen = activity.isKeyboardOpen()
if (isOpen == lastState) {
return
} else {
dispatchKeyboardEvent(isOpen)
lastState = isOpen
}
}
}
And add that listener in activity lifecycle callback
override fun onResume() {
super.onResume()
val view = getRootView()
view.viewTreeObserver.addOnGlobalLayoutListener(listener)
}
And remember to remove listener when activity is no longer active
override fun onPause() {
super.onPause()
val view = getRootView()
view.viewTreeObserver.removeOnGlobalLayoutListener(listener)
}

ActionMode callback never calls onCreateActionMode when started in fragment's OnViewCreated

#ExperimentalCoroutinesApi
class WeekdayTimesFragment #Inject constructor(viewModelFactory: SavedStateViewModelFactory.Factory)
:Fragment(R.layout.fragment_weekday_times), WeekdayAlarmTimesAdapter.OnWeekdayAlarmTimePressed, ActionModeListener {
private lateinit var weekdayAlarmTimesAdapter: WeekdayAlarmTimesAdapter
private val weekdayTimesViewModel
by viewModels<WeekdayTimesViewModel> { viewModelFactory.create(this) }
override val actionCallback = PrimaryActionCallback(this)
private var weekday: Int = 0
private val isActionModeActive: Boolean
get() = weekdayTimesViewModel.isActionModeActive
private val numSelected: Int
get() = weekdayTimesViewModel.selectedTimePositions.size
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
weekday = requireArguments().get("WEEKDAY") as Int
if(isActionModeActive) { //action mode may be active prior to rotation
startActionMode() //for some reason, starting action mode here does not work....
}
initRecyclerView() //will determine what recyclerview items are selected via the viewmodel
weekdayTimesViewModel.getTimesAndAlarmsForSelectedWeekday(Weekday[weekday])
.observe(viewLifecycleOwner) { times->
weekdayAlarmTimesAdapter.submitWeekdayTimes(times)
}
}
override fun alarmTimePressed(wasSelected: Boolean, position: Int, time: WeekdayTime) {
if(wasSelected) { //this item was selected prior to being selected, effectively unselecting it
weekdayTimesViewModel.selectedTimePositions.remove(position)
} else {
weekdayTimesViewModel.selectedTimePositions[position] =
Time(time.hour, time.minute, Weekday[weekday], time.alarm.alarmId, time.isDisabled, time.id)
}
if(numSelected == 0 && isActionModeActive) { //no more times are selected, turn off action mode
destroyActionMode() //need to de-select recyclerview items
} else {
if (!isActionModeActive) {
startActionMode()
} else {
actionCallback.changeTitle("$numSelected selected")
}
}
}
private fun startActionMode() {
actionCallback
.startActionMode(
requireView(),
R.menu.toolbar_contextual_menu,
"$numSelected selected")
//fragment hosting the viewpager
(requireParentFragment() as ActionModeListenerController).currentActionModeListener = this
weekdayTimesViewModel.isActionModeActive = true
}
override fun onActionItemClick(item: MenuItem?) {
item?.let {
when(it.itemId){
R.id.toolbar_disable_alarm-> {
requireContext().createGenericAlertDialog(
message = if(numSelected == 1) "Are you sure you want to disable this alarm?"
else "Are you sure you want to disable these alarms?",
positiveListener = { _, _ ->
weekdayTimesViewModel.disableSelectedTimes()
showUndoSnackbar(true)
}
).show()
}
R.id.toolbar_delete_alarms-> {
requireContext().createGenericAlertDialog(
message = if(numSelected == 1) "Are you sure you want to delete this alarm?"
else "Are you sure you want to delete these alarms?",
positiveListener = { _, _ ->
weekdayTimesViewModel.deleteSelectedTimes()
showUndoSnackbar(false)
}
).show()
}
}
}
}
//when we aren't destroying the view but need to end action mode (changing tab or after we confirm an action)
//we may want to do specific things specific to certain listeners, e.g. de-select certain recyclerview items.
override fun destroyActionMode() {
actionCallback.finishActionMode()
weekdayTimesViewModel.isActionModeActive = false
//also un-select all of the elements that have been selected
weekdayTimesViewModel.selectedTimePositions.clear()
//redraw recyclerview as well
rv_weekday_times.resetAdapterState()
}
private fun showUndoSnackbar(disablingTimes: Boolean){
val message =
if(disablingTimes) "Your alarm times have been disabled." else "Your alarm times have been deleted."
Snackbar.make(requireView(), message, Snackbar.LENGTH_LONG).setAction("UNDO"){
if(disablingTimes)
weekdayTimesViewModel.restoreDisabledTimes()
else
weekdayTimesViewModel.restoreDeletedTimes()
}.setActionTextColor(ContextCompat.getColor(requireContext(), R.color.colorPrimary))
.setAnchorView(requireActivity().bottom_nav_view).show()
}
//when rotating screen or navigating we can just end action mode. The state of whether we are
//in action mode either doesn't matter (navigation) or is already persisted, along with the selected
//items in the viewmodel(rotation)
override fun onDestroyView() {
actionCallback.finishActionMode()
super.onDestroyView()
}
private fun initRecyclerView() {
weekdayAlarmTimesAdapter = WeekdayAlarmTimesAdapter(this, weekdayTimesViewModel.selectedTimePositions.keys)
rv_weekday_times.apply {
adapter = weekdayAlarmTimesAdapter
layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
}
weekdayAlarmTimesAdapter.selectedItems.clear()
//clears selected items as these items no longer remain consistently for the lifecycle of the view
}
}
So my ActionMode works great when I get a callback (alarmTimePressed) which is through a click listener set in the recyclerview's OnBindViewHolder. I start my ActionMode and it works great, I save whether ActionMode is active in my viewmodel, which works well upon rotation, as if it is active prior to rotation, it still remains active. As you can see in my OnViewCreated, I call startActionMode() when ActionMode is meant to be active. The problem is that when I start it from there, the onCreateActionMode callback is never called. This is strange because even after rotating, if I select an item in the RecyclerView it correctly launches ActionMode. The view I am passing to startActionMode is the same in both cases. If you need the code for PrimaryActionCallback, it's here: https://hastebin.com/vepujuhefe.m. Didn't think it was very necessary but in case you felt like it was necessary to see :). Anyone know what's going on here?

Spinner - show hint when adapter is empty

I know there have been several questions that dealt with the problem how to add the "Select one..." hint for the Spinner before the first selection is made. But that's not my case.
What I need is to display the hint only when the SpinnerAdapter is empty. By default in such case, nothing happens on click (but that is not the major problem), and most of all, the spinner doesn't display any text, so it looks like this, which obviously doesn't feel right:
Any idea how to simply handle this problem? I've come up with 2 possible solutions, but I don't like any of them very much:
If the SpinnerAdapter is empty, hide the Spinner from the layout and display a TextView with the same background as the Spinner instead.
Implement a custom SpinnerAdapter whose getCount() returns 1 instead of 0 if the internal list is empty, and at the same time, have its getView() return a TextView with the required "Empty" message, possibly grey-coloured. But that would require specific checking if the selected item is not the "Empty" one.
You can use this SpinnerWithHintAdapter class below
class SpinnerWithHintAdapter(context: Context, resource: Int = android.R.layout.simple_spinner_dropdown_item) :
ArrayAdapter<Any>(context, resource) {
override fun isEnabled(position: Int): Boolean {
return position != 0
}
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
return (super.getDropDownView(position, convertView, parent) as TextView).apply {
if (position == 0) {
// Set the hint text color gray
setTextColor(Color.GRAY)
} else {
setTextColor(Color.BLACK)
}
}
}
fun attachTo(spinner: Spinner, itemSelectedCallback: ((Any?) -> Unit)? = null) {
spinner.apply {
adapter = this#SpinnerWithHintAdapter
itemSelectedCallback?.let {
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onNothingSelected(parent: AdapterView<*>?) {}
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
val selectedItem = parent?.getItemAtPosition(position)
// If user change the default selection
// First item is disable and it is used for hint
if (position > 0) {
it(selectedItem)
}
}
}
}
}
}
}
How to use? Let's assume I have data class called City
data class City(
val id: Int,
val cityName: String,
val provinceId: Int
) {
/**
* By overriding toString function, you will show the dropdown text correctly
*/
override fun toString(): String {
return cityName
}
}
In the activity, initiate the adapter, add hint(first item), add main items, and finally attach it to your spinner.
SpinnerWithHintAdapter(this#MyActivity)
.apply {
// add hint
add("City")
// add your main items
for (city in cityList) add(city)
// attach this adapter to your spinner
attachTo(yourSpinner) { selectedItem -> // optional item selected listener
selectedItem?.apply {
if (selectedItem is City) {
// do what you want with the selected item
}
}
}
}

Categories

Resources