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

1. 简介

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

什么是无障碍服务?

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

常见无障碍服务示例

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

构建无障碍服务

虽然 Google 为 Android 用户提供了开关控制、Voice Access 和 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" />

使用 &lt;accessibility-service&gt; 元素定义了以下元数据:

  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“播放”按钮,这应该会启动您正在使用的应用。

前往设置 >无障碍功能。设备上已安装全局操作栏服务

“无障碍设置”屏幕

点击全局操作栏服务并将其开启。您应该会看到以下权限对话框:

无障碍服务权限对话框。

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

运行该服务没有什么用处,因为我们尚未添加任何功能。我们开始吧。

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() 时,无障碍服务会停止。

运行服务

您需要先确保正确配置“Run”设置,然后才能使用 Android Studio 启动该服务。

修改您的运行配置(使用顶部菜单中的“Run”,然后转到“Edit Configurations”)。然后,在下拉菜单中将“启动选项”从“默认活动”更改为“默认活动”设为“Nothing”。

用于配置运行设置以使用 Android Studio 启动服务的下拉菜单。

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

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

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

overlay.png

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

5. 配置电源按钮

configurePowerButton() 方法添加到 configurePowerButton()

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() 方法添加到 configureVolumeButton()

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 方法添加到 findScrollableNode

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() 方法添加到 configureScrollButton()

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() 方法添加到 configureSwipeButton()

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,该 API 可代表用户执行手势。该代码会使用 GestureDescription 对象来指定要执行的手势的路径(此 Codelab 中使用了硬编码值),然后使用 AccessibilityService dispatchGesture() 方法代表用户分派滑动手势。

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

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

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

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

9. 总结

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

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

  1. 将操作栏设置为可移动(暂时只位于屏幕顶部)。
  2. 允许用户调高和调低音量。
  3. 允许用户左右滑动。
  4. 添加对操作栏可以响应的其他手势的支持。

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

  • 支持多个窗口。
  • 支持 AccessibilityEvent。当界面发生更改时,系统会使用 AccessibilityEvent 对象向无障碍服务提供有关这些更改的通知。然后,该服务就可以对界面更改进行适当响应。
  • 能够控制放大。

在此 Codelab 中,您将开始编写无障碍服务。如果您知道某用户存在特定无障碍功能问题并且想要解决相关问题,现在可以构建一项服务来帮助该用户。