创建自己的无障碍服务(视图)

概念和 Jetpack Compose 实现

无障碍服务是一种应用,可增强用户界面,以帮助残障用户或可能暂时无法与设备进行全面互动的用户。例如,正在开车、照顾孩子或参加喧闹聚会的用户可能需要其他或替代的界面反馈方式。

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 属性,请参阅以下参考文档:

如需详细了解可在运行时动态设置的配置设置,请参阅 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 文件,具体方法是在服务声明中添加一个指向 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)及更高版本的设备上的无障碍服务可以响应沿着设备的指纹传感器进行的方向性滑动(向上、向下、向左和向右)。如需配置服务以接收有关这些互动的回调,请完成以下步骤:

  1. 声明 USE_BIOMETRIC 权限和 CAPABILITY_CAN_REQUEST_FINGERPRINT_GESTURES 功能。
  2. android:accessibilityFlags 属性中设置 FLAG_REQUEST_FINGERPRINT_GESTURES 标志。
  3. 使用 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,并执行相应的操作。

在以下示例中,当收到事件时,代码会执行以下操作:

  1. 立即抓取生成事件的视图的父级。
  2. 在该视图中,查找标签和复选框作为子视图。
  3. 如果找到这些对象,则创建一个字符串来向用户报告,指明标签以及它是否被选中。

如果在遍历视图层次结构的过程中,在任意时间返回了 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_TOOLTIPACTION_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)及更高版本的设备会以标准化方式表示单向范围:

对无障碍事件做出响应

现在,您的服务已开始运行并监听事件,请编写代码,以便在接收到 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);
    ...
}

其他资源

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

指南

Codelab