创建您自己的无障碍服务

无障碍服务 是一个增强用户界面以帮助有残疾或暂时无法完全与设备交互的用户使用的应用。例如,开车、照顾年幼的孩子或参加非常吵闹的派对的用户可能需要额外的或替代的界面反馈。

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 设备的更佳控制。

监听手势

辅助功能服务可以监听特定手势并通过代表用户操作来响应。 此功能要求您的辅助功能服务请求激活触控探索功能。 您的服务可以通过将其 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);
}

有关更多信息,请参阅 Google I/O 2017 中的 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 或按下 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 中,辅助功能事件中可用的信息虽然提供了有关用户选择的用户界面控件的重大详细信息,但提供了有限的上下文信息。 在许多情况下,这种缺少的上下文信息对于理解所选控件的含义至关重要。

一个需要上下文信息的界面示例是日历或日程安排器。 如果用户在周一至周五的日期列表中选择了下午 4:00 的时间段,而辅助功能服务通告“下午 4:00”,但没有通告星期几名称、月份的日期或月份名称,则产生的反馈令人困惑。 在这种情况下,用户界面控件的上下文对于希望安排会议的用户至关重要。

自 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. 如果找到它们,则创建一个字符串以向用户报告,指示标签以及是否已选中。

如果在任何时候在遍历视图层次结构时返回了 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)引入了多种功能,让您能够访问应用 UI 中的 工具提示。 使用 getTooltipText() 读取工具提示的文本,并使用 ACTION_SHOW_TOOLTIPACTION_HIDE_TOOLTIP 指示 View 的实例显示或隐藏其工具提示。

提示文本

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

  • isShowingHintText()setShowingHintText() 方法分别指示和设置节点的当前文本内容是否代表节点的提示文本。
  • getHintText() 提供对提示文本本身的访问权限。 即使对象没有显示提示文本,调用 getHintText() 也会成功。

屏幕上文本字符的位置

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