创建您自己的辅助功能服务

辅助功能服务是一种增强用户界面以帮助残疾用户或可能暂时无法完全与设备交互的用户使用的应用。例如,正在驾驶、照顾幼儿或参加非常喧闹的派对的用户可能需要额外的或替代的界面反馈。

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 元素内,包含一个辅助功能服务意图过滤器。清单还必须通过添加 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,请在清单中添加 <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);
        }
    }
}

有关更多信息,请参阅 Google I/O 2017 上的 Android 辅助功能的新增功能 会议视频,从 6:35 开始。

辅助功能快捷方式

在运行 Android 8.0(API 级别 26)或更高版本的设备上,用户可以通过同时按住两个音量键来从任何屏幕启用和禁用他们首选的辅助功能服务。虽然此快捷方式默认启用和禁用 Talkback,但用户可以将按钮配置为启用和禁用安装在其设备上的任何服务。

为了让用户能够从辅助功能快捷方式访问特定的辅助功能服务,该服务需要在运行时请求该功能。

有关更多信息,请参阅 Google I/O 2017 上的 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);
    }
}

有关更多信息,请参阅 Google I/O 2017 上的 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);
        }
    }
}

有关更多信息,请参阅 Google I/O 2017 上的 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;
}

有关更多信息,请参阅 Google I/O 2017 上的 Android 辅助功能的新增功能 会议视频,从 10:59 开始。

代表用户执行操作

从 2011 年开始,辅助功能服务可以代表用户执行操作,包括更改输入焦点和选择(激活)用户界面元素。2012 年,操作范围扩展到包括滚动列表和与文本字段交互。辅助功能服务还可以执行全局操作,例如导航到主屏幕、按下后退按钮以及打开通知屏幕和最近使用的应用程序列表。从 2012 年开始,Android 包含辅助功能焦点,这使得所有可见元素都可以被辅助功能服务选择。

这些功能使辅助功能服务的开发者能够创建替代导航模式(例如手势导航),并为残疾用户提供对 Android 设备的更好控制。

侦听手势

辅助功能服务可以侦听特定手势,并通过代表用户采取行动来做出响应。此功能要求您的辅助功能服务请求激活“通过触控探索”功能。您的服务可以通过将服务的 flags 实例的 AccessibilityServiceInfo 成员设置为 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);
}

有关更多信息,请参阅 Google I/O 2017 中从 15:47 开始的 Android 辅助功能的新增功能 会议视频。

使用辅助功能操作

辅助功能服务可以代表用户采取行动,简化与应用的交互并提高工作效率。辅助功能服务执行操作的能力于 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 或按下 D 形方向键的中心按钮时,哪个屏幕上的用户界面元素接收输入。

用户界面中的一个元素可能具有输入焦点,而另一个元素具有辅助功能焦点。辅助功能焦点的目的是为辅助功能服务提供一种与屏幕上可见元素交互的方法,而不管该元素在系统角度是否可输入焦点。为了帮助确保您的辅助功能服务与应用的输入元素正确交互,请遵循 测试应用的辅助功能 的指南,在使用典型应用时测试您的服务。

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

收集信息

辅助功能服务具有收集和表示用户提供信息的关键单元(例如事件详细信息、文本和数字)的标准方法。

获取窗口更改详细信息

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

如果应用为其 View 对象定义了 辅助功能窗格标题,则您的服务可以识别应用的 UI 何时更新。当发生 TYPE_WINDOW_STATE_CHANGED 事件时,使用 getContentChangeTypes() 返回的类型来确定窗口如何更改。例如,框架可以检测窗格何时具有新标题或何时消失。

获取事件详细信息

Android 通过 AccessibilityEvent 对象向辅助功能服务提供有关用户界面交互的信息。在以前的 Android 版本中,辅助功能事件中可用的信息虽然提供了有关用户选择的 UI 控件的重要详细信息,但提供的上下文信息有限。在许多情况下,此缺少的上下文信息对于理解所选控件的含义至关重要。

日历或日程安排器是上下文至关重要的界面的一个示例。如果用户在周一至周五的日期列表中选择下午 4:00 的时间段,并且辅助功能服务宣布“下午 4 点”,但未宣布星期几、日期或月份,则结果反馈会令人困惑。在这种情况下,用户界面控件的上下文对于希望安排会议的用户至关重要。

自 2011 年以来,Android 通过基于视图层次结构组合辅助功能事件,大幅扩展了辅助功能服务可以获取的有关用户界面交互的信息量。视图层次结构是包含该组件(其父级)的用户界面组件以及该组件可能包含的用户界面元素(其子级)的集合。通过这种方式,Android 可以提供有关辅助功能事件的更丰富的详细信息,从而使辅助功能服务能够向用户提供更有用的反馈。

辅助功能服务通过系统传递到服务的 onAccessibilityEvent() 回调方法的 AccessibilityEvent 获取有关用户界面事件的信息。此对象提供有关事件的详细信息,包括正在操作的对象的类型、其描述性文本和其他详细信息。

  • AccessibilityEvent.getRecordCount()getRecord(int):这些方法允许您检索构成传递给您的 AccessibilityEventAccessibilityRecord 对象集。此级别的详细信息为触发辅助功能服务的事件提供了更多上下文。

  • AccessibilityRecord.getSource():此方法返回一个 AccessibilityNodeInfo 对象。此对象允许您请求源自辅助功能事件的组件的视图布局层次结构(父级和子级)。此功能允许辅助功能服务调查事件的完整上下文,包括任何包含视图或子视图的内容和状态。

Android 平台提供了使 AccessibilityService 能够查询视图层次结构的功能,收集有关生成事件的 UI 组件及其父级和子级的信息。为此,请在您的 XML 配置中设置以下行。

android:canRetrieveWindowContent="true"

完成后,使用 getSource() 获取 AccessibilityNodeInfo 对象。此调用仅在事件源自的窗口仍为活动窗口时返回对象。否则,它将返回 null,因此请相应地执行操作。

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

  1. 立即获取事件源自的视图的父级。

  2. 在该视图中,查找标签和复选框作为子视图。
  3. 如果找到它们,则创建一个字符串以报告给用户,指示标签及其是否被选中。

如果在遍历视图层次结构期间任何时候返回空值,则方法会静默放弃。

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)引入了多项功能,使您可以访问应用 UI 中的工具提示。使用getTooltipText()读取工具提示的文本,并使用ACTION_SHOW_TOOLTIPACTION_HIDE_TOOLTIP指示View的实例显示或隐藏其工具提示。

提示文本

从 2017 年开始,Android 包含多种与基于文本的对象的提示文本进行交互的方法。

屏幕上文本字符的位置

在运行 Android 8.0(API 级别 26)及更高版本的设备上,辅助功能服务可以确定TextView小部件中每个可见字符的边界框的屏幕坐标。服务通过调用refreshWithExtraData()找到这些坐标,并将EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY作为第一个参数,并将Bundle对象作为第二个参数传递。在方法执行时,系统会使用Rect对象的可打包数组填充Bundle参数。每个Rect对象都表示特定字符的边界框。

标准化单侧范围值

某些AccessibilityNodeInfo对象使用AccessibilityNodeInfo.RangeInfo的实例来指示 UI 元素可以采用一系列值。当使用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);
    ...
}

其他资源

要了解更多信息,请参阅以下资源。

指南

Codelabs