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

Kotlin Android中AccessibilityService无法滚动的问题求助

AccessibilityService滚动失效问题修复

我正在使用AccessibilityService实现以下功能:自动返回主屏幕,然后向下滚动直到找到“Maps”文本,找到后点击该文本打开地图应用。我希望在设置中启用该服务后立即执行此操作,因此在onServiceConnected()方法中触发了相关逻辑,但目前滚动功能无法正常工作。

原代码

import android.accessibilityservice.AccessibilityService
import android.os.Handler
import android.os.Looper
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.view.accessibility.AccessibilityNodeInfo.ACTION_SCROLL_FORWARD

class MyAccessibilityService : AccessibilityService() {

override fun onAccessibilityEvent(event: AccessibilityEvent?) {
}

override fun onInterrupt() {
}

override fun onServiceConnected() {
    super.onServiceConnected()
    performActionsForMaps()
}

private fun performActionsForMaps() {
    performGlobalAction(GLOBAL_ACTION_HOME)
    Handler(Looper.getMainLooper()).postDelayed({
        scrollDownUntilTextAppears("Maps")
    }, 2000)
}

private fun scrollDownUntilTextAppears(targetText: String) {
    val maxScrollAttempts = 10
    var scrollAttempts = 0
    while (!isTextVisible(targetText) && scrollAttempts < maxScrollAttempts) {
        val rootNode = rootInActiveWindow
        val scrollableNode = findScrollableNode(rootNode)
        if (scrollableNode != null) {
            Handler(Looper.getMainLooper()).postDelayed({
                scrollableNode.performAction(ACTION_SCROLL_FORWARD)
            }, 1000)
        }
        scrollAttempts++
    }
    if (isTextVisible(targetText)) {
        clickOnText(targetText)
    }
}

private fun findScrollableNode(nodeInfo: AccessibilityNodeInfo?): AccessibilityNodeInfo? {
    if (nodeInfo == null) return null
    for (i in 0 until nodeInfo.childCount) {
        val childNode = nodeInfo.getChild(i)
        if (childNode != null && childNode.isScrollable) {
            return childNode
        }
        val scrollableChild = findScrollableNode(childNode)
        if (scrollableChild != null) {
            return scrollableChild
        }
    }
    return null
}

private fun isTextVisible(targetText: String): Boolean {
    val rootNode = rootInActiveWindow
    return isTextVisibleRecursive(rootNode, targetText)
}

private fun isTextVisibleRecursive(
    nodeInfo: AccessibilityNodeInfo?,
    targetText: String
): Boolean {
    if (nodeInfo == null) return false
    for (i in 0 until nodeInfo.childCount) {
        val childNode = nodeInfo.getChild(i)
        if (childNode != null) {
            if (childNode.text != null && targetText.equals(
                    childNode.text.toString(),
                    ignoreCase = true
                )
            ) {
                return true
            }
            if (isTextVisibleRecursive(childNode, targetText)) {
                return true
            }
        }
    }
    return false
}

private fun clickOnText(targetText: String) {
    val rootNode = rootInActiveWindow
    clickOnTextRecursive(rootNode, targetText)
}

private fun clickOnTextRecursive(nodeInfo: AccessibilityNodeInfo?, targetText: String) {
    if (nodeInfo == null) return
    for (i in 0 until nodeInfo.childCount) {
        val childNode = nodeInfo.getChild(i)
        if (childNode != null) {
            if (childNode.text != null && targetText.equals(
                    childNode.text.toString(),
                    ignoreCase = true
                )
            ) {
                childNode.performAction(AccessibilityNodeInfo.ACTION_CLICK)
                return
            }
            clickOnTextRecursive(childNode, targetText)
        }
    }
}

companion object {
    private const val TAG = "MyAccessibilityService"
}
}

问题原因

  1. 同步循环与异步延迟冲突:scrollDownUntilTextAppears方法中使用while同步循环,同时通过Handler.postDelayed异步执行滚动动作,导致循环瞬间跑完所有滚动尝试,完全没有等待滚动后的界面更新就判断文本是否存在。
  2. AccessibilityNodeInfo未回收:所有获取的节点对象未调用recycle(),易引发内存泄漏。
  3. 未处理rootNode为空场景:rootInActiveWindow可能在界面未加载完成时返回null,导致后续逻辑失效。

修复后的代码

import android.accessibilityservice.AccessibilityService
import android.os.Handler
import android.os.Looper
import android.view.accessibility.AccessibilityNodeInfo
import android.view.accessibility.AccessibilityNodeInfo.ACTION_SCROLL_FORWARD

class MyAccessibilityService : AccessibilityService() {
    private val mainHandler = Handler(Looper.getMainLooper())
    private var scrollAttempts = 0
    private val maxScrollAttempts = 10
    private lateinit var targetText: String

    override fun onAccessibilityEvent(event: android.view.accessibility.AccessibilityEvent?) {}

    override fun onInterrupt() {}

    override fun onServiceConnected() {
        super.onServiceConnected()
        performActionsForMaps()
    }

    private fun performActionsForMaps() {
        performGlobalAction(GLOBAL_ACTION_HOME)
        targetText = "Maps"
        // 延迟确保主屏幕加载完成
        mainHandler.postDelayed({ checkAndScroll() }, 2000)
    }

    private fun checkAndScroll() {
        val rootNode = rootInActiveWindow ?: run {
            // root为空时延迟重试
            mainHandler.postDelayed(::checkAndScroll, 500)
            return
        }

        if (isTextVisible(rootNode, targetText)) {
            clickOnText(rootNode, targetText)
            rootNode.recycle()
            return
        }

        if (scrollAttempts >= maxScrollAttempts) {
            rootNode.recycle()
            return
        }

        val scrollableNode = findScrollableNode(rootNode)
        scrollableNode?.let {
            it.performAction(ACTION_SCROLL_FORWARD)
            it.recycle()
        }
        rootNode.recycle()

        scrollAttempts++
        // 延迟等待滚动完成后再检查
        mainHandler.postDelayed(::checkAndScroll, 1000)
    }

    private fun findScrollableNode(nodeInfo: AccessibilityNodeInfo?): AccessibilityNodeInfo? {
        if (nodeInfo == null) return null
        for (i in 0 until nodeInfo.childCount) {
            val childNode = nodeInfo.getChild(i)
            childNode?.let {
                if (it.isScrollable) return it
                val scrollableChild = findScrollableNode(it)
                if (scrollableChild != null) {
                    it.recycle()
                    return scrollableChild
                }
                it.recycle()
            }
        }
        return null
    }

    private fun isTextVisible(rootNode: AccessibilityNodeInfo, targetText: String): Boolean {
        return isTextVisibleRecursive(rootNode, targetText)
    }

    private fun isTextVisibleRecursive(
        nodeInfo: AccessibilityNodeInfo?,
        targetText: String
    ): Boolean {
        if (nodeInfo == null) return false
        for (i in 0 until nodeInfo.childCount) {
            val childNode = nodeInfo.getChild(i)
            childNode?.let {
                if (it.text != null && targetText.equals(it.text.toString(), ignoreCase = true)) {
                    it.recycle()
                    return true
                }
                if (isTextVisibleRecursive(it, targetText)) {
                    it.recycle()
                    return true
                }
                it.recycle()
            }
        }
        return false
    }

    private fun clickOnText(rootNode: AccessibilityNodeInfo, targetText: String) {
        clickOnTextRecursive(rootNode, targetText)
    }

    private fun clickOnTextRecursive(nodeInfo: AccessibilityNodeInfo?, targetText: String) {
        if (nodeInfo == null) return
        for (i in 0 until nodeInfo.childCount) {
            val childNode = nodeInfo.getChild(i)
            childNode?.let {
                if (it.text != null && targetText.equals(it.text.toString(), ignoreCase = true)) {
                    it.performAction(AccessibilityNodeInfo.ACTION_CLICK)
                    it.recycle()
                    return
                }
                clickOnTextRecursive(it, targetText)
                it.recycle()
            }
        }
    }

    companion object {
        private const val TAG = "MyAccessibilityService"
    }
}

关键修复点

  1. 异步递归替代同步循环:用checkAndScroll方法配合Handler.postDelayed实现滚动-检查的异步循环,确保每次滚动后等待界面更新再判断文本状态。
  2. 节点资源回收:所有获取的AccessibilityNodeInfo对象都调用recycle(),避免内存泄漏。
  3. 空值处理:当rootInActiveWindow返回null时,延迟重试,确保界面加载完成后执行逻辑。
  4. 复用Handler实例:统一使用一个mainHandler,避免重复创建资源。

内容的提问来源于stack exchange,提问作者user9541096

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 09:07:03