安卓无障碍开发指南
一、基础概念
AccessibilityService是Android提供的无障碍服务框架,主要功能包括:
- 监控界面变化
- 获取界面节点信息
- 模拟用户操作(点击、滑动、输入等)
- 跨应用交互
二、基础配置
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