第二个 Android 11 开发者预览版现已推出,快来测试并分享您的反馈吧

创建自己的无障碍服务

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

Android 提供标准的无障碍服务(包括 TalkBack),开发者也可以创建和分发自己打造的服务。本文档介绍了打造无障碍服务的基础知识。

注意:只有在目的是帮助残障用户与您的应用互动的情况下,您的应用才能使用平台级无障碍服务。

从 Android 1.6(API 级别 4)开始,您就可以打造和部署无障碍服务,并且这些服务在 Android 4.0(API 级别 14)中得到了显著改进。Android 支持库也随着 Android 4.0 的发布得到更新,为这些增强的无障碍功能(自 Android 1.6 起)提供支持。对于旨在实现更广泛兼容的无障碍服务的开发者,我们鼓励他们使用该支持库并开发支持 Android 4.0 中引入的更高级的无障碍功能。

创建无障碍服务

无障碍服务可与普通的应用捆绑在一起,也可作为独立的 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() {
        }

    ...
    }
    

如果您为此服务创建了一个新项目,并且不打算将任何应用与该服务相关联,则可以从源代码中移除初始 Activity 类。

清单声明和权限

提供无障碍服务的应用必须在其应用清单中包含特定的声明,这样才能被 Android 系统视为无障碍服务。本部分介绍了无障碍服务的必需设置和可选设置。

无障碍服务声明

要被视为无障碍服务,您必须在清单的 application 元素中添加一个 service 元素(而非 activity 元素)。此外,在 service 元素中,您还必须添加一个无障碍服务 Intent 过滤器。为了与 Android 4.1 及更高版本兼容,该清单还必须保护该服务,具体做法是添加 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>
    

在 Android 1.6(API 级别 4)或更高版本上部署的所有无障碍服务都需要这些声明。

无障碍服务配置

无障碍服务还必须提供一项配置,此配置可指定该服务能够处理的无障碍事件类型以及有关该服务的其他信息。无障碍服务的配置包含在 AccessibilityServiceInfo 类中。您的服务可以在运行时使用此类的实例和 setServiceInfo() 来构建和设置配置。不过,使用此方法时,并非所有的配置选项都可用。

从 Android 4.0 开始,您可以在清单中添加一个引用了配置文件的 <meta-data> 元素。通过该元素,您能够为无障碍服务设置所有选项,如下例所示:

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

该元数据元素引用了您在应用的资源目录中创建的 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
            // won't be passed to this service.
            eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED or AccessibilityEvent.TYPE_VIEW_FOCUSED

            // If you only want this service to work with specific applications, set their
            // package names here. Otherwise, when the service is activated, it will listen
            // to events from all applications.
            packageNames = arrayOf("com.example.android.myFirstApp", "com.example.android.mySecondApp")

            // Set the type of feedback your service will provide.
            feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN

            // Default services are invoked only if no package-specific ones are present
            // for the type of AccessibilityEvent generated. This service *is*
            // application-specific, so the flag isn't necessary. If this was a
            // general-purpose service, it would be worth considering 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
        // won't be passed to this service.
        info.eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED |
                AccessibilityEvent.TYPE_VIEW_FOCUSED;

        // If you only want this service to work with specific applications, set their
        // package names here. Otherwise, when the service is activated, it will listen
        // to events from all applications.
        info.packageNames = new String[]
                {"com.example.android.myFirstApp", "com.example.android.mySecondApp"};

        // Set the type of feedback your service will provide.
        info.feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN;

        // Default services are invoked only if no package-specific ones are present
        // for the type of AccessibilityEvent generated. This service *is*
        // application-specific, so the flag isn't necessary. If this was a
        // general-purpose service, it would be worth considering 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() 来注册回调。

注意:此功能仅在提供软件渲染的导航区域的设备上可用。服务必须始终使用 isAccessibilityButtonAvailable() 并通过实现 onAvailabilityChanged() 来根据“无障碍”按钮的可用性响应更改。这样,即使在不支持“无障碍”按钮,或者“无障碍”按钮不可用的情况下,用户仍可以随时访问服务的功能。

以下代码段演示了如何配置无障碍服务,以响应用户按下“无障碍”按钮操作:

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_FINGERPRINT 权限和 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"
        ... />
    

MyFingerprintGestureService.java

Kotlin

    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

    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 秒开始)。

为用户执行操作

从 Android 4.0(API 级别 14)开始,无障碍服务可以代表用户执行操作,包括更改输入焦点和选择(激活)界面元素。在 Android 4.1(API 级别 16)中,操作的范围已扩展到包括滚动列表及与文本字段互动。无障碍服务还可以执行全局操作,例如导航至主屏幕、按“返回”按钮以及打开通知屏幕和最近用过的应用列表。Android 4.1 还包含一种新的焦点类型 Accessibilty Focus,它使所有可见元素都可以通过无障碍服务进行选择。

这些新功能使无障碍服务的开发者能够创建其他导航模式(例如手势导航),并使残障用户能够更好地控制他们的 Android 设备。

监听手势

无障碍服务可以监听特定手势,并通过代表用户执行操作进行响应。此功能已在 Android 4.1(API 级别 16)中添加,并要求无障碍服务请求激活“触摸浏览”功能。您的服务可以通过将服务的 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 秒开始)。

使用无障碍操作

无障碍服务可以代表用户执行操作,从而简化用户与应用之间的互动并提高互动效率。从 Android 4.0(API 级别 14)开始,无障碍服务便可以执行操作,并且这在 Android 4.1(API 级别 16)中得到了显著改进。

为了代表用户执行操作,您的无障碍服务必须进行注册,以从几个或很多个应用中接收事件,并通过在服务配置文件中将 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

                // take action 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

            // take action on behalf of the user
            nodeInfo.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);

            // recycle the nodeInfo object
            nodeInfo.recycle();
        }
        ...
    }
    

通过 performAction() 方法,您的服务可以在应用内执行操作。如果您的服务需要执行全局操作(例如,导航至主屏幕、按“返回”按钮以及打开通知屏幕和最近用过的应用列表),则使用 performGlobalAction() 方法。

使用焦点类型

Android 4.1(API 级别 16)引入了一种名为“无障碍功能焦点”的新型界面焦点。无障碍服务可以使用此类焦点来选择任何可见的界面元素并对其执行操作。此类焦点与更为人熟知的“输入焦点”不同,后者可决定当用户输入字符、按键盘上的 Enter 键或者按方向键控件的中间按钮时,屏幕上的哪种界面元素接收输入。

无障碍功能焦点完全独立于输入焦点。实际上,可能会出现以下情况:界面中的一个元素具有输入焦点,而另一个元素具有无障碍功能焦点。无障碍功能焦点旨在为无障碍服务提供一种与屏幕上的任何可见元素进行互动的方法,无论该元素能否从系统的角度聚焦输入。您可以通过测试无障碍手势来查看实际运用中的无障碍功能焦点。如需详细了解如何测试此功能,请参阅测试手势导航

注意:当某个元素能够进行此类聚焦时,使用无障碍功能焦点的无障碍服务负责同步当前的输入焦点。未同步输入焦点与无障碍功能焦点的服务可能会导致在执行某些操作时,希望输入焦点位于特定位置的应用内出现问题。

无障碍服务可以使用 AccessibilityNodeInfo.findFocus() 方法来确定哪个界面元素具有输入焦点或无障碍功能焦点。您还可以使用 focusSearch() 方法搜索可通过输入焦点选择的元素。最后,您的无障碍服务可以使用 performAction(AccessibilityNodeInfo.ACTION_SET_ACCESSIBILITY_FOCUS) 方法设置无障碍功能焦点。

收集信息

无障碍服务还提供了标准方法来收集和表示用户提供的信息的关键单元(例如,事件详细信息、文字和数字)。

获取事件详细信息

Android 系统可通过 AccessibilityEvent 对象向无障碍服务提供有关界面互动的信息。对于 Android 4.0 之前的版本,无障碍事件中的可用信息在提供有关用户所选的界面控件的大量详细信息时,提供的上下文信息比较有限。在很多情况下,这种缺失的上下文信息可能对于理解所选控件的含义至关重要。

例如,对于日历或每日计划,上下文就至关重要。如果用户在周一至周五的日期列表中选择了下午 4:00 的时段,而无障碍服务读出“下午 4 点”,但未说明哪个工作日、一个月中的哪一天或哪个月份,则生成的反馈会令人感到困惑。在这种情况下,界面控件的上下文对于想要安排会议的用户来说至关重要。

Android 4.0 通过基于视图层次结构撰写无障碍事件,极大地扩展了无障碍服务可以获取的有关界面互动的信息量。视图层次结构是一系列界面组件,其中包含组件(其父级)及该组件可能包含的界面元素(其子级)。通过这种方式,Android 系统可以提供有关无障碍事件的更多详细信息,从而使无障碍服务能够为用户提供更加实用的反馈。

无障碍服务可通过系统传递给服务的 onAccessibilityEvent() 回调方法的 AccessibilityEvent 获取有关界面事件的信息。此对象提供了有关事件的详细信息,包括操作对象的类型、其说明性文字以及其他详细信息。从 Android 4.0(可通过支持库中的 AccessibilityEventCompat 对象支持该版本)开始,您可以使用以下调用获取有关该事件的其他信息:

获取窗口更改详细信息

Android 9(API 级别 28)及更高版本允许多个应用在某个应用同时重新绘制多个窗口时跟踪窗口更新。发生 TYPE_WINDOWS_CHANGED 事件时,使用 getWindowChanges() API 来确定窗口是如何更改的。在多窗口更新期间,每个窗口都会生成一系列自己的事件。getSource() 方法会返回与每个事件关联的窗口的根视图。

如果某个应用为其 View 对象定义了无障碍窗格标题,则您的服务可以识别该应用的界面何时更新。发生 TYPE_WINDOW_STATE_CHANGED 事件时,使用 getContentChangeTypes() 返回的类型来确定窗口是如何更改的。例如,框架可以检测窗格何时具有新标题,或窗格何时消失。

收集无障碍功能节点详细信息

此步骤并非强制进行,但极为实用。Android 平台可让 AccessibilityService 查询视图层次结构、收集有关生成事件的界面组件及其父级和子级的信息。为此,请确保在 XML 配置中设置以下行:

    android:canRetrieveWindowContent="true"
    

完成该操作后,使用 getSource() 获取 AccessibilityNodeInfo 对象。此调用仅会在生成事件的窗口仍为活动窗口时返回对象。如果不是活动窗口,则会返回 null,并执行相应的操作。下面的示例显示了一个代码段,演示了在接收到事件时执行以下操作:

  1. 立即抓取生成事件的视图的父级
  2. 在该视图中,查找标签和复选框作为子视图
  3. 如果找到这些对象,则创建一个字符串来向用户报告,指明标签以及它是否被选中。
  4. 如果在遍历视图层次结构的过程中,在任意时间返回了 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 fired 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 or not it's complete, based on
        // the text inside the label, and the state of the check-box.
        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 fired 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 or not it's complete, based on
        // the text inside the label, and the state of the check-box.
        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 的实例显示或隐藏它们的提示。

提示文字

Android 8.0(API 级别 26)包含多种与基于文字的对象的提示文字进行互动的方法:

屏幕上的文字字符的位置

在搭载 Android 8.0(API 级别 26)及更高版本的设备上,无障碍服务可确定 TextView 微件中每个可见字符的边界框的屏幕坐标。无障碍服务通过调用 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