You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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 
}

关键说明

  1. 多行文本判断:通过canScrollVertically方法判断文本字段是否支持垂直滚动,以此区分单行/多行场景;
  2. 手动焦点移动:使用AccessibilityNodeInfo的performAction方法触发焦点导航,避免事件重新注入后被Compose文本字段消耗;
  3. 原有逻辑保留:不影响服务对BACK、DPAD_CENTER/ENTER键的原有处理逻辑。

内容的提问来源于stack exchange,提问作者Muhammad Awais Shan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.02 01:27:27