Google 致力于为黑人社区推动种族平等。查看具体举措

创建自己的无障碍服务

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

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 还包含一种新的焦点类型,即“无障碍服务焦点”,它使所有可见元素都能由无障碍服务选择。

这些新功能使无障碍服务的开发者能够创建替代导航模式(如手势导航),并使残障用户能够更好地控制他们的 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