无障碍服务是一种应用,可改进界面,以协助残障用户或可能暂时无法与设备进行全面互动的用户。例如,正在开车、照顾年幼的孩子或参加非常嘈杂的派对的用户可能需要其他或替代的界面反馈。
Android 提供了标准的无障碍服务(包括 TalkBack),开发者可以创建和分发自己的服务。本文档介绍了构建无障碍服务的基础知识。
无障碍服务可以与常规应用捆绑在一起,也可以作为独立的 Android 项目创建。在这两种情况下,创建该服务的步骤都是相同的。
创建无障碍服务
在项目中,创建一个扩展 AccessibilityService
的类:
Kotlin
package com.example.android.apis.accessibility import android.accessibilityservice.AccessibilityService import android.view.accessibility.AccessibilityEvent class MyAccessibilityService : AccessibilityService() { ... override fun onInterrupt() {} override fun onAccessibilityEvent(event: AccessibilityEvent?) {} ... }
Java
package com.example.android.apis.accessibility; import android.accessibilityservice.AccessibilityService; import android.view.accessibility.AccessibilityEvent; public class MyAccessibilityService extends AccessibilityService { ... @Override public void onAccessibilityEvent(AccessibilityEvent event) { } @Override public void onInterrupt() { } ... }
如果您为此 Service
创建了一个新项目,但不打算让任何应用与之关联,可以从源代码中移除 Activity
起始类。
清单声明和权限
提供无障碍服务的应用必须在其应用清单中包含特定的声明,Android 系统才会将其视为无障碍服务。本部分介绍了无障碍服务的必需设置和可选设置。
无障碍服务声明
为了让您的应用被视为无障碍服务,请在清单的 application
元素内添加 service
元素,而不是 activity
元素。此外,请在 service
元素中添加一个无障碍服务 intent 过滤器。清单还必须通过添加 BIND_ACCESSIBILITY_SERVICE
权限来保护该服务,以确保只有系统可以绑定到它。示例如下:
<application> <service android:name=".MyAccessibilityService" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" android:label="@string/accessibility_service_label"> <intent-filter> <action android:name="android.accessibilityservice.AccessibilityService" /> </intent-filter> </service> </application>
无障碍服务配置
无障碍服务必须提供一项配置,指定服务处理的无障碍事件类型以及有关该服务的其他信息。无障碍服务的配置包含在 AccessibilityServiceInfo
类中。您的服务可以在运行时使用此类的实例和 setServiceInfo()
构建和设置配置。但是,使用此方法时,并非所有配置选项都可用。
您可以在清单中添加引用配置文件的 <meta-data>
元素,以便设置无障碍服务的完整选项,如以下示例所示:
<service android:name=".MyAccessibilityService"> ... <meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibility_service_config" /> </service>
此 <meta-data>
元素引用您在应用的资源目录中创建的 XML 文件:<project_dir>/res/xml/accessibility_service_config.xml>
。以下代码展示了服务配置文件的内容示例:
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:description="@string/accessibility_service_description" android:packageNames="com.example.android.apis" android:accessibilityEventTypes="typeAllMask" android:accessibilityFlags="flagDefault" android:accessibilityFeedbackType="feedbackSpoken" android:notificationTimeout="100" android:canRetrieveWindowContent="true" android:settingsActivity="com.example.android.accessibility.ServiceSettingsActivity" />
如需详细了解可在无障碍服务配置文件中使用的 XML 属性,请参阅以下参考文档:
android:description
android:packageNames
android:accessibilityEventTypes
android:accessibilityFlags
android:accessibilityFeedbackType
android:notificationTimeout
android:canRetrieveWindowContent
android:settingsActivity
如需详细了解可在运行时动态设置的配置设置,请参阅 AccessibilityServiceInfo
参考文档。
配置无障碍服务
在为无障碍服务设置配置变量时,请考虑以下事项,以告知系统如何以及何时运行:
- 您希望它响应哪些事件类型?
- 该服务是需要针对所有应用启用,还是只需针对特定软件包名称启用?
- 它使用哪些不同的反馈类型?
您可以通过两种方式设置这些变量。向后兼容的方式是在代码中使用 setServiceInfo(android.accessibilityservice.AccessibilityServiceInfo)
进行设置。为此,请替换 onServiceConnected()
方法并在其中配置服务,如以下示例所示:
Kotlin
override fun onServiceConnected() { info.apply { // Set the type of events that this service wants to listen to. Others // aren't passed to this service. eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED or AccessibilityEvent.TYPE_VIEW_FOCUSED // If you only want this service to work with specific apps, set their // package names here. Otherwise, when the service is activated, it // listens to events from all apps. packageNames = arrayOf("com.example.android.myFirstApp", "com.example.android.mySecondApp") // Set the type of feedback your service provides. feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN // Default services are invoked only if no package-specific services are // present for the type of AccessibilityEvent generated. This service is // app-specific, so the flag isn't necessary. For a general-purpose // service, consider setting the DEFAULT flag. // flags = AccessibilityServiceInfo.DEFAULT; notificationTimeout = 100 } this.serviceInfo = info }
Java
@Override public void onServiceConnected() { // Set the type of events that this service wants to listen to. Others // aren't passed to this service. info.eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED | AccessibilityEvent.TYPE_VIEW_FOCUSED; // If you only want this service to work with specific apps, set their // package names here. Otherwise, when the service is activated, it listens // to events from all apps. info.packageNames = new String[] {"com.example.android.myFirstApp", "com.example.android.mySecondApp"}; // Set the type of feedback your service provides. info.feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN; // Default services are invoked only if no package-specific services are // present for the type of AccessibilityEvent generated. This service is // app-specific, so the flag isn't necessary. For a general-purpose service, // consider setting the DEFAULT flag. // info.flags = AccessibilityServiceInfo.DEFAULT; info.notificationTimeout = 100; this.setServiceInfo(info); }
第二种方式是使用 XML 文件配置服务。某些配置选项(如 canRetrieveWindowContent
)仅在您使用 XML 配置服务时可用。使用 XML 进行定义时,上一个示例中的配置选项如下所示:
<accessibility-service android:accessibilityEventTypes="typeViewClicked|typeViewFocused" android:packageNames="com.example.android.myFirstApp, com.example.android.mySecondApp" android:accessibilityFeedbackType="feedbackSpoken" android:notificationTimeout="100" android:settingsActivity="com.example.android.apis.accessibility.TestBackActivity" android:canRetrieveWindowContent="true" />
如果您使用 XML,请通过在指向 XML 文件的服务声明中添加 <meta-data>
标记,在清单中引用它。如果您将 XML 文件存储在 res/xml/serviceconfig.xml
中,则新标记应如下所示:
<service android:name=".MyAccessibilityService"> <intent-filter> <action android:name="android.accessibilityservice.AccessibilityService" /> </intent-filter> <meta-data android:name="android.accessibilityservice" android:resource="@xml/serviceconfig" /> </service>
无障碍服务方法
无障碍服务必须扩展 AccessibilityService
类,并替换该类中的以下方法。这些方法按照 Android 系统调用它们的顺序显示:从服务启动时 (onServiceConnected()
) 到服务运行时(onAccessibilityEvent()
、onInterrupt()
),再到关闭时 (onUnbind()
)。
onServiceConnected()
:(可选)系统会在连接到无障碍服务时调用此方法。使用此方法可为您的服务执行一次性设置步骤,包括连接到用户反馈系统服务,例如音频管理器或设备振动器。如果您想在运行时设置服务配置或进行一次性调整,这对于调用setServiceInfo()
来说非常方便。onAccessibilityEvent()
:(必需)当系统检测到与无障碍服务指定的事件过滤参数匹配的AccessibilityEvent
时(例如,当用户点按某个按钮,或将焦点置于无障碍服务为其提供反馈的应用中的界面控件时),系统会回调此方法。当系统调用此方法时,它会传递关联的AccessibilityEvent
,服务随后可对其进行解读并利用它向用户提供反馈。此方法可以在服务的整个生命周期内被调用多次。onInterrupt()
:(必需)当系统想要中断服务提供的反馈时(通常是为了响应用户操作(例如将焦点移到其他控件)时,系统会调用此方法。此方法可以在服务的整个生命周期内被调用多次。onUnbind()
:(可选)系统会在系统即将关闭无障碍服务时调用此方法。使用此方法可执行任何一次性关机程序,包括取消分配用户反馈系统服务,例如音频管理器或设备振动器。
这些回调方法提供了无障碍服务的基本结构。您可以决定如何处理 Android 系统以 AccessibilityEvent
对象的形式提供的数据,并向用户提供反馈。如需详细了解如何从无障碍事件中获取信息,请参阅获取事件详情。
注册无障碍事件
无障碍服务配置参数最重要的功能之一是让您指定您的服务可以处理哪些类型的无障碍事件。指定此信息可让无障碍服务相互协作,并可让您灵活地仅处理来自特定应用的特定事件类型。事件过滤可以包含以下条件:
软件包名称:指定您希望服务处理其无障碍事件的应用软件包名称。如果省略此参数,您的无障碍服务会被视为可用于任何应用的服务无障碍事件。您可以在无障碍服务配置文件中使用
android:packageNames
属性将此参数设置为逗号分隔列表,或使用AccessibilityServiceInfo.packageNames
成员。事件类型:指定您希望服务处理的无障碍事件类型。您可以在无障碍服务配置文件中使用
android:accessibilityEventTypes
属性将此参数设置为以|
字符分隔的列表(例如accessibilityEventTypes="typeViewClicked|typeViewFocused"
)。或者,您可以使用AccessibilityServiceInfo.eventTypes
成员进行设置。
设置无障碍服务时,请仔细考虑您的服务可以处理哪些事件,并仅注册这些事件。由于用户一次可以激活多项无障碍服务,因此您的服务不得使用无法处理的事件。请注意,其他服务可能会处理这些事件以改善用户体验。
无障碍功能音量
搭载 Android 8.0(API 级别 26)及更高版本的设备包含 STREAM_ACCESSIBILITY
音量类别,该类别可让您控制无障碍服务音频输出的音量,而不影响设备上的其他声音。
无障碍服务可以通过设置 FLAG_ENABLE_ACCESSIBILITY_VOLUME
选项来使用此流类型。然后,您可以通过对设备的 AudioManager
实例调用 adjustStreamVolume()
方法来更改设备的无障碍音频音量。
以下代码段演示了无障碍服务如何使用 STREAM_ACCESSIBILITY
音量类别:
Kotlin
import android.media.AudioManager.* class MyAccessibilityService : AccessibilityService() { private val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager override fun onAccessibilityEvent(accessibilityEvent: AccessibilityEvent) { if (accessibilityEvent.source.text == "Increase volume") { audioManager.adjustStreamVolume(AudioManager.STREAM_ACCESSIBILITY, ADJUST_RAISE, 0) } } }
Java
import static android.media.AudioManager.*; public class MyAccessibilityService extends AccessibilityService { private AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE); @Override public void onAccessibilityEvent(AccessibilityEvent accessibilityEvent) { AccessibilityNodeInfo interactedNodeInfo = accessibilityEvent.getSource(); if (interactedNodeInfo.getText().equals("Increase volume")) { audioManager.adjustStreamVolume(AudioManager.STREAM_ACCESSIBILITY, ADJUST_RAISE, 0); } } }
如需了解详情,请观看 2017 年 Google I/O 大会的 Android 无障碍功能的新变化会议视频(从 6 分 35 秒开始)。
无障碍功能快捷方式
在搭载 Android 8.0(API 级别 26)及更高版本的设备上,用户可以通过同时按住两个音量键从任何屏幕启用和停用他们首选的无障碍服务。虽然此快捷方式默认启用和停用 Talkback,但用户可以配置该按钮以启用和停用其设备上安装的任何服务。
为了让用户能够通过无障碍功能快捷方式访问特定的无障碍服务,该服务需要在运行时请求该功能。
如需了解详情,请观看 2017 年 Google I/O 大会的 Android 无障碍功能的新变化会议视频(从 13 分 25 秒开始)。
“无障碍”按钮
在使用软件渲染的导航区域且搭载 Android 8.0(API 级别 26)或更高版本的设备上,导航栏的右侧包含一个“无障碍”按钮。当用户按下此按钮时,他们可以根据屏幕上当前显示的内容,调用多种已启用的无障碍功能和服务之一。
如需允许用户使用“无障碍”按钮调用指定的无障碍服务,该服务需要在 AccessibilityServiceInfo
对象的 android:accessibilityFlags
属性中添加 FLAG_REQUEST_ACCESSIBILITY_BUTTON
标志。然后,服务可以使用 registerAccessibilityButtonCallback()
注册回调。
以下代码段演示了如何配置无障碍服务,以响应用户按“无障碍”按钮的操作:
Kotlin
private var mAccessibilityButtonController: AccessibilityButtonController? = null private var accessibilityButtonCallback: AccessibilityButtonController.AccessibilityButtonCallback? = null private var mIsAccessibilityButtonAvailable: Boolean = false override fun onServiceConnected() { mAccessibilityButtonController = accessibilityButtonController mIsAccessibilityButtonAvailable = mAccessibilityButtonController?.isAccessibilityButtonAvailable ?: false if (!mIsAccessibilityButtonAvailable) return serviceInfo = serviceInfo.apply { flags = flags or AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON } accessibilityButtonCallback = object : AccessibilityButtonController.AccessibilityButtonCallback() { override fun onClicked(controller: AccessibilityButtonController) { Log.d("MY_APP_TAG", "Accessibility button pressed!") // Add custom logic for a service to react to the // accessibility button being pressed. } override fun onAvailabilityChanged( controller: AccessibilityButtonController, available: Boolean ) { if (controller == mAccessibilityButtonController) { mIsAccessibilityButtonAvailable = available } } } accessibilityButtonCallback?.also { mAccessibilityButtonController?.registerAccessibilityButtonCallback(it, null) } }
Java
private AccessibilityButtonController accessibilityButtonController; private AccessibilityButtonController .AccessibilityButtonCallback accessibilityButtonCallback; private boolean mIsAccessibilityButtonAvailable; @Override protected void onServiceConnected() { accessibilityButtonController = getAccessibilityButtonController(); mIsAccessibilityButtonAvailable = accessibilityButtonController.isAccessibilityButtonAvailable(); if (!mIsAccessibilityButtonAvailable) { return; } AccessibilityServiceInfo serviceInfo = getServiceInfo(); serviceInfo.flags |= AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON; setServiceInfo(serviceInfo); accessibilityButtonCallback = new AccessibilityButtonController.AccessibilityButtonCallback() { @Override public void onClicked(AccessibilityButtonController controller) { Log.d("MY_APP_TAG", "Accessibility button pressed!"); // Add custom logic for a service to react to the // accessibility button being pressed. } @Override public void onAvailabilityChanged( AccessibilityButtonController controller, boolean available) { if (controller.equals(accessibilityButtonController)) { mIsAccessibilityButtonAvailable = available; } } }; if (accessibilityButtonCallback != null) { accessibilityButtonController.registerAccessibilityButtonCallback( accessibilityButtonCallback, null); } }
如需了解详情,请观看 2017 年 Google I/O 大会的 Android 无障碍功能的新变化会议视频(从 16 分 28 秒开始)。
指纹手势
搭载 Android 8.0(API 级别 26)或更高版本的设备上的无障碍服务可以响应设备指纹传感器的方向性滑动(向上、向下、向左和向右)。如需配置服务以接收有关这些互动的回调,请完成以下顺序:
- 声明
USE_BIOMETRIC
权限和CAPABILITY_CAN_REQUEST_FINGERPRINT_GESTURES
功能。 - 在
android:accessibilityFlags
属性中设置FLAG_REQUEST_FINGERPRINT_GESTURES
标志。 - 使用
registerFingerprintGestureCallback()
注册回调。
请注意,并非所有设备都包含指纹传感器。如需确定设备是否支持传感器,请使用 isHardwareDetected()
方法。即使在具有指纹传感器的设备上,当该传感器用于身份验证目的时,您的服务也无法使用它。如需确定传感器何时可用,请调用 isGestureDetectionAvailable()
方法并实现 onGestureDetectionAvailabilityChanged()
回调。
以下代码段展示了使用指纹手势在虚拟游戏板上进行导航的示例:
// AndroidManifest.xml <manifest ... > <uses-permission android:name="android.permission.USE_FINGERPRINT" /> ... <application> <service android:name="com.example.MyFingerprintGestureService" ... > <meta-data android:name="android.accessibilityservice" android:resource="@xml/myfingerprintgestureservice" /> </service> </application> </manifest>
// myfingerprintgestureservice.xml <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" ... android:accessibilityFlags=" ... |flagRequestFingerprintGestures" android:canRequestFingerprintGestures="true" ... />
Kotlin
// MyFingerprintGestureService.kt import android.accessibilityservice.FingerprintGestureController.* class MyFingerprintGestureService : AccessibilityService() { private var gestureController: FingerprintGestureController? = null private var fingerprintGestureCallback: FingerprintGestureController.FingerprintGestureCallback? = null private var mIsGestureDetectionAvailable: Boolean = false override fun onCreate() { gestureController = fingerprintGestureController mIsGestureDetectionAvailable = gestureController?.isGestureDetectionAvailable ?: false } override fun onServiceConnected() { if (mFingerprintGestureCallback != null || !mIsGestureDetectionAvailable) return fingerprintGestureCallback = object : FingerprintGestureController.FingerprintGestureCallback() { override fun onGestureDetected(gesture: Int) { when (gesture) { FINGERPRINT_GESTURE_SWIPE_DOWN -> moveGameCursorDown() FINGERPRINT_GESTURE_SWIPE_LEFT -> moveGameCursorLeft() FINGERPRINT_GESTURE_SWIPE_RIGHT -> moveGameCursorRight() FINGERPRINT_GESTURE_SWIPE_UP -> moveGameCursorUp() else -> Log.e(MY_APP_TAG, "Error: Unknown gesture type detected!") } } override fun onGestureDetectionAvailabilityChanged(available: Boolean) { mIsGestureDetectionAvailable = available } } fingerprintGestureCallback?.also { gestureController?.registerFingerprintGestureCallback(it, null) } } }
Java
// MyFingerprintGestureService.java import static android.accessibilityservice.FingerprintGestureController.*; public class MyFingerprintGestureService extends AccessibilityService { private FingerprintGestureController gestureController; private FingerprintGestureController .FingerprintGestureCallback fingerprintGestureCallback; private boolean mIsGestureDetectionAvailable; @Override public void onCreate() { gestureController = getFingerprintGestureController(); mIsGestureDetectionAvailable = gestureController.isGestureDetectionAvailable(); } @Override protected void onServiceConnected() { if (fingerprintGestureCallback != null || !mIsGestureDetectionAvailable) { return; } fingerprintGestureCallback = new FingerprintGestureController.FingerprintGestureCallback() { @Override public void onGestureDetected(int gesture) { switch (gesture) { case FINGERPRINT_GESTURE_SWIPE_DOWN: moveGameCursorDown(); break; case FINGERPRINT_GESTURE_SWIPE_LEFT: moveGameCursorLeft(); break; case FINGERPRINT_GESTURE_SWIPE_RIGHT: moveGameCursorRight(); break; case FINGERPRINT_GESTURE_SWIPE_UP: moveGameCursorUp(); break; default: Log.e(MY_APP_TAG, "Error: Unknown gesture type detected!"); break; } } @Override public void onGestureDetectionAvailabilityChanged(boolean available) { mIsGestureDetectionAvailable = available; } }; if (fingerprintGestureCallback != null) { gestureController.registerFingerprintGestureCallback( fingerprintGestureCallback, null); } } }
如需了解详情,请观看 2017 年 Google I/O 大会的 Android 无障碍功能的新变化会议视频(从 9 分 03 秒开始)。
多语言文字转语音
从 Android 8.0(API 级别 26)开始,Android 的文字转语音 (TTS) 服务可以识别并读出单个文本块中以多种语言显示的词组。如需在无障碍服务中启用这种自动语言切换功能,请将所有字符串封装在 LocaleSpan
对象中,如以下代码段所示:
Kotlin
val localeWrappedTextView = findViewById<TextView>(R.id.my_french_greeting_text).apply { text = wrapTextInLocaleSpan("Bonjour!", Locale.FRANCE) } private fun wrapTextInLocaleSpan(originalText: CharSequence, loc: Locale): SpannableStringBuilder { return SpannableStringBuilder(originalText).apply { setSpan(LocaleSpan(loc), 0, originalText.length - 1, 0) } }
Java
TextView localeWrappedTextView = findViewById(R.id.my_french_greeting_text); localeWrappedTextView.setText(wrapTextInLocaleSpan("Bonjour!", Locale.FRANCE)); private SpannableStringBuilder wrapTextInLocaleSpan( CharSequence originalText, Locale loc) { SpannableStringBuilder myLocaleBuilder = new SpannableStringBuilder(originalText); myLocaleBuilder.setSpan(new LocaleSpan(loc), 0, originalText.length() - 1, 0); return myLocaleBuilder; }
如需了解详情,请观看 2017 年 Google I/O 大会的 Android 无障碍功能的新变化会议视频(从 10 分 59 秒开始)。
代表用户执行操作
从 2011 年开始,无障碍服务可以代表用户执行操作,包括更改输入焦点以及选择(激活)界面元素。2012 年,我们扩大了操作范围,新增了滚动列表以及与文本字段互动。无障碍服务还可以执行全局操作,例如导航到主屏幕、按“返回”按钮,以及打开通知屏幕和最近用过的应用列表。自 2012 年以来,Android 引入了无障碍功能焦点,使所有可见元素都可由无障碍服务选择。
借助这些功能,无障碍服务的开发者可以创建替代导航模式(例如手势导航),并使残障用户能够更好地控制其 Android 设备。
监听手势
无障碍服务可以监听特定手势,并通过代表用户执行操作进行响应。此功能要求无障碍服务请求激活“触摸浏览”功能。您的服务可以通过将服务的 AccessibilityServiceInfo
实例的 flags
成员设置为 FLAG_REQUEST_TOUCH_EXPLORATION_MODE
来请求激活,如以下示例所示。
Kotlin
class MyAccessibilityService : AccessibilityService() { override fun onCreate() { serviceInfo.flags = AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE } ... }
Java
public class MyAccessibilityService extends AccessibilityService { @Override public void onCreate() { getServiceInfo().flags = AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE; } ... }
在您的服务请求激活“触摸浏览”后,用户必须让该功能处于启用状态(如果该功能尚未激活)。当此功能处于启用状态时,您的服务会通过服务的 onGesture()
回调方法接收无障碍手势的通知,并且可以代表用户执行操作进行响应。
连续手势
搭载 Android 8.0(API 级别 26)的设备支持连续手势或包含多个 Path
对象的程序化手势。
指定一系列笔画时,您可以使用 GestureDescription.StrokeDescription
构造函数中的最后一个参数 willContinue
指定这些笔画属于同一程序化手势,如以下代码段所示:
Kotlin
// Simulates an L-shaped drag path: 200 pixels right, then 200 pixels down. private fun doRightThenDownDrag() { val dragRightPath = Path().apply { moveTo(200f, 200f) lineTo(400f, 200f) } val dragRightDuration = 500L // 0.5 second // The starting point of the second path must match // the ending point of the first path. val dragDownPath = Path().apply { moveTo(400f, 200f) lineTo(400f, 400f) } val dragDownDuration = 500L val rightThenDownDrag = GestureDescription.StrokeDescription( dragRightPath, 0L, dragRightDuration, true ).apply { continueStroke(dragDownPath, dragRightDuration, dragDownDuration, false) } }
Java
// Simulates an L-shaped drag path: 200 pixels right, then 200 pixels down. private void doRightThenDownDrag() { Path dragRightPath = new Path(); dragRightPath.moveTo(200, 200); dragRightPath.lineTo(400, 200); long dragRightDuration = 500L; // 0.5 second // The starting point of the second path must match // the ending point of the first path. Path dragDownPath = new Path(); dragDownPath.moveTo(400, 200); dragDownPath.lineTo(400, 400); long dragDownDuration = 500L; GestureDescription.StrokeDescription rightThenDownDrag = new GestureDescription.StrokeDescription(dragRightPath, 0L, dragRightDuration, true); rightThenDownDrag.continueStroke(dragDownPath, dragRightDuration, dragDownDuration, false); }
如需了解详情,请观看 2017 年 Google I/O 大会的 Android 无障碍功能的新变化会议视频(从 15 分 47 秒开始)。
使用无障碍操作
无障碍服务可以代表用户执行操作,以简化与应用的互动并提高工作效率。我们在 2011 年新增了无障碍服务执行操作的功能,并于 2012 年大幅拓展。
如需代表用户执行操作,您的无障碍服务必须注册以接收来自应用的事件,并通过在服务配置文件中将 android:canRetrieveWindowContent
设置为 true
来请求查看应用内容的权限。当您的服务收到事件后,它便可以使用 getSource()
从事件中检索 AccessibilityNodeInfo
对象。借助 AccessibilityNodeInfo
对象,您的服务可以探索视图层次结构以确定要执行的操作,然后使用 performAction()
为用户执行操作。
Kotlin
class MyAccessibilityService : AccessibilityService() { override fun onAccessibilityEvent(event: AccessibilityEvent) { // Get the source node of the event. event.source?.apply { // Use the event and node information to determine what action to // take. // Act on behalf of the user. performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) // Recycle the nodeInfo object. recycle() } } ... }
Java
public class MyAccessibilityService extends AccessibilityService { @Override public void onAccessibilityEvent(AccessibilityEvent event) { // Get the source node of the event. AccessibilityNodeInfo nodeInfo = event.getSource(); // Use the event and node information to determine what action to take. // Act on behalf of the user. nodeInfo.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); // Recycle the nodeInfo object. nodeInfo.recycle(); } ... }
借助 performAction()
方法,您的服务可在应用内执行操作。如果您的服务需要执行全局操作,例如导航到主屏幕、点按“返回”按钮或打开通知屏幕或最近用过的应用列表,请使用 performGlobalAction()
方法。
使用焦点类型
2012 年,Android 引入了一个名为“无障碍功能焦点”的界面焦点。无障碍服务可以使用此焦点来选择任何可见的界面元素并对其执行操作。此焦点类型与“输入焦点”不同,后者用于确定当用户输入字符、按键盘上的 Enter 键或按方向键的中心按钮时,屏幕上的哪个界面元素会接收输入。
可能会出现以下情况:界面中的一个元素具有输入焦点,而另一个元素具有无障碍功能焦点。无障碍功能焦点旨在为无障碍服务提供一种与屏幕上的可见元素互动的方法,无论从系统的角度来看,这些元素是否可聚焦输入。为帮助确保您的无障碍服务能够与应用的输入元素正确互动,请遵循测试应用无障碍功能的相关准则,以便在使用典型应用时测试您的服务。
无障碍服务可以使用 AccessibilityNodeInfo.findFocus()
方法确定哪个界面元素具有输入焦点或无障碍功能焦点。您还可以使用 focusSearch()
方法搜索可通过输入焦点选择的元素。最后,您的无障碍服务可以使用 performAction(AccessibilityNodeInfo.ACTION_SET_ACCESSIBILITY_FOCUS)
方法设置无障碍功能焦点。
网罗信息,集思广益
无障碍服务具有收集和表示用户提供的信息的关键单元(例如事件详细信息、文本和数字)的标准方法。
获取窗口更改详细信息
Android 9(API 级别 28)及更高版本允许应用在同时重新绘制多个窗口时跟踪窗口更新。发生 TYPE_WINDOWS_CHANGED
事件时,请使用 getWindowChanges()
API 确定窗口如何变化。在多窗口更新期间,每个窗口都会生成自己的一组事件。getSource()
方法会返回与每个事件关联的窗口的根视图。
如果应用为其 View
对象定义了无障碍窗格标题,您的服务就可以识别该应用的界面何时更新。发生 TYPE_WINDOW_STATE_CHANGED
事件时,请使用 getContentChangeTypes()
返回的类型来确定窗口如何变化。例如,框架可以检测窗格何时具有新标题或窗格何时消失。
获取事件详细信息
Android 通过 AccessibilityEvent
对象向无障碍服务提供有关界面互动的信息。在以前的 Android 版本中,无障碍事件中提供的信息在提供有关用户所选界面控件的重要详细信息的同时,提供的上下文信息也有限。在许多情况下,这种缺失的上下文信息可能对于理解所选控件的含义至关重要。
举例来说,日历或日程规划工具的上下文至关重要。如果用户在周一至周五的日期列表中选择了下午 4:00 的时段,并且无障碍服务读出“下午 4 点”,但未读出工作日名称、月份中的日期或月份名称,那么生成的反馈就会令人困惑。在这种情况下,界面控件的上下文对于想要安排会议的用户来说至关重要。
自 2011 年以来,Android 通过基于视图层次结构编写无障碍事件,大大增加了无障碍服务可以获取的有关界面互动的信息量。视图层次结构是一组界面组件,其中包含该组件(其父级)和该组件可能包含的界面元素(其子级)。通过这种方式,Android 可以提供有关无障碍事件的更丰富细节,让无障碍服务能够为用户提供更实用的反馈。
无障碍服务可通过系统传递给服务的 onAccessibilityEvent()
回调方法的 AccessibilityEvent
获取有关界面事件的信息。此对象提供了有关事件的详细信息,包括操作对象的类型、其描述性文字和其他详细信息。
AccessibilityEvent.getRecordCount()
和getRecord(int)
:您可以使用这些方法检索一组AccessibilityRecord
对象,这些对象对系统传递给您的AccessibilityEvent
有贡献。此级别的详细信息为触发无障碍服务的事件提供了更多上下文。AccessibilityRecord.getSource()
:此方法会返回一个AccessibilityNodeInfo
对象。借助此对象,您可以请求生成无障碍事件的组件的视图布局层次结构(父级和子级)。借助此功能,无障碍服务可以调查事件的完整上下文,包括任何封闭视图或子视图的内容和状态。
Android 平台为 AccessibilityService
提供了查询视图层次结构的功能,以便收集有关生成事件的界面组件及其父级和子级的信息。为此,请在 XML 配置中设置以下行:
android:canRetrieveWindowContent="true"
完成后,使用 getSource()
获取 AccessibilityNodeInfo
对象。仅当生成事件的窗口仍为活动窗口时,此调用才会返回对象。否则,它会返回 null,因此请采取相应措施。
在以下示例中,代码会在收到事件时执行以下操作:
- 立即获取生成事件的视图的父级。
- 在该视图中,查找标签和复选框作为子视图。
- 如果找到这些标签,则创建一个字符串来向用户报告,指明标签以及是否已被选中。
如果在遍历视图层次结构时在任意时间点返回 null 值,该方法会静默放弃。
Kotlin
// Alternative onAccessibilityEvent that uses AccessibilityNodeInfo. override fun onAccessibilityEvent(event: AccessibilityEvent) { val source: AccessibilityNodeInfo = event.source ?: return // Grab the parent of the view that fires the event. val rowNode: AccessibilityNodeInfo = getListItemNodeInfo(source) ?: return // Using this parent, get references to both child nodes, the label, and the // checkbox. val taskLabel: CharSequence = rowNode.getChild(0)?.text ?: run { rowNode.recycle() return } val isComplete: Boolean = rowNode.getChild(1)?.isChecked ?: run { rowNode.recycle() return } // Determine what the task is and whether it's complete based on the text // inside the label, and the state of the checkbox. if (rowNode.childCount < 2 || !rowNode.getChild(1).isCheckable) { rowNode.recycle() return } val completeStr: String = if (isComplete) { getString(R.string.checked) } else { getString(R.string.not_checked) } val reportStr = "$taskLabel$completeStr" speakToUser(reportStr) }
Java
// Alternative onAccessibilityEvent that uses AccessibilityNodeInfo. @Override public void onAccessibilityEvent(AccessibilityEvent event) { AccessibilityNodeInfo source = event.getSource(); if (source == null) { return; } // Grab the parent of the view that fires the event. AccessibilityNodeInfo rowNode = getListItemNodeInfo(source); if (rowNode == null) { return; } // Using this parent, get references to both child nodes, the label, and the // checkbox. AccessibilityNodeInfo labelNode = rowNode.getChild(0); if (labelNode == null) { rowNode.recycle(); return; } AccessibilityNodeInfo completeNode = rowNode.getChild(1); if (completeNode == null) { rowNode.recycle(); return; } // Determine what the task is and whether it's complete based on the text // inside the label, and the state of the checkbox. if (rowNode.getChildCount() < 2 || !rowNode.getChild(1).isCheckable()) { rowNode.recycle(); return; } CharSequence taskLabel = labelNode.getText(); final boolean isComplete = completeNode.isChecked(); String completeStr = null; if (isComplete) { completeStr = getString(R.string.checked); } else { completeStr = getString(R.string.not_checked); } String reportStr = taskLabel + completeStr; speakToUser(reportStr); }
现在,您已经拥有了一种可正常运行的完整无障碍服务。通过添加 Android 的文字转语音引擎或使用 Vibrator
提供触感反馈,尝试配置它与用户的互动方式。
处理文字
搭载 Android 8.0(API 级别 26)及更高版本的设备包含多项文字处理功能,可让无障碍服务更轻松地识别屏幕上显示的特定文字单元并对其进行操作。
提示
Android 9(API 级别 28)引入了几项功能,可让您访问应用界面中的提示。使用 getTooltipText()
读取提示的文本,并使用 ACTION_SHOW_TOOLTIP
和 ACTION_HIDE_TOOLTIP
指示 View
的实例显示或隐藏提示。
提示文字
从 2017 年开始,Android 包含多种与基于文本的对象的提示文本互动的方法:
isShowingHintText()
和setShowingHintText()
方法分别指示和设置节点的当前文本内容是否表示节点的提示文本。getHintText()
可提供对提示文本本身的访问权限。即使某个对象未显示提示文本,调用getHintText()
也会成功。
屏幕上的文字字符的位置
在搭载 Android 8.0(API 级别 26)及更高版本的设备上,无障碍服务可以确定 TextView
widget 中每个可见字符的边界框的屏幕坐标。服务通过调用 refreshWithExtraData()
并传入 EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY
作为第一个参数,传入 Bundle
对象作为第二个参数,以此来找到这些坐标。随着方法的执行,系统会使用 Rect
对象的 Parcelable 数组填充 Bundle
参数。每个 Rect
对象都表示特定字符的边界框。
标准化的单向范围值
某些 AccessibilityNodeInfo
对象使用 AccessibilityNodeInfo.RangeInfo
实例来指示界面元素可以采用一系列值。使用 RangeInfo.obtain()
创建范围时,或使用 getMin()
和 getMax()
检索范围的极值时,请注意,搭载 Android 8.0(API 级别 26)及更高版本的设备以标准化方式表示单端范围:
- 对于没有最小值的范围,
Float.NEGATIVE_INFINITY
表示最小值。 - 对于没有最大值的范围,
Float.POSITIVE_INFINITY
表示最大值。
对无障碍事件做出响应
现在,您的服务已设置为运行并监听事件,接下来请编写代码,使其知道在 AccessibilityEvent
到达时要执行的操作。首先,替换 onAccessibilityEvent(AccessibilityEvent)
方法。在该方法中,使用 getEventType()
确定事件类型,并使用 getContentDescription()
提取与触发事件的视图相关联的任何标签文本:
Kotlin
override fun onAccessibilityEvent(event: AccessibilityEvent) { var eventText: String = when (event.eventType) { AccessibilityEvent.TYPE_VIEW_CLICKED -> "Clicked: " AccessibilityEvent.TYPE_VIEW_FOCUSED -> "Focused: " else -> "" } eventText += event.contentDescription // Do something nifty with this text, like speak the composed string back to // the user. speakToUser(eventText) ... }
Java
@Override public void onAccessibilityEvent(AccessibilityEvent event) { final int eventType = event.getEventType(); String eventText = null; switch(eventType) { case AccessibilityEvent.TYPE_VIEW_CLICKED: eventText = "Clicked: "; break; case AccessibilityEvent.TYPE_VIEW_FOCUSED: eventText = "Focused: "; break; } eventText = eventText + event.getContentDescription(); // Do something nifty with this text, like speak the composed string back to // the user. speakToUser(eventText); ... }
其他资源
如需了解详情,请参阅以下资源: