创建自己的无障碍服务

无障碍服务能够增强用户界面的辅助功能 残障用户或暂时不能充分互动的用户 互动。例如,开车、照顾年幼的儿童、 或者参加非常嘈杂的聚会,可能需要使用额外或替代界面 反馈。

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 将其应用清单视为无障碍服务 系统。本部分介绍了 无障碍服务。

无障碍服务声明

为了让您的应用被视为无障碍服务,请添加 service 元素,而不是 application 元素中的 activity 元素。此外,请在 service 元素内添加 无障碍服务 intent 过滤器。清单还必须保护 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> 元素引用了您在 app 的资源目录: <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"
/>

有关可在 无障碍服务配置文件,请参阅以下参考文档 文档:

如需详细了解可动态设置哪些配置设置 请参阅 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> 代码添加到您的 service 声明。如果您将 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 选项。然后,您可以通过调用 该 adjustStreamVolume() 方法 AudioManager

以下代码段演示了无障碍服务如何使用 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)或更高版本,导航栏的右侧就会包含一个 “无障碍”按钮。当用户按下此按钮时,他们可以调用 启用多种无障碍功能和服务,具体取决于 屏幕上当前显示的内容。

为了让用户能够使用无障碍功能调用指定的无障碍服务 按钮,该服务需要将 FLAG_REQUEST_ACCESSIBILITY_BUTTON AccessibilityServiceInfo 对象的 android:accessibilityFlags 中的标志 属性。然后,该服务就可以使用 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. 设置 FLAG_REQUEST_FINGERPRINT_GESTURES android:accessibilityFlags 属性中的标记。
  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 设备。

监听手势

无障碍服务可以监听特定手势,并通过对 。此功能要求无障碍服务请求 触摸浏览功能激活。您的服务可请求此权限 方法是设置 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 对象。

指定一系列笔触时,您可以指定这些笔触属于 通过使用最后一个参数 willContinue(在 GestureDescription.StrokeDescription 构造函数,如以下代码段所示:

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。

使用无障碍操作

无障碍服务可以代表用户执行操作,以简化与 并提高工作效率无障碍服务能够 执行操作,并在 2012 年大幅扩展。

如需代表用户执行操作,您的无障碍服务必须注册 接收来自应用的事件并请求查看内容的权限 通过在android:canRetrieveWindowContenttrue 服务配置文件。当您的 然后它就可以检索 AccessibilityNodeInfo 使用 getSource()。 借助 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 PM”,但不会公布工作日 名称、日期或月份名称,则生成的反馈是 令人感到困惑在这种情况下,界面控件的上下文对于 想要安排会议的用户

自 2011 年以来,Android 显著扩展了 无障碍服务可通过编写 基于视图层次结构的无障碍事件视图层次结构是 包含该组件(其父级)和用户的界面组件 该组件可能包含的界面元素(其子项)。在 这样一来,Android 就可以提供有关无障碍事件的更丰富细节, 无障碍服务可为用户提供更实用的反馈。

无障碍服务可借助 AccessibilityEvent 由系统传递到服务 onAccessibilityEvent() 回调方法。此对象提供了 包括被操作对象的类型、其描述性文字以及 其他详细信息

  • AccessibilityEvent.getRecordCount()getRecord(int): 这些方法可让您检索 AccessibilityRecord 这些对象构成了 AccessibilityEvent 传递给您的 系统。这种详细程度提供了事件的更多背景信息, 触发您的无障碍服务。

  • AccessibilityRecord.getSource(): 此方法会返回一个 AccessibilityNodeInfo 对象。借助此对象,您可以 请求所创建组件的视图布局层次结构(父级和子级) 是否触发相应无障碍事件此功能可让 服务调查事件的完整上下文,包括 任何封装视图或子视图的状态。

Android 平台为 AccessibilityService 提供了查询功能 视图层次结构,收集有关生成界面组件的信息 事件及其父项和子项。为此,请设置以下代码行 :

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() 方法分别指示和设置节点的当前文本 content 表示节点的提示文本。
  • getHintText() 用于访问提示文本本身。即使某个对象未显示 提示文本,调用 getHintText() 会成功。

屏幕上的文字字符的位置

在搭载 Android 8.0(API 级别 26)及更高版本的设备上,无障碍服务 可以确定每个可见字符的边界框的屏幕坐标 放置在 TextView widget 中。服务 通过调用 refreshWithExtraData()、 传入 EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY 作为第一个参数和一个 Bundle 对象 作为第二个参数。在该方法执行时,系统会填充 Bundle 参数,带有以下 parcelable 数组: Rect 对象。每个 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