开发适用于 Android 的无障碍服务

1. 简介

无障碍服务是 Android 框架的一项功能,旨在代表安装在 Android 设备上的应用向用户提供替代导航反馈。无障碍服务可以代表应用与用户进行通信,例如在用户将光标悬停在屏幕的重要区域时,将文字转换为语音或提供触感反馈。此 Codelab 将介绍如何创建非常简单的无障碍服务。

什么是无障碍服务?

无障碍服务可帮助残障用户使用 Android 设备和应用。它是一项长时间运行的特权服务,可帮助用户处理屏幕上的信息,并让用户与设备进行有意义的互动。

常见无障碍服务示例

  • 开关控制:可让有行动障碍的 Android 用户使用一个或多个开关与设备互动。
  • 语音操控(Beta 版):可让行动不便的 Android 用户通过语音指令控制设备。
  • Talkback:一种屏幕阅读器,通常供视力损害或盲人用户使用。

构建无障碍服务

虽然 Google 为 Android 用户提供了开关控制、语音操控和 TalkBack 等服务,但这些服务无法满足所有残疾用户的需求。由于许多残障人士有独特的需求,因此 Android 用于创建无障碍服务的 API 是开放的,开发者可以自由创建无障碍服务并通过 Play 商店分发这些服务。

构建内容

在此 Codelab 中,您将开发一个使用无障碍 API 执行一些实用操作的简单服务。如果您能编写基本的 Android 应用,就可以开发类似的服务。

无障碍功能 API 非常强大:您将构建的服务的代码仅包含在四个文件中,并且使用了大约 200 行代码!

最终用户

您将为具有以下特征的假设用户构建服务:

  • 用户难以触及设备侧面的按钮。
  • 用户难以滚动或滑动。

服务详情

您的服务会在屏幕上叠加一个全局操作栏。用户可以点按此栏中的按钮来执行以下操作:

  1. 在不触碰手机侧面实际电源按钮的情况下关闭设备。
  2. 无需触摸手机侧面的音量按钮即可调节音量。
  3. 执行滚动操作,但不实际滚动。
  4. 执行滑动操作,而无需使用滑动手势。

所需条件

本 Codelab 假定您将使用以下内容:

  1. 一台运行 Android Studio 的计算机。
  2. 用于执行简单 shell 命令的终端。
  3. 一部运行 Android 7.0(Nougat)的设备,该设备已连接到您将用于开发的计算机。

让我们开始吧!

2. 准备工作

使用终端创建一个您将要使用的目录。切换到此目录。

下载代码

您可以克隆包含此 Codelab 代码的代码库:

git clone https://github.com/android/codelab-android-accessibility.git

该代码库包含多个 Android Studio 项目。使用 Android Studio 打开 GlobalActionBarService

点击 Studio 图标启动 Android Studio:

用于启动 Android Studio 的徽标。

选择 Import project (Eclipse ADT, Gradle, etc.) 选项:

Android Studio 的欢迎界面。

前往您克隆源代码的位置,然后选择 GlobalActionBarService

然后,使用终端切换到根目录。

3. 了解起始代码

探索您打开的项目。

我们已为您创建了无障碍服务的精简框架。您在此 Codelab 中编写的所有代码都仅限于以下四个文件:

  1. app/src/main/AndroidManifest.xml
  2. app/src/main/res/layout/action_bar.xml
  3. app/src/main/res/xml/global_action_bar_service.xml
  4. app/src/main/java/com/example/android/globalactionbarservice/GlobalActionBarService.java

下面将逐步介绍每个文件的内容。

AndroidManifest.xml

无障碍服务的相关信息在清单中声明:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   package="com.example.android.globalactionbarservice">

   <application>
       <service
           android:name=".GlobalActionBarService"
           android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
           android:exported="true">
           <intent-filter>
               <action android:name="android.accessibilityservice.AccessibilityService" />
           </intent-filter>
           <meta-data
               android:name="android.accessibilityservice"
               android:resource="@xml/global_action_bar_service" />
       </service>
   </application>
</manifest>

AndroidManifest.xml 中声明了以下三个必需项:

  1. 绑定到无障碍服务的权限:
<service
    ...
    android:permission = "android.permission.BIND_ACCESSIBILITY_SERVICE">
    ...             
</service>
  1. AccessibilityService intent:
<intent-filter>
   <action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
  1. 包含您要创建的服务的元数据的文件位置:
<meta-data
       ...
       android:resource="@xml/global_action_bar_service" />
</service>

global_action_bar_service.xml

此文件包含服务的元数据。

<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
   android:accessibilityFeedbackType="feedbackGeneric"
   android:accessibilityFlags="flagDefault"
   android:canPerformGestures="true"
   android:canRetrieveWindowContent="true" />

使用 <accessibility-service> 元素定义了以下元数据:

  1. 此服务的反馈类型(此 Codelab 使用 feedbackGeneric,这是一个不错的默认值)。
  2. 服务的无障碍标志(此 Codelab 使用默认标志)。
  3. 服务所需的功能:
  4. 为了实现滑动操作,android:canPerformGestures 设置为 true
  5. 为了检索窗口内容,android:canRetrieveWindowContent 设置为 true

GlobalActionBarService.java

无障碍服务的大部分代码都位于 GlobalActionBarService.java 中。最初,该文件包含无障碍服务的绝对最低限度代码:

  1. 扩展 AccessibilityService 的类。
  2. 一些必需的被替换方法(在此 Codelab 中留空)。
public class GlobalActionBarService extends AccessibilityService {

   @Override
   public void onAccessibilityEvent(AccessibilityEvent event) {

   }

   @Override
   public void onInterrupt() {

   }
}

您将在本 Codelab 期间向此文件添加代码。

action_bar.xml

该服务公开了一个包含四个按钮的界面,而 action_bar.xml 布局文件包含用于显示这些按钮的标记:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:orientation="horizontal"
   android:layout_width="match_parent"
   android:layout_height="wrap_content">
</LinearLayout>

此文件目前包含一个空的 LinearLayout。在此 Codelab 中,您将为按钮添加标记。

启动应用

请确保已将设备连接到计算机。按屏幕顶部菜单栏中的绿色 Play 按钮 用于启动服务的 Android Studio“播放”按钮,这应该会启动您正在处理的应用。

前往设置 > 无障碍功能。设备上已安装 Global Action Bar Service

无障碍设置界面

点击 Global Action Bar Service 并将其开启。您应该会看到以下权限对话框:

无障碍服务权限对话框。

无障碍服务会请求权限来观察用户操作、检索窗口内容并代表用户执行手势!使用第三方无障碍服务时,请务必确保您信任相应来源

运行该服务不会执行任何操作,因为我们尚未添加任何功能。让我们开始吧。

4. 创建按钮

打开 res/layout 中的 action_bar.xml。在当前为空的 LinearLayout 中添加标记:

<LinearLayout ...>
    <Button
        android:id="@+id/power"
        android:text="@string/power"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <Button
        android:id="@+id/volume_up"
        android:text="@string/volume"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <Button
        android:id="@+id/scroll"
        android:text="@string/scroll"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <Button
        android:id="@+id/swipe"
        android:text="@string/swipe"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
</LinearLayout>

这会创建用户将按下的按钮,以触发设备上的操作。

打开 GlobalActionBarService.java 并添加一个变量来存储操作栏的布局:

public class GlobalActionBarService extends AccessibilityService {
    FrameLayout mLayout;
    ...
}

现在,添加 onServiceStarted() 方法:

public class GlobalActionBarService extends AccessibilityService {
   FrameLayout mLayout;

   @Override
   protected void onServiceConnected() {
       // Create an overlay and display the action bar
       WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
       mLayout = new FrameLayout(this);
       WindowManager.LayoutParams lp = new WindowManager.LayoutParams();
       lp.type = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY;
       lp.format = PixelFormat.TRANSLUCENT;
       lp.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
       lp.width = WindowManager.LayoutParams.WRAP_CONTENT;
       lp.height = WindowManager.LayoutParams.WRAP_CONTENT;
       lp.gravity = Gravity.TOP;
       LayoutInflater inflater = LayoutInflater.from(this);
       inflater.inflate(R.layout.action_bar, mLayout);
       wm.addView(mLayout, lp);
   }
}

该代码会扩充布局,并在屏幕顶部附近添加操作栏。

当服务连接时,系统会运行 onServiceConnected() 方法。此时,无障碍服务已获得正常运行所需的所有权限。您将在此处使用的关键权限是 WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY 权限。此权限可让您直接在屏幕上现有内容之上绘制内容,而无需经历复杂的权限流程。

无障碍服务生命周期

无障碍服务的生命周期完全由系统管理,并遵循既定的服务生命周期。

  • 当用户在设备设置中明确开启无障碍服务时,该服务会启动。
  • 系统绑定到服务后,会调用 onServiceConnected()。想要执行绑定后设置的服务可以替换此方法。
  • 无障碍服务会在以下情况下停止:用户在设备设置中将其关闭,或者该服务调用 disableSelf()

运行服务

在您使用 Android Studio 启动服务之前,需要确保运行设置已正确配置。

修改您的运行配置(使用顶部菜单中的“运行”,然后前往“修改配置”)。然后,使用下拉菜单将“启动选项”从“默认 activity”更改为“无”。

下拉以配置运行设置,以便使用 Android Studio 启动服务。

现在,您应该可以使用 Android Studio 启动服务了。

按屏幕顶部菜单栏中的绿色 Play 按钮 用于启动服务的 Android Studio“播放”按钮,然后,依次前往设置 > 无障碍功能,然后开启全局操作栏服务

您应该会看到四个按钮,它们构成了服务界面,叠加在屏幕上显示的内容之上。

overlay.png

现在,您将为这四个按钮添加功能,以便用户可以通过触摸它们来执行有用的操作。

5. 配置电源按钮

configurePowerButton() 方法添加到 GlobalActionBarService.java

private void configurePowerButton() {
   Button powerButton = (Button) mLayout.findViewById(R.id.power);
   powerButton.setOnClickListener(new View.OnClickListener() {
       @Override
       public void onClick(View view) {
           performGlobalAction(GLOBAL_ACTION_POWER_DIALOG);
       }
   });
}

为了访问电源按钮菜单,configurePowerButton() 使用了 AccessibilityService 提供的 performGlobalAction() 方法。您刚刚添加的代码很简单:点击按钮会触发 onClickListener()。这会调用 performGlobalAction(GLOBAL_ACTION_POWER_DIALOG) 并向用户显示电源对话框。

请注意,全局操作不与任何视图相关联。按“返回”按钮、“主屏幕”按钮、“最近使用的应用”按钮是全局操作的其他示例。

现在,将 configurePowerButton() 添加到 onServiceConnected() 方法的末尾:

@Override
protected void onServiceConnected() {
   ...
   configurePowerButton();
}

按屏幕顶部菜单栏中的绿色 Play 按钮 用于启动服务的 Android Studio“播放”按钮,然后,依次前往设置 > 无障碍功能,然后启动全局操作栏服务

按电源按钮以显示电源对话框。

6. 配置音量按钮

configureVolumeButton() 方法添加到 GlobalActionBarService.java

private void configureVolumeButton() {
   Button volumeUpButton = (Button) mLayout.findViewById(R.id.volume_up);
   volumeUpButton.setOnClickListener(new View.OnClickListener() {
       @Override
       public void onClick(View view) {
           AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
           audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
                   AudioManager.ADJUST_RAISE, AudioManager.FLAG_SHOW_UI);
       }
   });
}

configureVolumeButton() 方法添加了一个 onClickListener(),当用户按下音量按钮时,该监听器会被触发。在此监听器内,configureVolumeButton() 使用 AudioManager 调整音频流音量。

请注意,任何人都可以控制音量(您无需是无障碍服务即可执行此操作)。

现在,将 configureVolumeButton() 添加到 onServiceConnected() 方法的末尾:

@Override
protected void onServiceConnected() {
   ...

   configureVolumeButton();
}

按屏幕顶部菜单栏中的绿色 Play 按钮 用于启动服务的 Android Studio“播放”按钮,然后,前往“设置”>“无障碍功能”,并启动全局操作栏服务

按音量按钮可调节音量。

假设用户无法触及设备侧面的音量控件,现在可以使用全局操作栏服务来更改(调高)音量。

7. 配置滚动按钮

此部分涉及编写两种方法的代码。第一个方法用于查找可滚动节点,第二个方法用于代表用户执行滚动操作。

findScrollableNode 方法添加到 GlobalActionBarService.java

private AccessibilityNodeInfo findScrollableNode(AccessibilityNodeInfo root) {
   Deque<AccessibilityNodeInfo> deque = new ArrayDeque<>();
   deque.add(root);
   while (!deque.isEmpty()) {
       AccessibilityNodeInfo node = deque.removeFirst();
       if (node.getActionList().contains(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD)) {
           return node;
       }
       for (int i = 0; i < node.getChildCount(); i++) {
           deque.addLast(node.getChild(i));
       }
   }
   return null;
}

无障碍服务无法访问屏幕上的实际视图。相反,它会看到屏幕上内容的反映,以由 AccessibilityNodeInfo 对象组成的树的形式呈现。这些对象包含有关其所代表的视图的信息(视图的位置、与视图关联的任何文本、为无障碍功能添加的元数据、视图支持的操作等)。findScrollableNode() 方法从根节点开始,对此树执行广度优先遍历。如果找到可滚动的节点(即支持 AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD 操作)的节点),则返回该节点,否则返回 null。

现在,将 configureScrollButton() 方法添加到 GlobalActionBarService.java

private void configureScrollButton() {
   Button scrollButton = (Button) mLayout.findViewById(R.id.scroll);
   scrollButton.setOnClickListener(new View.OnClickListener() {
       @Override
       public void onClick(View view) {
           AccessibilityNodeInfo scrollable = findScrollableNode(getRootInActiveWindow());
           if (scrollable != null) {
               scrollable.performAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD.getId());
           }
       }
   });
}

此方法会创建一个 onClickListener(),当点击滚动按钮时,该监听器会触发。它会尝试查找可滚动节点,如果成功找到,则执行滚动操作。

现在,将 configureScrollButton() 添加到 onServiceConnected()

@Override
protected void onServiceConnected() {
   ...

   configureScrollButton();
}

按屏幕顶部菜单栏中的绿色 Play 按钮 用于启动服务的 Android Studio“播放”按钮,然后,前往“设置”>“无障碍功能”,并启动全局操作栏服务

按返回按钮,前往设置 > 无障碍功能。无障碍设置 activity 中的项可滚动,触摸“滚动”按钮会执行滚动操作。假设用户无法轻松执行滚动操作,现在可以使用“滚动”按钮滚动浏览商品列表。

8. 配置滑动按钮

configureSwipeButton() 方法添加到 GlobalActionBarService.java

private void configureSwipeButton() {
   Button swipeButton = (Button) mLayout.findViewById(R.id.swipe);
   swipeButton.setOnClickListener(new View.OnClickListener() {
       @Override
       public void onClick(View view) {
           Path swipePath = new Path();
           swipePath.moveTo(1000, 1000);
           swipePath.lineTo(100, 1000);
           GestureDescription.Builder gestureBuilder = new GestureDescription.Builder();
           gestureBuilder.addStroke(new GestureDescription.StrokeDescription(swipePath, 0, 500));
           dispatchGesture(gestureBuilder.build(), null, null);
       }
   });
}

configureSwipeButton() 方法使用 N 中新增的 API 来代表用户执行手势。该代码使用 GestureDescription 对象指定要执行的手势的路径(此 Codelab 中使用了硬编码的值),然后使用 AccessibilityServicedispatchGesture() 方法代表用户调度滑动操作。

现在,将 configureSwipeButton() 添加到 onServiceConnected()

@Override
protected void onServiceConnected() {
   ...
   configureSwipeButton();
}

按屏幕顶部菜单栏中的绿色 Play 按钮 用于启动服务的 Android Studio“播放”按钮,然后,前往“设置”>“无障碍功能”,并启动全局操作栏服务

测试滑动功能的最简单方法是打开手机上安装的 Google 地图应用。地图加载完毕后,触摸“滑动”按钮会将屏幕向右滑动。

9. 总结

恭喜!您已构建了一个简单实用的无障碍服务。

您可以通过多种方式扩展此服务。例如:

  1. 使操作栏可移动(目前它只是位于屏幕顶部)。
  2. 允许用户调高和调低音量。
  3. 允许用户向左和向右滑动。
  4. 添加了对操作栏可响应的其他手势的支持。

此 Codelab 仅涵盖无障碍功能 API 提供的一小部分功能。该 API 还涵盖以下内容(部分列表):

  • 支持多个窗口。
  • 支持 AccessibilityEvent。当界面发生变化时,系统会使用 AccessibilityEvent 对象将这些变化通知给无障碍服务。然后,服务可以根据界面变化做出适当的响应。
  • 能够控制放大倍数。

本 Codelab 将帮助您开始编写无障碍服务。如果您知道有用户存在特定的无障碍问题,并且您想解决这些问题,现在可以构建一项服务来帮助该用户。