创建自己的无障碍服务

无障碍服务是一种应用,可改进界面,为残障用户或可能暂时无法与设备进行全面互动的用户提供帮助。例如,正在驾车、照顾小孩子或参加热闹非凡的派对的用户可能需要其他或备选的界面反馈。

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,方法是将 <meta-data> 标记添加到指向该 XML 文件的服务声明中。如果您将 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):借助这些方法,您可以检索对系统传递给您的 AccessibilityEvent 有贡献的一组 AccessibilityRecord 对象。这种详细程度可为触发无障碍服务的事件提供更多上下文。

  • 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