Skip to content
返回

Android 无障碍开发基础配置

Updated:  at  13:00

安卓无障碍开发指南

一、基础概念

AccessibilityService是Android提供的无障碍服务框架,主要功能包括:

  1. 监控界面变化
  2. 获取界面节点信息
  3. 模拟用户操作(点击、滑动、输入等)
  4. 跨应用交互

二、基础配置

1. AndroidManifest.xml配置要点

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <!--    查询应用-->
    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
        tools:ignore="QueryAllPackagesPermission" />

    <application>

        <service
            android:name=".MyAccessibilityService"
            android:exported="true"
            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
            <intent-filter>
                <action android:name="android.accessibilityservice.AccessibilityService" />
            </intent-filter>

            <meta-data
                android:name="android.accessibilityservice"
                android:resource="@xml/accessibility_service_config" />
        </service>

    </application>

</manifest>

2. 无障碍服务配置文件(accessibility_config.xml)

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/accessibility_service_description"
    android:packageNames="com.example.qingwei"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFeedbackType="feedbackAllMask"
    android:accessibilityFlags="flagIncludeNotImportantViews|flagReportViewIds|flagRequestEnhancedWebAccessibility|flagRetrieveInteractiveWindows"
    android:canRequestEnhancedWebAccessibility="true"
    android:notificationTimeout="100"
    android:canPerformGestures="true"
    android:canRetrieveWindowContent="true"
    android:canRequestTouchExplorationMode="true"
    android:settingsActivity="com.example.qingwei.SettingsActivity" />

3. 基础服务类实现

class MyAccessibilityService : AccessibilityService() {

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

    override fun onAccessibilityEvent(event: AccessibilityEvent) {
        when (event.eventType) {
             AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED,
             AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED -> {  
                 val className = event.className?.toString()
             }
        }
    }
}

核心功能

1. 基本操作

// 节点点击
fun clickNode(node: AccessibilityNodeInfo) {
    node.performAction(ACTION_CLICK)
}

// 节点点击(通过节点位置)
fun clickByNodeCenter(node: AccessibilityNodeInfo, service: AccessibilityService) {
        val rect = Rect()
        node.getBoundsInScreen(rect)

        // 校验 bounds 合法性
        if (rect.left < 0 || rect.top < 0 || rect.right <= rect.left || rect.bottom <= rect.top) {
            Log.w("MyAccessibilityService", "点击跳过:坐标非法 $rect")
            return
        }

        val path = Path().apply {
            moveTo(rect.centerX().toFloat(), rect.centerY().toFloat())
        }

        val gesture = GestureDescription.Builder()
            .addStroke(GestureDescription.StrokeDescription(path, 0, 100))
            .build()

        service.dispatchGesture(gesture, null, null)
    }

// 全局返回
fun goBack() {
    performGlobalAction(GLOBAL_ACTION_BACK)
}

// 返回桌面
fun goHome() {
    performGlobalAction(GLOBAL_ACTION_HOME)
}

//跳到无障碍
fun goAs(){
    val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
    startActivity(intent)
}

// 创建主协程
fun createCs(){
    CoroutineScope(Dispatchers.Main).launch {
        
    }
}

2. 应用控制

fun launchApp(packageName: String) {
        val packageManager: PackageManager = this.packageManager
        var intent: Intent? = packageManager.getLaunchIntentForPackage(packageName)

        if (intent != null) {
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            startActivity(intent)
        } else {
            Toast.makeText(this, "Application not found or cannot be launched.", Toast.LENGTH_SHORT)
                .show()
        }
    }

3. 手势操作

// 自定义滑动
fun performScrollGesture(x1: Float, y1: Float, x2: Float, y2: Float) {
        val path = Path()
        path.moveTo(x1, y1)
        path.lineTo(x2, y2)

        val strokeDescription = StrokeDescription(path, 0, 1000)
        val gestureDescription = GestureDescription.Builder()
            .addStroke(strokeDescription)
            .build()

        dispatchGesture(gestureDescription, null, null)
    }
// 屏幕滚动
fun scroll() {
        val displayMetrics = resources.displayMetrics
        val screenWidth = displayMetrics.widthPixels
        val screenHeight = displayMetrics.heightPixels

        val x = screenWidth / 2.0.toFloat()
        val y1 = screenHeight * 0.7.toFloat()
        val y2 = screenHeight * 0.3.toFloat()

        performScrollGesture(x, y1, x, y2)
    }

3. 节点操作

// 打印节点树
fun printNodeTreeWithCoordinates(
        node: AccessibilityNodeInfo?,
        indent: String = "",
        path: String = "",
        x: Int = 0,
        y: Int = 0,
        isLast: Boolean = true
    ) {
        if (node == null) return

        // 获取当前节点的信息
        val className = node.className?.toString() ?: "null"
        val viewId = node.viewIdResourceName ?: "null"
        val text = node.text?.toString() ?: "null"
        val desc = node.contentDescription?.toString() ?: "null"
        val clickable = node.isClickable
        val selected = node.isSelected
        val visibleToUser = node.isVisibleToUser

        // 构建并打印当前节点的信息,包括坐标 (x, y) 和连接符
        val coordinates = "($x,$y)"
        val connector = if (isLast) "└─" else "├─"
        val currentPath = if (path.isEmpty()) "$x" else "$path-$x"

        val txt = "$indent$connector $coordinates [${currentPath}] Class: $className, " +
                "ID: $viewId, Text: $text, desc: $desc, " +
                "clickable: $clickable, selected: $selected, visibleToUser: $visibleToUser"

        Log.d(TAG, txt)

        // 准备下一层级的前缀
        val newIndent = if (isLast) "$indent  " else "$indent│ "

        // 递归遍历子节点
        val childCount = node.childCount
        for (i in 0 until childCount) {
            val child = node.getChild(i)
            printNodeTreeWithCoordinates(
                child,
                newIndent,
                currentPath,
                i,
                y + 1,
                i == childCount - 1
            )
        }
    }

fun findNodeByPath(rootNode: AccessibilityNodeInfo?, path: String): AccessibilityNodeInfo? {
        if (rootNode == null) return null
        if (path.isEmpty()) return rootNode

        val indices = path.split("-").mapNotNull { it.toIntOrNull() }
        var currentNode: AccessibilityNodeInfo? = rootNode

        for ((i, index) in indices.withIndex()) {
            if (i == 0) {
                if (index != 0) {
                    currentNode = null
                    break
                }
            } else {
                if (currentNode == null || index < 0 || index >= currentNode.childCount) {
                    return null
                }
                currentNode = currentNode.getChild(index)
            }
        }

        return currentNode
    }
//查找所有text等于text的节点
fun findNodesByTextEquals(root: AccessibilityNodeInfo?, text: String): List<AccessibilityNodeInfo> {
    val result = mutableListOf<AccessibilityNodeInfo>()
    if (root == null) return result

    if ((root.text?.equals(text) == true) || (root.contentDescription?.equals(text) == true)) {
        result.add(root)
    }

    for (i in 0 until root.childCount) {
        val child = root.getChild(i)
        result.addAll(findNodesByTextEquals(child, text))
    }

    return result
}
//查找以text开头的节点
fun findNodesByTextStartsWith(root: AccessibilityNodeInfo?, text: String): List<AccessibilityNodeInfo> {
    val result = mutableListOf<AccessibilityNodeInfo>()
    if (root == null) return result

    if ((root.text?.startsWith(text) == true) || (root.contentDescription?.startsWith(text) == true)) {
        result.add(root)
    }

    for (i in 0 until root.childCount) {
        val child = root.getChild(i)
        result.addAll(findNodesByTextStartsWith(child, text))
    }

    return result
}
//查找以text结尾的节点
fun findNodesByTextEndsWith(root: AccessibilityNodeInfo?, text: String): List<AccessibilityNodeInfo> {
    val result = mutableListOf<AccessibilityNodeInfo>()
    if (root == null) return result

    if ((root.text?.endsWith(text) == true) || (root.contentDescription?.endsWith(text) == true)) {
        result.add(root)
    }

    for (i in 0 until root.childCount) {
        val child = root.getChild(i)
        result.addAll(findNodesByTextEndsWith(child, text))
    }

    return result
}

问题

1.Logcat乱码:help并且找到Edit Custom VM Options… 并打开文件,在文件中添加-Dfile.encoding=UTF-8

完整代码

完整代码:https://gitcode.com/qq_34854236/JinBi



上一篇
使用 Java AIO 实现简易内网穿透
下一篇
安卓开发 VpnService 使用,完成 TCP 和 UDP 协议(抓包)