Android多屏场景下如何访问副屏AccessibilityNode并实现点击
问题描述
我正在开发一款支持双屏的Android AccessibilityService,需求是:
- 主屏设置可拖动触摸区域(类似触控栏),用户拖动该区域时,副屏上的光标随之移动
- 副屏运行独立应用,不使用Presentation模式
目前已实现的功能:
通过拥有android.permission.SYSTEM_ALERT_WINDOW权限(已通过Settings.canDrawOverlays()验证)的Service,完成了主屏触控区域绘制、副屏光标绘制,拖动触控栏时光标同步移动,且能实时获取副屏光标的坐标。
当前待解决问题:
需要实现点击交互——用户点击主屏触控栏时,触发副屏对应位置的点击。计划借助AccessibilityService实现,但无法访问副屏的AccessibilityNode:
rootInActiveWindow仅返回当前活跃屏幕(始终为主屏)的根节点windowsOnAllDisplays始终返回空列表,无法获取副屏窗口
相关代码如下:
private fun click(action: Int = AccessibilityNodeInfo.ACTION_CLICK) { val nodeInfo = if(displayManager.displays.size > 1){ windowsOnAllDisplays.get(displayManager.displays[1].displayId, emptyList()).firstOrNull()?.root ?: rootInActiveWindow } else { rootInActiveWindow } ?: return val nearestNodeToMouse = findSmallestNodeAtPoint(nodeInfo, cursorLayout.x, cursorLayout.y) if (nearestNodeToMouse != null) { logNodeHierachy(nearestNodeToMouse, 0) nearestNodeToMouse.performAction(action) } nodeInfo.recycle() } private fun findSmallestNodeAtPoint(sourceNode: AccessibilityNodeInfo, x: Int, y: Int): AccessibilityNodeInfo? { val bounds = Rect() sourceNode.getBoundsInScreen(bounds) if (!bounds.contains(x, y)) { return null } for (i in 0 until sourceNode.childCount) { val child = sourceNode.getChild(i) if(child != null) { val nearestSmaller = findSmallestNodeAtPoint(child, x, y) if (nearestSmaller != null) { return nearestSmaller } } } return sourceNode }
可行解决方案
方案1:利用AccessibilityService的getWindows()结合坐标转换
Android的AccessibilityNodeInfo坐标基于全局屏幕坐标系,可以尝试:
- 调用
getWindows()获取所有窗口,遍历窗口通过AccessibilityWindowInfo.getDisplayId()匹配副屏的displayId - 获取副屏的显示区域(
Display.getBounds()),将副屏内的光标坐标转换为全局坐标系坐标:
全局X = 副屏左上角X + 光标在副屏的X坐标
全局Y = 副屏左上角Y + 光标在副屏的Y坐标 - 用转换后的全局坐标调用
findSmallestNodeAtPoint查找副屏窗口根节点下的对应控件
注意:需确保AccessibilityService配置中声明了TYPE_WINDOW_STATE_CHANGED等必要事件类型,且该方式在Android 10+设备上兼容性更好。
方案2:模拟系统触摸事件(需ROOT/系统签名)
如果无法通过AccessibilityNode实现,可直接向副屏注入触摸事件:
val inputManager = getSystemService(INPUT_SERVICE) as InputManager val displayId = displayManager.displays[1].displayId // 构造按下事件 val downEvent = MotionEvent.obtain( SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, cursorXInDisplay, // 光标在副屏内的X坐标 cursorYInDisplay, // 光标在副屏内的Y坐标 0 ) downEvent.displayId = displayId inputManager.injectInputEvent(downEvent, InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH) // 构造抬起事件 val upEvent = MotionEvent.obtain( SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, cursorXInDisplay, cursorYInDisplay, 0 ) upEvent.displayId = displayId inputManager.injectInputEvent(upEvent, InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH) downEvent.recycle() upEvent.recycle()
限制:普通应用无法直接调用injectInputEvent,需要ROOT权限或应用拥有系统签名,部分厂商可能限制该接口使用。
方案3:副屏应用主动配合(跨进程通信)
如果副屏运行的是自研应用,可通过IPC实现点击:
- 主屏AccessibilityService检测到点击后,通过
Messenger、AIDL或ContentProvider将光标坐标发送给副屏应用 - 副屏应用接收坐标后,通过
View.hitTest()找到对应控件,直接调用performClick()触发交互
该方案无需特殊权限,稳定性最高,但前提是副屏应用处于可控状态。
内容的提问来源于stack exchange,提问作者Tomislav
相关产品推荐
相关产品推荐

