「無障礙服務」是一種應用程式,可強化使用者介面的效能,協助失能或暫時無法與裝置完整互動的使用者。這些服務會在背景執行,並與系統通訊,檢查畫面內容及代表使用者與應用程式互動。例如螢幕閱讀器 (如 TalkBack)、切換控制功能和語音控制系統。
本指南涵蓋建構 Android 無障礙服務的基本概念。
無障礙服務生命週期
如要建立無障礙服務,您必須擴充 AccessibilityService 類別,並在應用程式的資訊清單中宣告服務。
建立服務類別
建立擴充 AccessibilityService 的類別。您必須覆寫下列方法:
onAccessibilityEvent:當系統偵測到符合服務設定的事件 (例如焦點變更或點選按鈕) 時,就會呼叫這個方法。服務會在此解讀使用者介面。onInterrupt:當系統中斷服務的回饋時 (例如使用者快速移動焦點時停止語音輸出),系統會呼叫這個方法。
package com.example.android.apis.accessibility import android.accessibilityservice.AccessibilityService import android.accessibilityservice.AccessibilityServiceInfo import android.accessibilityservice.FingerprintGestureController import android.accessibilityservice.AccessibilityButtonController import android.accessibilityservice.GestureDescription import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo import android.graphics.Path import android.os.Build import android.media.AudioManager import android.content.Context class MyAccessibilityService : AccessibilityService() { override fun onAccessibilityEvent(event: AccessibilityEvent) { // Interpret the event and provide feedback to the user } override fun onInterrupt() { // Interrupt any ongoing feedback } override fun onServiceConnected() { // Perform initialization here } }
在資訊清單中宣告
在 AndroidManifest.xml 檔案中註冊服務。您必須嚴格執行 BIND_ACCESSIBILITY_SERVICE 權限,確保只有系統能繫結至您的服務。
如要確保設定按鈕正常運作,請宣告 ServiceSettingsActivity。
<application> <service android:name=".accessibility.MyAccessibilityService" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" android:exported="true" android:label="@string/accessibility_service_label"> <intent-filter> <action android:name="android.accessibilityservice.AccessibilityService" /> </intent-filter> <meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibility_service_config" /> </service> <activity android:name=".accessibility.ServiceSettingsActivity" android:exported="true" android:label="@string/accessibility_service_settings_label" /> </application>
設定服務
在 res/xml/accessibility_service_config.xml 中建立設定檔。這個檔案定義了服務處理的事件和提供的意見回饋。請務必參照您在資訊清單中宣告的 ServiceSettingsActivity:
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:description="@string/accessibility_service_description" android:accessibilityEventTypes="typeAllMask" android:accessibilityFlags="flagDefault|flagRequestFingerprintGestures|flagRequestAccessibilityButton" android:accessibilityFeedbackType="feedbackSpoken" android:notificationTimeout="100" android:canRetrieveWindowContent="true" android:canPerformGestures="true" android:settingsActivity="com.example.android.apis.accessibility.ServiceSettingsActivity" />
設定檔包含下列主要屬性:
android:accessibilityEventTypes:要接收的事件。使用typeAllMask建立一般用途服務。android:canRetrieveWindowContent:如果服務需要檢查 UI 階層 (例如從畫面讀取文字),則必須為true。android:canPerformGestures:如要以程式輔助方式調度手勢 (例如滑動或輕觸),則必須為true。android:accessibilityFlags:合併旗標以啟用功能。指紋手勢需要flagRequestFingerprintGestures。 軟體無障礙按鈕需要flagRequestAccessibilityButton。
如需完整的設定選項清單,請參閱 AccessibilityServiceInfo。
執行階段設定
XML 設定是靜態的,但您也可以在執行階段動態修改服務設定。根據使用者偏好設定切換功能時,這項功能就派得上用場。
覆寫 onServiceConnected(),使用 setServiceInfo() 套用執行階段更新:
override fun onServiceConnected() { val info = AccessibilityServiceInfo() // Set the type of events that this service wants to listen to. info.eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED or AccessibilityEvent.TYPE_VIEW_FOCUSED // Set the type of feedback your service provides. info.feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN // Set flags at runtime. info.flags = AccessibilityServiceInfo.FLAG_DEFAULT or AccessibilityServiceInfo.FLAG_REQUEST_FINGERPRINT_GESTURES this.setServiceInfo(info) }
解讀 UI 內容
onAccessibilityEvent() 觸發時,系統會提供 AccessibilityEvent。這個事件是無障礙樹狀結構的進入點,代表畫面內容的階層式結構。
服務主要與 AccessibilityNodeInfo 物件互動,這些物件代表按鈕、清單和文字等 UI 元素。這些 UI 元素的資料會正規化為 AccessibilityNodeInfo。
以下範例說明如何擷取事件來源,並遍歷無障礙樹狀結構來尋找資訊。
override fun onAccessibilityEvent(event: AccessibilityEvent) { // Get the source node of the event val sourceNode: AccessibilityNodeInfo? = event.source if (sourceNode == null) return // Inspect properties if (sourceNode.isCheckable) { val state = if (sourceNode.isChecked) "Checked" else "Unchecked" val label = sourceNode.text ?: sourceNode.contentDescription // Provide feedback (for example, speak to the user) speakToUser("$label is $state") } // Always recycle nodes to prevent memory leaks sourceNode.recycle() } private fun speakToUser(text: String) { // Your text-to-speech implementation goes here }
代表使用者執行操作
無障礙服務可以代表使用者執行動作,例如點選按鈕或捲動清單。
如要執行動作,請在 AccessibilityNodeInfo 物件上呼叫 performAction()。
fun performClick(node: AccessibilityNodeInfo) { if (node.isClickable) { node.performAction(AccessibilityNodeInfo.ACTION_CLICK) } }
如要執行影響整個系統的全域動作 (例如按下「返回」按鈕或開啟通知欄),請使用 performGlobalAction()。
// Navigate back fun navigateBack() { performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK) }
管理重點目標
Android 有兩種不同的焦點類型:輸入焦點 (鍵盤輸入內容的去向) 和無障礙焦點 (無障礙服務檢查的內容)。
下列程式碼片段說明如何找出目前具有無障礙焦點的元素:
// Find the node that currently has accessibility focus // Note: rootInActiveWindow can be null if the window is not available val root = rootInActiveWindow if (root != null) { val focusedNode = root.findFocus(AccessibilityNodeInfo.FOCUS_ACCESSIBILITY) // Do something with focusedNode // Always recycle nodes focusedNode?.recycle() // rootInActiveWindow doesn't need to be recycled, but obtained nodes do. }
下列程式碼片段說明如何將無障礙焦點移至特定元素:
// Request that the system give focus to a given node fun focusNode(node: AccessibilityNodeInfo) { node.performAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) }
建立無障礙服務時,請尊重使用者的焦點狀態,並避免竊取焦點,除非使用者動作明確觸發。
使用手勢
服務可以將自訂手勢 (例如滑動、輕觸或多點觸控互動) 傳送至螢幕。如要這麼做,請在設定中宣告 android:canPerformGestures="true",以便使用 dispatchGesture() API。
簡單手勢
如要執行簡單手勢,請先建立 Path 物件,代表與特定手勢相關的動作。然後將 Path 包裝在 GestureDescription 中,以說明筆觸。最後,呼叫 dispatchGesture
來調度手勢。
fun swipeRight() { // Create a path for the swipe (from x=100 to x=500) val swipePath = Path() swipePath.moveTo(100f, 500f) swipePath.lineTo(500f, 500f) // Build the stroke description (0ms delay, 500ms duration) val stroke = GestureDescription.StrokeDescription(swipePath, 0, 500) // Build the gesture description val gestureBuilder = GestureDescription.Builder() gestureBuilder.addStroke(stroke) // Dispatch the gesture dispatchGesture(gestureBuilder.build(), object : AccessibilityService.GestureResultCallback() { override fun onCompleted(gestureDescription: GestureDescription?) { super.onCompleted(gestureDescription) // Gesture finished successfully } }, null) }
連續手勢
如要進行複雜的互動 (例如繪製 L 形或執行精確的多步驟拖曳),可以使用 willContinue 參數將筆劃串連在一起。
fun performLShapedGesture() { val path1 = Path().apply { moveTo(200f, 200f) lineTo(400f, 200f) } val path2 = Path().apply { moveTo(400f, 200f) lineTo(400f, 400f) } // First stroke: willContinue = true val stroke1 = GestureDescription.StrokeDescription(path1, 0, 500, true) // Second stroke: continues immediately after stroke1 val stroke2 = stroke1.continueStroke(path2, 0, 500, false) val builder = GestureDescription.Builder() builder.addStroke(stroke1) builder.addStroke(stroke2) dispatchGesture(builder.build(), null, null) }
音訊管理
建立無障礙服務 (尤其是螢幕閱讀器) 時,請使用 STREAM_ACCESSIBILITY 音訊串流。使用者可藉此獨立控制服務音量,不受系統媒體音量影響。
fun increaseAccessibilityVolume() { val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager audioManager.adjustStreamVolume( AudioManager.STREAM_ACCESSIBILITY, AudioManager.ADJUST_RAISE, 0 ) }
請務必在設定中加入 FLAG_ENABLE_ACCESSIBILITY_VOLUME 旗標,無論是 XML 或透過執行階段的 setServiceInfo 都是如此。
進階功能
指紋手勢
在搭載 Android 10 (API 級別 29) 以上版本的裝置上,服務可以擷取指紋感應器上的方向滑動動作。這項功能有助於提供替代導覽控制項。
在 onServiceConnected() 方法中新增下列邏輯:
// Import: android.os.Build // Import: android.accessibilityservice.FingerprintGestureController private var gestureController: FingerprintGestureController? = null override fun onServiceConnected() { // Check if the device is running Android 10 (Q) or higher if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { gestureController = fingerprintGestureController val callback = object : FingerprintGestureController.FingerprintGestureCallback() { override fun onGestureDetected(gesture: Int) { when (gesture) { FingerprintGestureController.FINGERPRINT_GESTURE_SWIPE_DOWN -> { // Handle swipe down } FingerprintGestureController.FINGERPRINT_GESTURE_SWIPE_UP -> { // Handle swipe up } } } } gestureController?.registerFingerprintGestureCallback(callback, null) } }
無障礙工具按鈕
如果裝置使用軟體導覽鍵,使用者可以透過導覽列中的無障礙工具按鈕叫用服務。
如要使用這項功能,請在服務設定中新增 FLAG_REQUEST_ACCESSIBILITY_BUTTON 旗標。然後將註冊邏輯新增至 onServiceConnected() 方法。
// Import: android.accessibilityservice.AccessibilityButtonController override fun onServiceConnected() { // ... existing initialization code ... val controller = accessibilityButtonController controller.registerAccessibilityButtonCallback( object : AccessibilityButtonController.AccessibilityButtonCallback() { override fun onClicked(controller: AccessibilityButtonController) { // Respond to button tap } } ) }
多語言文字轉語音
如果來源文字標記為 LocaleSpan,朗讀文字的服務可以自動切換語言。這樣一來,服務就能正確發音混合語言的內容,不必手動切換。
import android.text.Spannable import android.text.SpannableStringBuilder import android.text.style.LocaleSpan import java.util.Locale // Wrap text in LocaleSpan to indicate language val spannable = SpannableStringBuilder("Bonjour") spannable.setSpan( LocaleSpan(Locale.FRANCE), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE )
當服務處理 AccessibilityNodeInfo 時,請檢查 LocaleSpan 物件的 text 屬性,判斷正確的文字轉語音語言。
其他資源
如要進一步瞭解相關內容,請參閱下列資源: