创建无障碍服务

无障碍服务是一种应用,可增强界面,以协助残障用户或可能暂时无法与设备进行全面互动的用户。 这些服务在后台运行,并与系统通信以检查屏幕内容,并代表用户与应用互动。示例包括屏幕阅读器(如 TalkBack)、开关控制工具和语音控制系统。

本指南介绍了构建 Android 无障碍服务的基础知识。

无障碍服务生命周期

如需创建无障碍服务,您必须扩展 AccessibilityService 类,并在应用的清单中声明该服务。

创建服务类

创建一个扩展 AccessibilityService 的类。您必须替换以下方法:

  • onAccessibilityEvent:当系统检测到与服务配置匹配的事件(例如焦点更改或按钮点击)时调用。您的服务在此处解读界面。
  • onInterrupt:当系统中断服务的反馈(例如,在用户快速移动焦点时停止语音输出)时调用。
package com.example.android.apis.accessibility

import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.AccessibilityServiceInfo
import android.accessibilityservice.FingerprintGestureController
import android.accessibilityservice.AccessibilityButtonController
import android.accessibilityservice.GestureDescription
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.graphics.Path
import android.os.Build
import android.media.AudioManager
import android.content.Context

class MyAccessibilityService : AccessibilityService() {

    override fun onAccessibilityEvent(event: AccessibilityEvent) {
        // Interpret the event and provide feedback to the user
    }

    override fun onInterrupt() {
        // Interrupt any ongoing feedback
    }

    override fun onServiceConnected() {
        // Perform initialization here
    }
}

在清单中声明

AndroidManifest.xml 文件中注册您的服务。您必须严格 执行 BIND_ACCESSIBILITY_SERVICE 权限,以便只有系统可以绑定到您的服务。

如需确保设置按钮正常运行,请声明 ServiceSettingsActivity

<application>
  <service android:name=".accessibility.MyAccessibilityService"
      android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
      android:exported="true"
      android:label="@string/accessibility_service_label">
      <intent-filter>
          <action android:name="android.accessibilityservice.AccessibilityService" />
      </intent-filter>
      <meta-data
          android:name="android.accessibilityservice"
          android:resource="@xml/accessibility_service_config" />
  </service>

  <activity android:name=".accessibility.ServiceSettingsActivity"
      android:exported="true"
      android:label="@string/accessibility_service_settings_label" />
</application>

配置服务

res/xml/accessibility_service_config.xml 中创建一个配置文件。此文件定义了您的服务处理哪些事件以及提供哪些反馈。 请务必引用您在清单中声明的 ServiceSettingsActivity

<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/accessibility_service_description"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFlags="flagDefault|flagRequestFingerprintGestures|flagRequestAccessibilityButton"
    android:accessibilityFeedbackType="feedbackSpoken"
    android:notificationTimeout="100"
    android:canRetrieveWindowContent="true"
    android:canPerformGestures="true"
    android:settingsActivity="com.example.android.apis.accessibility.ServiceSettingsActivity" />

配置文件包含以下关键属性:

  • android:accessibilityEventTypes:您想要接收的事件。对于通用服务,请使用 typeAllMask
  • android:canRetrieveWindowContent:如果您的服务需要检查界面层次结构(例如,从屏幕读取文本),则必须为 true
  • android:canPerformGestures:如果您打算以编程方式调度手势(如滑动或点按),则必须为 true
  • android:accessibilityFlags:组合标志以启用功能。 指纹手势需要 flagRequestFingerprintGestures。软件无障碍功能按钮需要 flagRequestAccessibilityButton

如需查看配置选项的完整列表,请参阅 AccessibilityServiceInfo

运行时配置

虽然 XML 配置是静态的,但您也可以在运行时动态修改服务配置。这对于根据用户偏好设置切换功能非常有用。

替换 onServiceConnected() 以使用 setServiceInfo() 应用运行时更新:

override fun onServiceConnected() {
    val info = AccessibilityServiceInfo()

    // Set the type of events that this service wants to listen to.
    info.eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED or AccessibilityEvent.TYPE_VIEW_FOCUSED

    // Set the type of feedback your service provides.
    info.feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN

    // Set flags at runtime.
    info.flags = AccessibilityServiceInfo.FLAG_DEFAULT or
            AccessibilityServiceInfo.FLAG_REQUEST_FINGERPRINT_GESTURES

    this.setServiceInfo(info)
}

解读界面内容

onAccessibilityEvent() 触发时,系统会提供 AccessibilityEvent。此事件充当 无障碍功能树的入口点,无障碍功能树是屏幕内容的分层表示形式。

您的服务主要与 AccessibilityNodeInfo 对象互动,这些对象表示按钮、列表和文本等界面元素。有关这些界面元素的数据会归一化为 AccessibilityNodeInfo

以下示例展示了如何检索事件的来源并遍历无障碍功能树以查找信息。

override fun onAccessibilityEvent(event: AccessibilityEvent) {
    // Get the source node of the event
    val sourceNode: AccessibilityNodeInfo? = event.source

    if (sourceNode == null) return

    // Inspect properties
    if (sourceNode.isCheckable) {
        val state = if (sourceNode.isChecked) "Checked" else "Unchecked"
        val label = sourceNode.text ?: sourceNode.contentDescription
        
        // Provide feedback (for example, speak to the user)
        speakToUser("$label is $state")
    }

    // Always recycle nodes to prevent memory leaks
    sourceNode.recycle()
}

private fun speakToUser(text: String) {
    // Your text-to-speech implementation goes here
}

代表用户执行操作

无障碍服务可以代表用户执行操作,例如点击按钮或滚动列表。

如需执行操作,请对 AccessibilityNodeInfo 对象调用 performAction()

fun performClick(node: AccessibilityNodeInfo) {
    if (node.isClickable) {
        node.performAction(AccessibilityNodeInfo.ACTION_CLICK)
    }
}

对于影响整个系统的全局操作(例如按“返回”按钮或打开通知栏),请使用 performGlobalAction()

// Navigate back
fun navigateBack() {
    performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK)
}

管理焦点

Android 有两种截然不同的焦点类型:输入焦点(键盘输入 的位置)和无障碍功能焦点(无障碍服务检查的内容)。

以下代码段展示了如何查找当前具有无障碍功能焦点的元素:

// Find the node that currently has accessibility focus
// Note: rootInActiveWindow can be null if the window is not available
val root = rootInActiveWindow
if (root != null) {
    val focusedNode = root.findFocus(AccessibilityNodeInfo.FOCUS_ACCESSIBILITY)

    // Do something with focusedNode

    // Always recycle nodes
    focusedNode?.recycle()
    // rootInActiveWindow doesn't need to be recycled, but obtained nodes do.
}

以下代码段展示了如何将无障碍功能焦点移到特定元素:

// Request that the system give focus to a given node
fun focusNode(node: AccessibilityNodeInfo) {
    node.performAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS)
}

创建无障碍服务时,请尊重用户的焦点状态,除非用户操作明确触发,否则请避免窃取焦点。

执行手势

您的服务可以向屏幕调度自定义手势,例如滑动、点按或多点触控互动。为此,请在配置中声明 android:canPerformGestures="true",以便您可以使用 该 dispatchGesture() API。

简单手势

如需执行简单手势,请先创建一个 Path 对象来表示与给定手势关联的移动。然后,将 Path 封装在 GestureDescription 中以描述笔划。最后,调用 dispatchGesture 以调度手势。

fun swipeRight() {
    // Create a path for the swipe (from x=100 to x=500)
    val swipePath = Path()
    swipePath.moveTo(100f, 500f)
    swipePath.lineTo(500f, 500f)

    // Build the stroke description (0ms delay, 500ms duration)
    val stroke = GestureDescription.StrokeDescription(swipePath, 0, 500)

    // Build the gesture description
    val gestureBuilder = GestureDescription.Builder()
    gestureBuilder.addStroke(stroke)

    // Dispatch the gesture
    dispatchGesture(gestureBuilder.build(), object : AccessibilityService.GestureResultCallback() {
        override fun onCompleted(gestureDescription: GestureDescription?) {
            super.onCompleted(gestureDescription)
            // Gesture finished successfully
        }
    }, null)
}

连续手势

对于复杂的互动(例如绘制 L 形或执行精确的多步拖动),您可以使用 willContinue 参数将笔划链接在一起。

fun performLShapedGesture() {
    val path1 = Path().apply {
        moveTo(200f, 200f)
        lineTo(400f, 200f)
    }

    val path2 = Path().apply {
        moveTo(400f, 200f)
        lineTo(400f, 400f)
    }

    // First stroke: willContinue = true
    val stroke1 = GestureDescription.StrokeDescription(path1, 0, 500, true)

    // Second stroke: continues immediately after stroke1
    val stroke2 = stroke1.continueStroke(path2, 0, 500, false)

    val builder = GestureDescription.Builder()
    builder.addStroke(stroke1)
    builder.addStroke(stroke2)

    dispatchGesture(builder.build(), null, null)
}

音频管理

创建无障碍服务(尤其是屏幕阅读器)时,请使用 STREAM_ACCESSIBILITY 音频串流。这样,用户就可以独立于系统媒体音量控制服务音量。

fun increaseAccessibilityVolume() {
    val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
    audioManager.adjustStreamVolume(
        AudioManager.STREAM_ACCESSIBILITY,
        AudioManager.ADJUST_RAISE,
        0
    )
}

请务必在配置中添加 FLAG_ENABLE_ACCESSIBILITY_VOLUME 标志,无论是在 XML 中还是在运行时通过 setServiceInfo 添加。

高级功能

指纹手势

在搭载 Android 10(API 级别 29)或更高版本的设备上,您的服务可以捕获指纹传感器上的方向性滑动。这对于提供备用导航控件非常有用。

将以下逻辑添加到 onServiceConnected() 方法:

// Import: android.os.Build
// Import: android.accessibilityservice.FingerprintGestureController

private var gestureController: FingerprintGestureController? = null

override fun onServiceConnected() {
    // Check if the device is running Android 10 (Q) or higher
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        gestureController = fingerprintGestureController

        val callback = object : FingerprintGestureController.FingerprintGestureCallback() {
            override fun onGestureDetected(gesture: Int) {
                when (gesture) {
                    FingerprintGestureController.FINGERPRINT_GESTURE_SWIPE_DOWN -> {
                        // Handle swipe down
                    }
                    FingerprintGestureController.FINGERPRINT_GESTURE_SWIPE_UP -> {
                        // Handle swipe up
                    }
                }
            }
        }

        gestureController?.registerFingerprintGestureCallback(callback, null)
    }
}

无障碍功能按钮

在使用软件导航键的设备上,用户可以通过导航栏中的“无障碍功能按钮”调用您的服务。

如需使用此功能,请将 FLAG_REQUEST_ACCESSIBILITY_BUTTON 标志添加到服务配置。然后,将注册逻辑添加到 onServiceConnected() 方法。

// Import: android.accessibilityservice.AccessibilityButtonController

override fun onServiceConnected() {
    // ... existing initialization code ...

    val controller = accessibilityButtonController

    controller.registerAccessibilityButtonCallback(
        object : AccessibilityButtonController.AccessibilityButtonCallback() {
            override fun onClicked(controller: AccessibilityButtonController) {
                // Respond to button tap
            }
        }
    )
}

多语言文本转语音

如果源文本使用 LocaleSpan 标记,则朗读文本的服务可以自动切换语言。这样,您的服务就可以正确发音混合语言内容,而无需手动切换。

import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.LocaleSpan
import java.util.Locale

// Wrap text in LocaleSpan to indicate language
val spannable = SpannableStringBuilder("Bonjour")
spannable.setSpan(
    LocaleSpan(Locale.FRANCE),
    0,
    spannable.length,
    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)

当您的服务处理 AccessibilityNodeInfo 时,请检查 text 属性以查找 LocaleSpan 对象,以确定正确的文本转语音语言。

其他资源

如需了解详情,请参阅以下资源:

指南

Codelab

视图内容