From 18bddf9f3c3abc192c8c9fdfeeef90f2558bbb02 Mon Sep 17 00:00:00 2001 From: Sergey Bobrenok Date: Thu, 26 Feb 2026 23:34:28 +0300 Subject: [PATCH 1/2] Add custom key and focus event listeners It fixes button focus inside ListView. By default, ListView doesn't allow focusing views inside its items, and navigation between views inside ListView's items is not supported. --- .../mountpointlist/MountpointFragment.kt | 101 +++++++++++++++++- .../mountpoint/MountPointsArrayAdapter.kt | 6 +- 2 files changed, 105 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/ru/nsu/bobrofon/easysshfs/mountpointlist/MountpointFragment.kt b/app/src/main/java/ru/nsu/bobrofon/easysshfs/mountpointlist/MountpointFragment.kt index 496ac75..69c7820 100644 --- a/app/src/main/java/ru/nsu/bobrofon/easysshfs/mountpointlist/MountpointFragment.kt +++ b/app/src/main/java/ru/nsu/bobrofon/easysshfs/mountpointlist/MountpointFragment.kt @@ -2,6 +2,7 @@ package ru.nsu.bobrofon.easysshfs.mountpointlist import android.os.Bundle +import android.view.KeyEvent import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater @@ -10,6 +11,7 @@ import android.view.View import android.view.ViewGroup import android.widget.AbsListView import android.widget.AdapterView +import android.widget.Button import android.widget.ListAdapter import androidx.core.view.MenuHost import androidx.core.view.MenuProvider @@ -28,6 +30,96 @@ class MountpointFragment : EasySSHFSFragment(), AdapterView.OnItemClickListener, private lateinit var listAdapter: ListAdapter private lateinit var mountPointsList: MountPointsList + /** + * buttonOnKeyListener is used when a mount button is focused, to implement custom D-pad + * navigation between the button and an enclosing ListView's items. + * + * The listener expects the ListView item to look like this: + * |----------------------| + * | +--------+| + * | Item view | Button || + * | +--------+| + * |----------------------| + */ + private val buttonOnKeyListener = View.OnKeyListener { _, keycode, event -> + if (event.action != KeyEvent.ACTION_DOWN) { + return@OnKeyListener false + } + + val dpadNavigationKeys = arrayOf( + KeyEvent.KEYCODE_DPAD_RIGHT, + KeyEvent.KEYCODE_DPAD_LEFT, + KeyEvent.KEYCODE_DPAD_UP, + KeyEvent.KEYCODE_DPAD_DOWN + ) + if (!dpadNavigationKeys.contains(keycode)) { + return@OnKeyListener false + } + + // We are leaving the button, so we need to pass the focus back. + listView.descendantFocusability = ViewGroup.FOCUS_BLOCK_DESCENDANTS + listView.requestFocus() + + // The button is on the right side of the ListView item. + // Moving left means "go back to the item containing this button". + if (keycode == KeyEvent.KEYCODE_DPAD_LEFT) { + // Focus is already passed to the original ListView item. + true + } else { + // Allow ListView to select the next item. Otherwise, the currently selected item will + // remain selected after moving out of the button. + listView.onKeyDown(keycode, event) + } + } + + /** + * buttonOnFocusChangeListener implements custom focus logic for buttons inside a ListView. + */ + private val buttonOnFocusChangeListener = View.OnFocusChangeListener { buttonView, hasFocus -> + if (hasFocus) { + return@OnFocusChangeListener + } + + // We should keep ButtonView unfocusable to allow ListView/GridView to handle onItemClick + // properly. + buttonView.isFocusable = false + + // Restore the original state in case the button lost the focus not via buttonOnKeyListener. + listView.descendantFocusability = ViewGroup.FOCUS_BLOCK_DESCENDANTS + } + + /** + * listViewOnKeyListener is used when a mount button is not focused, to implement custom D-pad + * navigation between the selected ListView item and the button inside it. + * + * The listener expects the ListView item to look like this: + * |----------------------| + * | +--------+| + * | Item view | Button || + * | +--------+| + * |----------------------| + */ + private val listViewOnKeyListener = View.OnKeyListener { _, keycode, event -> + val selectedItemView = listView.selectedView ?: return@OnKeyListener false + + if (event.action != KeyEvent.ACTION_DOWN) { + return@OnKeyListener false + } + if (keycode != KeyEvent.KEYCODE_DPAD_RIGHT) { + return@OnKeyListener false + } + // The button is on the right side of the item view. + // Moving right means "moving to the button". + + listView.descendantFocusability = ViewGroup.FOCUS_AFTER_DESCENDANTS + + val button = selectedItemView.findViewById