1. 簡介
無障礙服務是 Android 架構的一項功能,旨在代表 Android 裝置上安裝的應用程式,為使用者提供其他瀏覽方式的回饋。無障礙服務可代表應用程式與使用者溝通,例如將文字轉換成語音,或在使用者將滑鼠遊標懸停在螢幕上的重要區域時提供觸覺回饋。本程式碼研究室將說明如何建立非常簡單的無障礙服務。
什麼是無障礙服務?
無障礙服務可協助身心障礙使用者使用 Android 裝置和應用程式,這是一種長時間執行的特殊權限服務,可協助使用者處理畫面上的資訊,並讓使用者與裝置進行有意義的互動。
常見的無障礙服務範例
- 切換控制功能:允許行動能力受限的 Android 使用者透過一或多個外接切換裝置與裝置互動。
- Voice Access (Beta 版):可讓行動能力受限的 Android 使用者透過語音指令控制裝置。
- Talkback:視障或失明使用者常用的螢幕閱讀器,
建構無障礙服務
雖然 Google 為 Android 使用者提供切換控制功能、Voice Access 和 Talkback 等服務,但這些服務可能無法為所有身心障礙使用者提供服務。許多身心障礙使用者的需求都不同,因此 Android 用來建立無障礙服務的 API 已開放使用,開發人員可自由打造無障礙服務,並透過 Play 商店發布。
課程內容
在本程式碼研究室中,您將使用 Accessibility API 開發簡單的服務,以便執行一些實用工作。如果您可以編寫基本 Android 應用程式,就能開發類似服務。
無障礙 API 功能強大:您要建構的服務的程式碼僅納入 4 檔案中,且使用約 200 行程式碼!
使用者
您將建構一個具備下列特性的假想使用者服務:
- 使用者無法輕觸裝置上的側邊按鈕。
- 使用者無法順利捲動或滑動。
服務詳細資料
您的服務會在螢幕上重疊顯示全域動作列。使用者可以輕觸此列上的按鈕,執行下列動作:
- 請將裝置關機,而且無法碰到手機側邊的電源按鈕。
- 調整音量時,不必輕觸手機側邊的音量按鈕。
- 不需實際捲動即可執行捲動操作。
- 不必使用滑動手勢,即可進行滑動。
軟硬體需求
本程式碼研究室假設您將使用下列資料:
- 執行 Android Studio 的電腦。
- 執行簡單殼層指令的終端機。
- 搭載 Android 7.0 (Nougat) 的裝置,已連線至您將用於開發的電腦。
立即開始!
2. 開始設定
使用終端機,建立您要使用的目錄。變更為這個目錄。
下載程式碼
您可以複製包含本程式碼研究室程式碼的存放區:
git clone https://github.com/android/codelab-android-accessibility.git
存放區含有數個 Android Studio 專案。使用 Android Studio 開啟 GlobalActionBarService。
按一下 Studio 圖示以啟動 Android Studio:
選取「Import project (Eclipse ADT, Gradle, etc.)」選項:
前往您複製來源的位置,然後選取「GlobalActionBarService」。
然後,使用終端機切換到根目錄。
3. 瞭解範例程式碼
探索已開啟的專案。
系統已為您建立無障礙服務的基礎架構。您在本程式碼研究室中編寫的所有程式碼只能使用以下四個檔案:
- app/src/main/AndroidManifest.xml
- app/src/main/res/layout/action_bar.xml
- app/src/main/res/xml/global_action_bar_service.xml
- 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 中宣告下列三個必要項目:
- 以下權限可以繫結至無障礙服務:
<service
...
android:permission = "android.permission.BIND_ACCESSIBILITY_SERVICE">
...
</service>
- AccessibilityService 意圖:
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
- 檔案所在位置 (包含所建立服務的中繼資料):
<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> 元素定義下列中繼資料:
- 這項服務的意見回饋類型 (本程式碼研究室使用 feedbackGeneric 做為預設選項)。
- 服務的無障礙旗標 (本程式碼研究室會使用預設旗標)。
- 服務所需的功能:
- 為執行滑動操作,android:canPerformGestures 是設為 android:canPerformGestures。
- 為了擷取視窗內容,android:canRetrieveWindowContent 會設為 true。
GlobalActionBarService.java
無障礙服務大部分的程式碼都位於 GlobalActionBarService.java。初始檔案包含無障礙服務的絕對最低限度程式碼:
- 擴充 AccessibilityService 的類別。
- 幾個必要的覆寫方法 (在本程式碼研究室中留空)。
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。在本程式碼研究室中,您將為按鈕新增標記。
啟動應用程式
確認裝置已連接至電腦。在畫面頂端的選單列中,按下「播放」圖示 。這應該會啟動你目前使用的應用程式。
前往「設定」>無障礙功能:裝置已安裝全域動作列服務。
按一下「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 啟動服務。
在畫面頂端的選單列中,按下「播放」圖示 。然後,前往設定 >無障礙功能並開啟通用動作列服務。
您應該會看到構成服務 UI 的四個按鈕,疊加在畫面上顯示的內容上。
現在您將為四個按鈕新增功能,讓使用者可透過輕觸按鈕執行實用動作。
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();
}
在畫面頂端的選單列中,按下「播放」圖示 。然後,前往設定 >無障礙功能,並啟動全域動作列服務。
按下電源按鈕,顯示電源對話方塊。
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();
}
在畫面頂端的選單列中,按下「播放」圖示 。接著,依序前往「設定」>「存取無障礙功能,並啟動全域動作列服務。
按下音量按鈕可調整音量。
假想使用者無法連線至裝置側邊的音量控制項,現在可改用全域動作列服務調整 (增加) 音量。
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();
}
在畫面頂端的選單列中,按下「播放」圖示 。接著,依序前往「設定」>「存取無障礙功能,並啟動全域動作列服務。
按下返回按鈕,前往「設定」>「設定」無障礙功能。無障礙設定活動中的項目可捲動,輕觸 [捲動] 按鈕則執行捲動動作。假設使用者無法輕鬆執行捲動動作,現在可以使用捲動按鈕捲動項目清單。
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();
}
在畫面頂端的選單列中,按下「播放」圖示 。接著,依序前往「設定」>「存取無障礙功能,並啟動全域動作列服務。
如要測試滑動功能,最簡單的方法是開啟手機上安裝的地圖應用程式。地圖載入後,輕觸滑動按鈕,讓畫面向右滑動。
9. 摘要
恭喜!你已建構簡單實用的無障礙服務。
您可以透過多種方式擴充此服務。例如:
- 將動作列設為可移動 (目前只是位於螢幕頂端)。
- 允許使用者同時調高及調低音量。
- 允許使用者左右滑動。
- 新增動作列可回應的其他手勢。
本程式碼研究室僅涵蓋無障礙 API 提供的一小部分功能。這個 API 也包含下列內容 (部分清單):
- 支援多個視窗。
- 支援 AccessibilityEvent。UI 變更時,無障礙服務會使用 AccessibilityEvent 物件來通知這些變更。這樣一來,服務就能根據 UI 變更適時回應。
- 控制放大功能。
本程式碼研究室可協助您開始編寫無障礙服務。如果您知道有使用者遇到特定無障礙問題,可以立即建立服務來協助使用者。