Accessibility Service导致Compose TextField中DPAD导航失效,XML正常
Accessibility Service导致Compose DPAD上下键焦点导航失效的解决方案
问题背景
设置flagRequestFilterKeyEvents后,硬件按键事件会先经过Accessibility Service处理,再重新注入输入管道。XML布局中ViewRootImpl会对重新注入的事件执行焦点搜索,但Compose的BasicTextField会直接消费DPAD上下键用于光标移动,不会将事件传递给框架焦点搜索流程,导致文本字段获焦时无法用DPAD上下键移动焦点。手动遍历视图树处理焦点无法适配多行文本场景。
核心解决方案
针对DPAD上下键,区分当前聚焦的文本字段是单行还是多行:
- 多行文本:放行事件,允许光标正常移动;
- 单行文本:手动调用Accessibility焦点移动API,绕过重新注入事件后的消费问题。
修改后的服务代码
override fun onKeyEvent(event: KeyEvent): Boolean { val code = event.keyCode Log.v( TAG, "onKeyEvent code=$code action=${event.action} repeat=${event.repeatCount}" + " flags=0x${ Integer.toHexString(event.flags) }" + " canceled=${(event.flags and KeyEvent.FLAG_CANCELED) != 0}" + " longPress=${(event.flags and KeyEvent.FLAG_LONG_PRESS) != 0}" ) // 处理BACK键逻辑 if (code == KeyEvent.KEYCODE_BACK) { val active = recording || transcribeJob?.isActive == true if (!active) return false Log.i(TAG, "BACK received in onKeyEvent action=${event.action} — consuming") if (event.action == KeyEvent.ACTION_UP && (event.flags and KeyEvent.FLAG_CANCELED) == 0) { cancelActiveOperation() } return true } // 新增DPAD上下键处理逻辑 if (code == KeyEvent.KEYCODE_DPAD_UP || code == KeyEvent.KEYCODE_DPAD_DOWN) { if (event.action != KeyEvent.ACTION_DOWN) return false val focusedNode = findFocusedEditable() ?: return false // 判断文本字段是否支持垂直滚动(多行) val canScrollVertically = focusedNode.canScrollVertically(1) || focusedNode.canScrollVertically(-1) if (canScrollVertically) { // 多行文本,放行事件让光标移动 return false } else { // 单行文本,手动触发焦点移动 val action = if (code == KeyEvent.KEYCODE_DPAD_DOWN) { AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY } else { AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY } val success = focusedNode.performAction(action) if (success) { Log.d(TAG, "手动移动焦点成功,方向:${if (code == KeyEvent.KEYCODE_DPAD_DOWN) "下" else "上"}") return true } // 移动失败则放行事件 return false } } // 原有DPAD_CENTER和ENTER键处理逻辑 if (code != KeyEvent.KEYCODE_DPAD_CENTER && code != KeyEvent.KEYCODE_ENTER) { return false } if (event.action == KeyEvent.ACTION_DOWN) { main.removeCallbacks(debouncedStop) if (recording || longPressTriggered) { dpadDown = true return true } if (!dpadDown) { val node = findFocusedEditable() if (node == null) { Log.d(TAG, "DOWN: no focused editable, pass-through") return false } Log.d( TAG, "DOWN on editable, arming long-press timer (${ViewConfiguration.getLongPressTimeout()}ms) — consuming" ) dpadDown = true longPressTriggered = false main.postDelayed( longPressRunnable, ViewConfiguration.getLongPressTimeout().toLong() ) return true } return false } if (event.action == KeyEvent.ACTION_UP) { val canceled = (event.flags and KeyEvent.FLAG_CANCELED) != 0 if (canceled) { if (longPressTriggered) { Log.d(TAG, "UP canceled during long-press flow — consuming") return true } Log.d(TAG, "UP canceled without long-press — pass-through") dpadDown = false main.removeCallbacks(longPressRunnable) return false } if (!dpadDown) return false dpadDown = false main.removeCallbacks(longPressRunnable) if (longPressTriggered) { Log.d( TAG, "UP after long-press: scheduling debounced stop (${STOP_DEBOUNCE_MS}ms)" ) main.postDelayed(debouncedStop, STOP_DEBOUNCE_MS) return true } Log.d(TAG, "UP short-press on editable: pass-through") return false } return false }
关键说明
- 多行文本判断:通过
canScrollVertically方法判断文本字段是否支持垂直滚动,以此区分单行/多行场景; - 手动焦点移动:使用AccessibilityNodeInfo的
performAction方法触发焦点导航,避免事件重新注入后被Compose文本字段消耗; - 原有逻辑保留:不影响服务对BACK、DPAD_CENTER/ENTER键的原有处理逻辑。
内容的提问来源于stack exchange,提问作者Muhammad Awais Shan
相关产品推荐
相关产品推荐

