開發 Android 適用的無障礙服務

1. 簡介

無障礙服務是 Android 架構的一項功能,可代表 Android 裝置上安裝的應用程式,向使用者提供替代導覽回饋。無障礙服務可代表應用程式與使用者溝通,例如在使用者將游標懸停在螢幕的重要區域時,將文字轉換為語音或提供觸覺回饋。本程式碼研究室會說明如何建立非常簡單的無障礙服務。

什麼是無障礙服務?

無障礙服務可協助身心障礙使用者操作 Android 裝置和應用程式。這項長期執行的特殊權限服務可協助使用者處理螢幕上的資訊,並與裝置進行有意義的互動。

常見的無障礙服務示例

  • 切換控制功能:行動不便的 Android 使用者可以透過一或多個外接切換裝置操作裝置。
  • Voice Access (Beta 版):讓行動不便的 Android 使用者透過語音指令控制裝置。
  • TalkBack:視障或失明使用者常用的螢幕閱讀器。

建構無障礙服務

Google 為 Android 使用者提供切換控制、語音存取和 TalkBack 等服務,但這些服務無法滿足所有身心障礙使用者的需求。由於許多身心障礙使用者有獨特需求,Android 建立無障礙服務的 API 是開放的,開發人員可以自由建立無障礙服務,並透過 Play 商店發布。

建構項目

在本程式碼研究室中,您將開發簡單的服務,使用無障礙 API 執行幾項實用功能。如果您會編寫基本的 Android 應用程式,就能開發類似的服務。

無障礙 API 功能強大:您要建構的服務程式碼只包含四個檔案,且使用約 200 行程式碼!

使用者

您將為假設使用者建構服務,該使用者具有下列特徵:

  • 使用者難以觸及裝置側邊按鈕。
  • 使用者難以捲動或滑動畫面。

服務詳細資料

服務會在畫面上疊加顯示全域動作列。使用者可以輕觸這個工具列上的按鈕,執行下列動作:

  1. 在不按壓手機側邊電源鍵的情況下關閉裝置。
  2. 不必觸碰手機側邊的音量鍵,即可調整音量。
  3. 執行捲動動作,但不會實際捲動。
  4. 不必使用滑動手勢即可滑動。

軟硬體需求

本程式碼研究室假設您使用下列項目:

  1. 執行 Android Studio 的電腦。
  2. 用於執行簡單殼層指令的終端機。
  3. 搭載 Android 7.0 (Nougat) 的裝置,並連上您要用於開發的電腦。

立即開始!

2. 開始設定

使用終端機建立工作目錄。變更為這個目錄。

下載程式碼

您可以複製包含本程式碼研究室程式碼的存放區:

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. 瞭解範例程式碼

瀏覽開啟的專案。

系統已為您建立無障礙服務的基礎架構。在本程式碼研究室中,您編寫的所有程式碼都僅限於下列四個檔案:

  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-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. 這項服務的意見回饋類型 (本程式碼研究室使用 feedbackGeneric,這是合適的預設值)。
  2. 服務的無障礙旗標 (本程式碼研究室使用預設旗標)。
  3. 服務所需的功能:
  4. 為了執行滑動操作,android:canPerformGestures 會設為 true
  5. 如要擷取視窗內容,請將 android:canRetrieveWindowContent 設為 true

GlobalActionBarService.java

無障礙服務的大部分程式碼都位於 GlobalActionBarService.java 中。一開始,檔案會包含無障礙服務的絕對最低程式碼:

  1. 擴充 AccessibilityService 的類別。
  2. 幾個必要覆寫方法 (在本程式碼研究室中留空)。
public class GlobalActionBarService extends AccessibilityService {

   @Override
   public void onAccessibilityEvent(AccessibilityEvent event) {

   }

   @Override
   public void onInterrupt() {

   }
}

您會在程式碼研究室中將程式碼新增至這個檔案。

action_bar.xml

這項服務會顯示含有四個按鈕的 UI,而 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。在本程式碼研究室中,您將新增按鈕的標記。

啟動應用程式

確認裝置已連上電腦。按下畫面頂端選單列的綠色「播放」圖示 啟動服務時使用的 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 啟動服務,請先確認執行設定正確無誤。

編輯執行設定 (使用頂端選單中的「Run」,然後前往「Edit Configurations」。然後使用下拉式選單,將「啟動選項」從「預設活動」變更為「無」。

下拉選單可設定執行設定,以便使用 Android Studio 啟動服務。

現在您應該可以透過 Android Studio 啟動服務。

按下畫面頂端選單列的綠色「播放」圖示 啟動服務時使用的 Android Studio「播放」按鈕。接著依序前往「設定」>「無障礙設定」,然後開啟「全域動作列服務」

畫面上顯示的內容上方,應該會疊加顯示四個按鈕,組成服務 UI。

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() 會使用 performGlobalAction() 方法,此方法由 AccessibilityService 提供。您剛才新增的程式碼很簡單:點選按鈕會觸發 onClickListener()。這會呼叫 performGlobalAction(GLOBAL_ACTION_POWER_DIALOG),並向使用者顯示電源對話方塊。

請注意,全域動作不會與任何檢視畫面繫結。按下「返回」按鈕、「主畫面」按鈕和「最近使用的應用程式」按鈕,也是全域動作的例子。

現在,請在 onServiceConnected() 方法的結尾新增 configurePowerButton()

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

按下畫面頂端選單列的綠色「播放」圖示 啟動服務時使用的 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 調整串流音量。

請注意,任何人都可以控制音量 (不需是無障礙服務)。

現在,請在 onServiceConnected() 方法的結尾新增 configureVolumeButton()

@Override
protected void onServiceConnected() {
   ...

   configureVolumeButton();
}

按下畫面頂端選單列的綠色「播放」圖示 啟動服務時使用的 Android Studio「播放」按鈕。接著依序前往「設定」>「無障礙功能」,然後啟動「全域動作列服務」

按下音量鍵即可調整音量。

假設使用者無法觸及裝置側邊的音量控制項,現在可以透過 Global Action Bar Service 變更 (調高) 音量。

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 動作) 的節點),就會傳回該節點,否則會傳回空值。

現在,請將 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();
}

按下畫面頂端選單列的綠色「播放」圖示 啟動服務時使用的 Android Studio「播放」按鈕。接著依序前往「設定」>「無障礙功能」,然後啟動「全域動作列服務」

按下返回鍵,依序前往「設定」>「無障礙設定」。無障礙設定活動中的項目可捲動,輕觸「捲動」按鈕即可執行捲動動作。假設使用者無法輕鬆執行捲動動作,現在可以使用「捲動」按鈕捲動項目清單。

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 物件指定要執行的手勢路徑 (本程式碼研究室使用硬式編碼值),然後代表使用者透過 AccessibilityServicedispatchGesture() 方法調度滑動手勢。

現在將 configureSwipeButton() 新增至 onServiceConnected()

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

按下畫面頂端選單列的綠色「播放」圖示 啟動服務時使用的 Android Studio「播放」按鈕。接著依序前往「設定」>「無障礙功能」,然後啟動「全域動作列服務」

如要測試滑動功能,最簡單的方法是開啟手機上安裝的「地圖」應用程式。地圖載入後,輕觸「滑動」按鈕即可向右滑動畫面。

9. 摘要

恭喜!您已建構簡單實用的無障礙服務。

您可以透過多種方式擴充這項服務。例如:

  1. 讓動作列可移動 (目前只會顯示在畫面頂端)。
  2. 允許使用者調高及調低音量。
  3. 允許使用者向左和向右滑動。
  4. 新增動作列可回應的其他手勢。

本程式碼研究室僅涵蓋無障礙 API 提供的一小部分功能。這項 API 也涵蓋下列項目 (部分清單):

  • 支援多個視窗。
  • 支援 AccessibilityEvent。當 UI 變更時,無障礙服務會使用 AccessibilityEvent 物件收到這些變更的通知。服務隨後可適當回應 UI 變更。
  • 可控制放大倍率。

本程式碼研究室會協助您開始編寫無障礙服務。如果您知道有特定無障礙問題的使用者需要協助,現在可以建構服務來幫助他們。