開發 Android 適用的無障礙服務

1. 簡介

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

什麼是無障礙服務?

無障礙服務可協助身心障礙使用者使用 Android 裝置和應用程式,這是一種長時間執行的特殊權限服務,可協助使用者處理畫面上的資訊,並讓使用者與裝置進行有意義的互動。

常見的無障礙服務範例

  • 切換控制功能:允許行動能力受限的 Android 使用者透過一或多個外接切換裝置與裝置互動。
  • Voice Access (Beta 版):可讓行動能力受限的 Android 使用者透過語音指令控制裝置。
  • Talkback:視障或失明使用者常用的螢幕閱讀器,

建構無障礙服務

雖然 Google 為 Android 使用者提供切換控制功能、Voice Access 和 Talkback 等服務,但這些服務可能無法為所有身心障礙使用者提供服務。許多身心障礙使用者的需求都不同,因此 Android 用來建立無障礙服務的 API 已開放使用,開發人員可自由打造無障礙服務,並透過 Play 商店發布。

課程內容

在本程式碼研究室中,您將使用 Accessibility API 開發簡單的服務,以便執行一些實用工作。如果您可以編寫基本 Android 應用程式,就能開發類似服務。

無障礙 API 功能強大:您要建構的服務的程式碼僅納入 4 檔案中,且使用約 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" />

使用 &lt;accessibility-service&gt; 元素定義下列中繼資料:

  1. 這項服務的意見回饋類型 (本程式碼研究室使用 feedbackGeneric 做為預設選項)。
  2. 服務的無障礙旗標 (本程式碼研究室會使用預設旗標)。
  3. 服務所需的功能:
  4. 為執行滑動操作,android:canPerformGestures 是設為 android:canPerformGestures
  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「Play」按鈕。這應該會啟動你目前使用的應用程式。

前往「設定」>無障礙功能:裝置已安裝全域動作列服務

無障礙設定畫面

按一下「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」設定 (透過頂端選單開啟「Run」,並前往「Edit Configurations」)。然後使用下拉式選單,將啟動選項從「Default Activity」變更為「Default Activity」「什麼都不要」

這個下拉式選單即可調整執行設定,透過 Android Studio 啟動服務。

現在,您應該可以使用 Android Studio 啟動服務。

在畫面頂端的選單列中,按下「播放」圖示 用來啟動服務的 Android Studio「Play」按鈕。然後,前往設定 >無障礙功能並開啟通用動作列服務。

您應該會看到構成服務 UI 的四個按鈕,疊加在畫面上顯示的內容上。

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();
}

在畫面頂端的選單列中,按下「播放」圖示 用來啟動服務的 Android Studio「Play」按鈕。然後,前往設定 >無障礙功能,並啟動全域動作列服務

按下電源按鈕,顯示電源對話方塊。

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();
}

在畫面頂端的選單列中,按下「播放」圖示 用來啟動服務的 Android Studio「Play」按鈕。接著,依序前往「設定」>「存取無障礙功能,並啟動全域動作列服務。

按下音量按鈕可調整音量。

假想使用者無法連線至裝置側邊的音量控制項,現在可改用全域動作列服務調整 (增加) 音量。

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

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

在畫面頂端的選單列中,按下「播放」圖示 用來啟動服務的 Android Studio「Play」按鈕。接著,依序前往「設定」>「存取無障礙功能,並啟動全域動作列服務。

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

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,可代表使用者執行手勢。程式碼會使用 GestureDescription 物件指定要執行手勢的路徑 (本程式碼研究室中會使用硬式編碼值),然後使用 AccessibilityService dispatchGesture() 方法代替使用者分派滑動手勢。

現在將 configureSwipeButton() 加入 onServiceConnected()

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

在畫面頂端的選單列中,按下「播放」圖示 用來啟動服務的 Android Studio「Play」按鈕。接著,依序前往「設定」>「存取無障礙功能,並啟動全域動作列服務。

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

9. 摘要

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

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

  1. 將動作列設為可移動 (目前只是位於螢幕頂端)。
  2. 允許使用者同時調高及調低音量。
  3. 允許使用者左右滑動。
  4. 新增動作列可回應的其他手勢。

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

  • 支援多個視窗。
  • 支援 AccessibilityEvent。UI 變更時,無障礙服務會使用 AccessibilityEvent 物件來通知這些變更。這樣一來,服務就能根據 UI 變更適時回應。
  • 控制放大功能。

本程式碼研究室可協助您開始編寫無障礙服務。如果您知道有使用者遇到特定無障礙問題,可以立即建立服務來協助使用者。