建立無障礙服務

「無障礙服務」是一種應用程式,可強化使用者介面的效能,協助失能或暫時無法與裝置完整互動的使用者。這些服務會在背景執行,並與系統通訊,檢查畫面內容及代表使用者與應用程式互動。例如螢幕閱讀器 (如 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 屬性,判斷正確的文字轉語音語言。

其他資源

如要進一步瞭解相關內容,請參閱下列資源:

指南

程式碼研究室

Views content