建立專屬的無障礙服務

「無障礙服務」是一種應用程式,可強化使用者介面的效能,協助失能或暫時無法與裝置完整互動的使用者。舉例來說,正在開車、照顧幼童,或參加極度喧囂派對的使用者,就可能需要其他介面或替代介面的回饋。

Android 提供標準的無障礙功能 (包括 TalkBack),開發人員可自行建立並發布各項服務。本文說明建構無障礙服務的基本概念。

無障礙服務可與一般應用程式搭配使用,或是以獨立 Android 專案的形式建立服務。兩種方式的服務建立步驟都相同。

建立無障礙服務

請在您的專案中建立擴充 AccessibilityService 的類別:

Kotlin

package com.example.android.apis.accessibility

import android.accessibilityservice.AccessibilityService
import android.view.accessibility.AccessibilityEvent

class MyAccessibilityService : AccessibilityService() {
...
    override fun onInterrupt() {}

    override fun onAccessibilityEvent(event: AccessibilityEvent?) {}
...
}

Java

package com.example.android.apis.accessibility;

import android.accessibilityservice.AccessibilityService;
import android.view.accessibility.AccessibilityEvent;

public class MyAccessibilityService extends AccessibilityService {
...
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
    }

    @Override
    public void onInterrupt() {
    }

...
}

如果您為此 Service 建立新專案,且不打算與應用程式建立關聯,則可以從來源中移除範例 Activity 類別。

資訊清單宣告和權限

提供無障礙服務的應用程式必須在應用程式資訊清單中加入特定宣告,Android 系統才會將其視為無障礙服務。本節說明無障礙服務的必要和選用設定。

無障礙服務宣告

為了讓系統將您的應用程式視為無障礙服務,請在資訊清單的 application 元素中加入 service 元素,而非 activity 元素。此外,請在 service 元素中加入無障礙服務的意圖篩選器。資訊清單也必須透過新增 BIND_ACCESSIBILITY_SERVICE 權限來保護服務,確保只有系統可繫結該服務。範例如下:

  <application>
    <service android:name=".MyAccessibilityService"
        android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
        android:label="@string/accessibility_service_label">
      <intent-filter>
        <action android:name="android.accessibilityservice.AccessibilityService" />
      </intent-filter>
    </service>
  </application>

無障礙服務設定

無障礙服務必須提供設定,指定該服務能處理的無障礙事件類型,以及服務的其他相關資訊。無障礙服務的設定已納入 AccessibilityServiceInfo 類別。您的服務可以在執行階段使用這個類別的例項和 setServiceInfo() 來建構及設定。不過,並非所有設定選項都可使用這個方法。

您可以在資訊清單中加入 <meta-data> 元素和設定檔的參照,以便為無障礙服務完整設定選項,如以下範例所示:

<service android:name=".MyAccessibilityService">
  ...
  <meta-data
    android:name="android.accessibilityservice"
    android:resource="@xml/accessibility_service_config" />
</service>

這個 <meta-data> 元素會參照您在應用程式資源目錄中建立的 XML 檔案:<project_dir>/res/xml/accessibility_service_config.xml>。以下程式碼是服務設定檔的內容示例:

<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/accessibility_service_description"
    android:packageNames="com.example.android.apis"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFlags="flagDefault"
    android:accessibilityFeedbackType="feedbackSpoken"
    android:notificationTimeout="100"
    android:canRetrieveWindowContent="true"
    android:settingsActivity="com.example.android.accessibility.ServiceSettingsActivity"
/>

如想深入瞭解可在無障礙服務設定檔中使用的 XML 屬性,請參閱下列參考文件:

如要深入瞭解可以在執行階段動態設定的組態設定,請參閱 AccessibilityServiceInfo 參考文件。

設定無障礙服務

如要為無障礙服務指定設定變數,讓系統瞭解服務的執行方式與時機,請考量以下事項:

  • 您希望無障礙服務回應哪些事件類型?
  • 您需要為所有應用程式啟用該服務,還是只為特定套件名稱啟用?
  • 使用哪些不同的回饋類型?

設定這些變數的方法有兩種。回溯相容方法是使用 setServiceInfo(android.accessibilityservice.AccessibilityServiceInfo) 在程式碼中設定,如要進行此操作,請覆寫 onServiceConnected() 方法並在該處設定服務,如以下範例所示:

Kotlin

override fun onServiceConnected() {
    info.apply {
        // Set the type of events that this service wants to listen to. Others
        // aren't passed to this service.
        eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED or AccessibilityEvent.TYPE_VIEW_FOCUSED

        // If you only want this service to work with specific apps, set their
        // package names here. Otherwise, when the service is activated, it
        // listens to events from all apps.
        packageNames = arrayOf("com.example.android.myFirstApp", "com.example.android.mySecondApp")

        // Set the type of feedback your service provides.
        feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN

        // Default services are invoked only if no package-specific services are
        // present for the type of AccessibilityEvent generated. This service is
        // app-specific, so the flag isn't necessary. For a general-purpose
        // service, consider setting the DEFAULT flag.

        // flags = AccessibilityServiceInfo.DEFAULT;

        notificationTimeout = 100
    }

    this.serviceInfo = info

}

Java

@Override
public void onServiceConnected() {
    // Set the type of events that this service wants to listen to. Others
    // aren't passed to this service.
    info.eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED |
            AccessibilityEvent.TYPE_VIEW_FOCUSED;

    // If you only want this service to work with specific apps, set their
    // package names here. Otherwise, when the service is activated, it listens
    // to events from all apps.
    info.packageNames = new String[]
            {"com.example.android.myFirstApp", "com.example.android.mySecondApp"};

    // Set the type of feedback your service provides.
    info.feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN;

    // Default services are invoked only if no package-specific services are
    // present for the type of AccessibilityEvent generated. This service is
    // app-specific, so the flag isn't necessary. For a general-purpose service,
    // consider setting the DEFAULT flag.

    // info.flags = AccessibilityServiceInfo.DEFAULT;

    info.notificationTimeout = 100;

    this.setServiceInfo(info);

}

第二個方法是使用 XML 檔案設定服務。某些設定方法 (例如 canRetrieveWindowContent) 只有在使用 XML 設定服務時才能使用。以下是使用 XML 定義後,上述範例的設定選項:

<accessibility-service
     android:accessibilityEventTypes="typeViewClicked|typeViewFocused"
     android:packageNames="com.example.android.myFirstApp, com.example.android.mySecondApp"
     android:accessibilityFeedbackType="feedbackSpoken"
     android:notificationTimeout="100"
     android:settingsActivity="com.example.android.apis.accessibility.TestBackActivity"
     android:canRetrieveWindowContent="true"
/>

如果您使用 XML,請在服務宣告中加入 <meta-data> 標記,指向 XML 檔案,藉此在資訊清單中參照該標記。如果您將 XML 檔案儲存在 res/xml/serviceconfig.xml 中,新標記會如下所示:

<service android:name=".MyAccessibilityService">
     <intent-filter>
         <action android:name="android.accessibilityservice.AccessibilityService" />
     </intent-filter>
     <meta-data android:name="android.accessibilityservice"
     android:resource="@xml/serviceconfig" />
</service>

無障礙服務方法

無障礙服務必須擴充 AccessibilityService 類別,並覆寫該類別中的以下方法。這些方法會按照 Android 系統呼叫的順序呈現:從服務啟動 (onServiceConnected()) 到執行期間 (onAccessibilityEvent()onInterrupt()) 至服務關閉 (onUnbind())。

  • onServiceConnected():(選用) 系統會在連線至無障礙服務時呼叫這個方法。使用這個方法可針對服務執行一次性設定步驟,包括連線至使用者回饋系統服務,例如音訊管理員或裝置震動功能。如要在執行階段設定服務設定或進行一次調整,從這個位置呼叫 setServiceInfo() 非常方便。

  • onAccessibilityEvent():(必要) 當系統偵測到 AccessibilityEvent 符合無障礙服務指定的事件篩選參數,例如當使用者輕觸按鈕,或聚焦於無障礙服務提供回饋的應用程式中的 UI 控制項時,就會回呼這個方法。當系統呼叫此方法時,它會傳遞相關聯的 AccessibilityEvent,之後服務即可對其解讀並用於向使用者提供回饋。在服務的生命週期內,系統可能會多次呼叫這個方法。

  • onInterrupt():(必要) 當系統要中斷服務所提供的回饋時,會呼叫這個方法,通常是為了回應使用者動作,例如將焦點移至其他控制項。在服務的生命週期內,系統可能會多次呼叫這個方法。

  • onUnbind():(選用) 當系統即將關閉無障礙服務時,系統會呼叫這個方法。使用這個方法可以執行任何一次性的關閉程序,包括取消配置使用者回饋系統服務,例如音訊管理員或裝置震動功能。

這些回呼方法為無障礙服務提供了基本結構。您可以決定如何處理 Android 系統提供的資料 (格式為 AccessibilityEvent 物件),並向使用者提供回饋。如要進一步瞭解如何取得無障礙事件的資訊,請參閱「取得事件詳細資訊」。

註冊無障礙事件

無障礙服務設定參數的最重要功能之一,就是可指定服務能處理的無障礙功能事件類型。指定這項資訊可讓無障礙服務互相合作,並讓您靈活地只處理特定應用程式的某些事件類型。事件篩選條件可包含以下標準:

  • 套件名稱:指定您希望服務處理無障礙事件的應用程式套件名稱。如果省略此參數,系統會將無障礙服務視為適用於任何應用程式的無障礙事件。您可以在無障礙服務設定檔中,透過 android:packageNames 屬性將此參數設為以半形逗號分隔的清單,或使用 AccessibilityServiceInfo.packageNames 成員進行設定。

  • 事件類型:指定您希望服務處理的無障礙事件類型。您可以在無障礙服務設定檔中,使用 android:accessibilityEventTypes 屬性設定此參數,將其設為以 | 字元分隔的清單,例如 accessibilityEventTypes="typeViewClicked|typeViewFocused"。此外,您也可以使用 AccessibilityServiceInfo.eventTypes 成員進行設定。

設定無障礙服務時,請審慎考量服務能夠處理哪些事件,並只註冊這些事件。由於使用者一次可以啟用多項無障礙服務,因此請勿將服務用於無法處理的事件。提醒您,其他服務可能會處理這些事件,藉此改善使用者體驗。

無障礙工具音量

搭載 Android 8.0 (API 級別 26) 以上版本的裝置含有 STREAM_ACCESSIBILITY 音量類別,可讓您控制無障礙服務的音訊輸出音量,不受裝置上的其他音訊影響。

無障礙服務可透過設定 FLAG_ENABLE_ACCESSIBILITY_VOLUME 選項來使用此串流類型。然後,您可以在裝置的 AudioManager 例項上呼叫 adjustStreamVolume() 方法,變更裝置的無障礙音訊音量。

下列程式碼片段示範無障礙服務如何使用 STREAM_ACCESSIBILITY 音量類別:

Kotlin

import android.media.AudioManager.*

class MyAccessibilityService : AccessibilityService() {

    private val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager

    override fun onAccessibilityEvent(accessibilityEvent: AccessibilityEvent) {
        if (accessibilityEvent.source.text == "Increase volume") {
            audioManager.adjustStreamVolume(AudioManager.STREAM_ACCESSIBILITY, ADJUST_RAISE, 0)
        }
    }
}

Java

import static android.media.AudioManager.*;

public class MyAccessibilityService extends AccessibilityService {
    private AudioManager audioManager =
            (AudioManager) getSystemService(AUDIO_SERVICE);

    @Override
    public void onAccessibilityEvent(AccessibilityEvent accessibilityEvent) {
        AccessibilityNodeInfo interactedNodeInfo =
                accessibilityEvent.getSource();
        if (interactedNodeInfo.getText().equals("Increase volume")) {
            audioManager.adjustStreamVolume(AudioManager.STREAM_ACCESSIBILITY,
                ADJUST_RAISE, 0);
        }
    }
}

如需詳細資訊,請觀看 2017 年 Google I/O 大會的 Android 無障礙功能最新消息研討會影片,從 6:35 開始。

無障礙捷徑

在搭載 Android 8.0 (API 級別 26) 以上版本的裝置上,使用者可以同時按住兩個音量鍵,從任何螢幕中啟用或停用偏好的無障礙服務。雖然這個捷徑預設為可啟用和停用 Talkback,使用者依然可以設定該按鈕,用於啟用和停用他們裝置上安裝的任何服務。

如要讓使用者從無障礙工具捷徑存取特定的無障礙服務,您需要在執行階段要求這項功能。

如需詳細資訊,請觀看 2017 年 Google I/O 大會的 Android 無障礙功能最新消息研討會影片,從 13:25 開始。

無障礙工具按鈕

如果裝置使用軟體轉譯的導覽區域,且搭載 Android 8.0 (API 級別 26) 以上版本,導覽列的右側會有「無障礙工具按鈕」。視使用者目前顯示的內容而定,使用者按下這個按鈕時,可能會叫用下列其中一種已啟用的無障礙功能和服務。

如要允許使用者使用無障礙工具按鈕叫用特定的無障礙服務,該服務需要在 AccessibilityServiceInfo 物件的 android:accessibilityFlags 屬性中新增 FLAG_REQUEST_ACCESSIBILITY_BUTTON 標記。接著,服務就可以使用 registerAccessibilityButtonCallback() 註冊回呼。

下列程式碼片段示範如何設定無障礙服務,以回應使用者按下無障礙工具按鈕的動作:

Kotlin

private var mAccessibilityButtonController: AccessibilityButtonController? = null
private var accessibilityButtonCallback:
        AccessibilityButtonController.AccessibilityButtonCallback? = null
private var mIsAccessibilityButtonAvailable: Boolean = false

override fun onServiceConnected() {
    mAccessibilityButtonController = accessibilityButtonController
    mIsAccessibilityButtonAvailable =
            mAccessibilityButtonController?.isAccessibilityButtonAvailable ?: false

    if (!mIsAccessibilityButtonAvailable) return

    serviceInfo = serviceInfo.apply {
        flags = flags or AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON
    }

    accessibilityButtonCallback =
        object : AccessibilityButtonController.AccessibilityButtonCallback() {
            override fun onClicked(controller: AccessibilityButtonController) {
                Log.d("MY_APP_TAG", "Accessibility button pressed!")

                // Add custom logic for a service to react to the
                // accessibility button being pressed.
            }

            override fun onAvailabilityChanged(
                    controller: AccessibilityButtonController,
                    available: Boolean
            ) {
                if (controller == mAccessibilityButtonController) {
                    mIsAccessibilityButtonAvailable = available
                }
            }
    }

    accessibilityButtonCallback?.also {
        mAccessibilityButtonController?.registerAccessibilityButtonCallback(it, null)
    }
}

Java

private AccessibilityButtonController accessibilityButtonController;
private AccessibilityButtonController
        .AccessibilityButtonCallback accessibilityButtonCallback;
private boolean mIsAccessibilityButtonAvailable;

@Override
protected void onServiceConnected() {
    accessibilityButtonController = getAccessibilityButtonController();
    mIsAccessibilityButtonAvailable =
            accessibilityButtonController.isAccessibilityButtonAvailable();

    if (!mIsAccessibilityButtonAvailable) {
        return;
    }

    AccessibilityServiceInfo serviceInfo = getServiceInfo();
    serviceInfo.flags
            |= AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON;
    setServiceInfo(serviceInfo);

    accessibilityButtonCallback =
        new AccessibilityButtonController.AccessibilityButtonCallback() {
            @Override
            public void onClicked(AccessibilityButtonController controller) {
                Log.d("MY_APP_TAG", "Accessibility button pressed!");

                // Add custom logic for a service to react to the
                // accessibility button being pressed.
            }

            @Override
            public void onAvailabilityChanged(
              AccessibilityButtonController controller, boolean available) {
                if (controller.equals(accessibilityButtonController)) {
                    mIsAccessibilityButtonAvailable = available;
                }
            }
        };

    if (accessibilityButtonCallback != null) {
        accessibilityButtonController.registerAccessibilityButtonCallback(
                accessibilityButtonCallback, null);
    }
}

如需詳細資訊,請觀看 2017 年 Google I/O 大會的 Android 無障礙功能最新消息研討會影片,從 16:28 開始。

指紋手勢

在搭載 Android 8.0 (API 級別 26) 以上版本的裝置,無障礙服務可針對沿著裝置指紋感應器滑動的方向 (向上、向下、向左和向右) 做出回應。如要設定服務來接收這些互動的回呼,請依序完成以下步驟:

  1. 宣告 USE_BIOMETRIC 權限和 CAPABILITY_CAN_REQUEST_FINGERPRINT_GESTURES 功能。
  2. android:accessibilityFlags 屬性中設定 FLAG_REQUEST_FINGERPRINT_GESTURES 標記。
  3. 使用 registerFingerprintGestureCallback() 註冊回呼。

請注意,並非所有裝置都有指紋感應器。如要確認裝置是否支援感應器,請使用 isHardwareDetected() 方法。即使裝置含有指紋感應器,服務也無法將感應器用於身分驗證用途。如要得知感應器何時可用,請呼叫 isGestureDetectionAvailable() 方法,並實作 onGestureDetectionAvailabilityChanged() 回呼。

下列程式碼片段示範如何使用指紋手勢瀏覽虛擬遊戲圖版:

// AndroidManifest.xml
<manifest ... >
    <uses-permission android:name="android.permission.USE_FINGERPRINT" />
    ...
    <application>
        <service android:name="com.example.MyFingerprintGestureService" ... >
            <meta-data
                android:name="android.accessibilityservice"
                android:resource="@xml/myfingerprintgestureservice" />
        </service>
    </application>
</manifest>
// myfingerprintgestureservice.xml
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    ...
    android:accessibilityFlags=" ... |flagRequestFingerprintGestures"
    android:canRequestFingerprintGestures="true"
    ... />

Kotlin

// MyFingerprintGestureService.kt
import android.accessibilityservice.FingerprintGestureController.*

class MyFingerprintGestureService : AccessibilityService() {

    private var gestureController: FingerprintGestureController? = null
    private var fingerprintGestureCallback:
            FingerprintGestureController.FingerprintGestureCallback? = null
    private var mIsGestureDetectionAvailable: Boolean = false

    override fun onCreate() {
        gestureController = fingerprintGestureController
        mIsGestureDetectionAvailable = gestureController?.isGestureDetectionAvailable ?: false
    }

    override fun onServiceConnected() {
        if (mFingerprintGestureCallback != null || !mIsGestureDetectionAvailable) return

        fingerprintGestureCallback =
                object : FingerprintGestureController.FingerprintGestureCallback() {
                    override fun onGestureDetected(gesture: Int) {
                        when (gesture) {
                            FINGERPRINT_GESTURE_SWIPE_DOWN -> moveGameCursorDown()
                            FINGERPRINT_GESTURE_SWIPE_LEFT -> moveGameCursorLeft()
                            FINGERPRINT_GESTURE_SWIPE_RIGHT -> moveGameCursorRight()
                            FINGERPRINT_GESTURE_SWIPE_UP -> moveGameCursorUp()
                            else -> Log.e(MY_APP_TAG, "Error: Unknown gesture type detected!")
                        }
                    }

                    override fun onGestureDetectionAvailabilityChanged(available: Boolean) {
                        mIsGestureDetectionAvailable = available
                    }
                }

        fingerprintGestureCallback?.also {
            gestureController?.registerFingerprintGestureCallback(it, null)
        }
    }
}

Java

// MyFingerprintGestureService.java
import static android.accessibilityservice.FingerprintGestureController.*;

public class MyFingerprintGestureService extends AccessibilityService {
    private FingerprintGestureController gestureController;
    private FingerprintGestureController
            .FingerprintGestureCallback fingerprintGestureCallback;
    private boolean mIsGestureDetectionAvailable;

    @Override
    public void onCreate() {
        gestureController = getFingerprintGestureController();
        mIsGestureDetectionAvailable =
                gestureController.isGestureDetectionAvailable();
    }

    @Override
    protected void onServiceConnected() {
        if (fingerprintGestureCallback != null
                || !mIsGestureDetectionAvailable) {
            return;
        }

        fingerprintGestureCallback =
               new FingerprintGestureController.FingerprintGestureCallback() {
            @Override
            public void onGestureDetected(int gesture) {
                switch (gesture) {
                    case FINGERPRINT_GESTURE_SWIPE_DOWN:
                        moveGameCursorDown();
                        break;
                    case FINGERPRINT_GESTURE_SWIPE_LEFT:
                        moveGameCursorLeft();
                        break;
                    case FINGERPRINT_GESTURE_SWIPE_RIGHT:
                        moveGameCursorRight();
                        break;
                    case FINGERPRINT_GESTURE_SWIPE_UP:
                        moveGameCursorUp();
                        break;
                    default:
                        Log.e(MY_APP_TAG,
                                  "Error: Unknown gesture type detected!");
                        break;
                }
            }

            @Override
            public void onGestureDetectionAvailabilityChanged(boolean available) {
                mIsGestureDetectionAvailable = available;
            }
        };

        if (fingerprintGestureCallback != null) {
            gestureController.registerFingerprintGestureCallback(
                    fingerprintGestureCallback, null);
        }
    }
}

如需詳細資訊,請觀看 2017 年 Google I/O 大會的 Android 無障礙功能最新消息研討會影片,從 9:03 開始。

多語文字轉語音

從 Android 8.0 版 (API 級別 26) 開始,Android 的文字轉語音 (TTS) 服務可以在單一文字區塊中識別和朗讀多種語言的語音內容。如要在無障礙服務中啟用這種自動語言切換功能,請將所有字串納入 LocaleSpan 物件中,如以下程式碼片段所示:

Kotlin

val localeWrappedTextView = findViewById<TextView>(R.id.my_french_greeting_text).apply {
    text = wrapTextInLocaleSpan("Bonjour!", Locale.FRANCE)
}

private fun wrapTextInLocaleSpan(originalText: CharSequence, loc: Locale): SpannableStringBuilder {
    return SpannableStringBuilder(originalText).apply {
        setSpan(LocaleSpan(loc), 0, originalText.length - 1, 0)
    }
}

Java

TextView localeWrappedTextView = findViewById(R.id.my_french_greeting_text);
localeWrappedTextView.setText(wrapTextInLocaleSpan("Bonjour!", Locale.FRANCE));

private SpannableStringBuilder wrapTextInLocaleSpan(
        CharSequence originalText, Locale loc) {
    SpannableStringBuilder myLocaleBuilder =
            new SpannableStringBuilder(originalText);
    myLocaleBuilder.setSpan(new LocaleSpan(loc), 0,
            originalText.length() - 1, 0);
    return myLocaleBuilder;
}

如需詳細資訊,請觀看 2017 年 Google I/O 大會的 Android 無障礙功能最新消息研討會影片,從 10:59 開始。

代表使用者執行操作

自 2011 年起,無障礙服務可以代表使用者執行各種操作,包括變更輸入焦點及選取 (啟用) 使用者介面元素。到了 2012 年,操作範圍擴大,涵蓋捲動清單以及與文字欄位互動。無障礙服務也可以執行全域動作,例如前往主畫面、按下「返回」按鈕、開啟通知畫面和最近使用的應用程式清單。自 2012 年起,Android 加入「無障礙功能焦點」,讓無障礙服務可選取所有可見元素。

這些功能使無障礙服務的開發人員可建立替代導覽模式 (例如手勢操作),並幫助身心障礙使用者進一步控管 Android 裝置。

監聽手勢

無障礙服務可以監聽特定手勢,並代表使用者執行操作來作出回應。您的無障礙服務需啟用「輕觸探索」,才能使用這項功能。將服務的 AccessibilityServiceInfo 例項中 flags 成員設定為 FLAG_REQUEST_TOUCH_EXPLORATION_MODE,即可要求啟用功能,如以下範例所示。

Kotlin

class MyAccessibilityService : AccessibilityService() {

    override fun onCreate() {
        serviceInfo.flags = AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE
    }
    ...
}

Java

public class MyAccessibilityService extends AccessibilityService {
    @Override
    public void onCreate() {
        getServiceInfo().flags = AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE;
    }
    ...
}

當服務要求啟用「輕觸探索」後,如果這項功能尚未啟用,使用者須啟用這項功能。啟用這項功能後,您的服務可透過服務的 onGesture() 回呼方法接收無障礙手勢通知,並可藉由為使用者執行操作來回應手勢。

連續手勢

搭載 Android 8.0 版 (API 級別 26) 的裝置支援「連續手勢」,或包含多個 Path 物件的程式輔助手勢。

指定一連串的筆劃順序時,您可以使用 GestureDescription.StrokeDescription 建構函式中的最終引數 willContinue,指定這些筆劃屬於同一個程式輔助手勢,如以下程式碼片段所示:

Kotlin

// Simulates an L-shaped drag path: 200 pixels right, then 200 pixels down.
private fun doRightThenDownDrag() {
    val dragRightPath = Path().apply {
        moveTo(200f, 200f)
        lineTo(400f, 200f)
    }
    val dragRightDuration = 500L // 0.5 second

    // The starting point of the second path must match
    // the ending point of the first path.
    val dragDownPath = Path().apply {
        moveTo(400f, 200f)
        lineTo(400f, 400f)
    }
    val dragDownDuration = 500L
    val rightThenDownDrag = GestureDescription.StrokeDescription(
            dragRightPath,
            0L,
            dragRightDuration,
            true
    ).apply {
        continueStroke(dragDownPath, dragRightDuration, dragDownDuration, false)
    }
}

Java

// Simulates an L-shaped drag path: 200 pixels right, then 200 pixels down.
private void doRightThenDownDrag() {
    Path dragRightPath = new Path();
    dragRightPath.moveTo(200, 200);
    dragRightPath.lineTo(400, 200);
    long dragRightDuration = 500L; // 0.5 second

    // The starting point of the second path must match
    // the ending point of the first path.
    Path dragDownPath = new Path();
    dragDownPath.moveTo(400, 200);
    dragDownPath.lineTo(400, 400);
    long dragDownDuration = 500L;
    GestureDescription.StrokeDescription rightThenDownDrag =
            new GestureDescription.StrokeDescription(dragRightPath, 0L,
            dragRightDuration, true);
    rightThenDownDrag.continueStroke(dragDownPath, dragRightDuration,
            dragDownDuration, false);
}

如需詳細資訊,請觀看 2017 年 Google I/O 大會的 Android 無障礙功能最新消息研討會影片,從 15:47 開始。

使用無障礙動作

無障礙服務可以代表使用者執行各種操作,簡化與應用程式的互動,進而提升效率。2011 年已新增讓無障礙服務執行操作的功能,並在 2012 年大幅擴充這項功能。

為了代表使用者執行操作,您的無障礙服務必須註冊以便接收應用程式事件,並且在服務設定檔中將 android:canRetrieveWindowContent 設為 true,要求取得查看應用程式內容的權限。當服務收到事件時,就可以使用 getSource(),從事件中擷取 AccessibilityNodeInfo 物件。接著,藉由 AccessibilityNodeInfo 物件,您的服務可以探索檢視區塊階層,以判斷應採取的行動,然後再使用 performAction() 為使用者執行操作。

Kotlin

class MyAccessibilityService : AccessibilityService() {

    override fun onAccessibilityEvent(event: AccessibilityEvent) {
        // Get the source node of the event.
        event.source?.apply {

            // Use the event and node information to determine what action to
            // take.

            // Act on behalf of the user.
            performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)

            // Recycle the nodeInfo object.
            recycle()
        }
    }
    ...
}

Java

public class MyAccessibilityService extends AccessibilityService {

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        // Get the source node of the event.
        AccessibilityNodeInfo nodeInfo = event.getSource();

        // Use the event and node information to determine what action to take.

        // Act on behalf of the user.
        nodeInfo.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);

        // Recycle the nodeInfo object.
        nodeInfo.recycle();
    }
    ...
}

performAction() 方法可讓服務在應用程式中執行操作。如果您的服務需要執行全域動作,例如前往主畫面、輕觸「返回」按鈕、開啟通知畫面,或是開啟最近使用的應用程式清單,請使用 performGlobalAction() 方法。

使用焦點類型

2012 年,Android 推出了名為「無障礙焦點」的使用者介面焦點。無障礙服務可利用這個焦點選取任何可見的使用者介面元素,並對其執行操作。這個焦點類型與「輸入焦點」不同,後者會決定當使用者輸入字元、按下鍵盤的 Enter 鍵、或是按下 D-pad 的中間按鈕時,畫面上的使用者介面元素會接收哪些輸入內容。

事實上,使用者介面中的某一個元素可能具有輸入焦點功能,而另一個元素則具有無障礙焦點。無障礙焦點旨在為無障礙服務提供與畫面上可見元素互動的方法,無論該元素是否可從系統的角度聚焦輸入。為確保您的無障礙服務能正確與應用程式的輸入元素互動,請按照「測試應用程式的無障礙程度」指南操作,在使用一般應用程式時測試服務。

無障礙服務可使用 AccessibilityNodeInfo.findFocus() 方法,判斷哪些使用者介面元素包含輸入焦點或無障礙焦點。您也可以使用 focusSearch() 方法,搜尋可透過輸入焦點選取的元素。最後,您的無障礙服務可以使用 performAction(AccessibilityNodeInfo.ACTION_SET_ACCESSIBILITY_FOCUS) 方法設定無障礙焦點。

收集資訊

無障礙服務也具備標準方法,可用來收集和代表使用者提供資訊的主要單位,例如事件詳細資訊、文字和數字。

取得視窗變更詳細資訊

Android 9 (API 等級 28) 以上版本允許應用程式在同時重新繪製多個視窗時追蹤視窗更新。發生 TYPE_WINDOWS_CHANGED 事件時,請使用 getWindowChanges() API 判斷視窗的變化方式。在多視窗模式更新期間,每個視窗都會產生一組專屬事件。getSource() 方法會傳回與每個事件關聯的視窗根層級檢視區塊。

如果某個應用程式為其 View 物件定義了 無障礙窗格標題,當應用程式的使用者介面更新時,您的服務就可以識別。發生 TYPE_WINDOW_STATE_CHANGED 事件時,請使用 getContentChangeTypes() 傳回的類型來判斷視窗的變化情形。例如,架構可偵測窗格何時有新標題,或者窗格何時消失。

取得事件詳細資訊

Android 會透過 AccessibilityEvent 物件,為無障礙服務提供有關使用者介面互動的資訊。在先前的 Android 版本中,無障礙事件雖然會提供使用者所選 UI 控制項的重要詳細資料,但可提供的背景資訊有限。在許多情況下,這些缺少的背景資訊可能對理解所選控制項的含義至關重要。

以行事曆或日行程規劃工具為例,說明背景資訊在介面中的重要性。如果使用者在星期一至星期五的一日行程清單中選取下午 4:00 時段,而無障礙服務宣告的是「下午 4:00」,但並未宣告星期幾、幾月幾日,或是哪個月份,所產生的回饋會令人困惑。在這種情況下,對於想安排會議時間的使用者來說,使用者介面控制項的結構定義非常重要。

自 2011 年起,Android 可根據檢視區塊階層編寫無障礙事件,大幅增加無障礙服務可取得的使用者介面互動相關資訊。檢視區塊階層是指包含該元件的一系列使用者介面元件 (其父項),以及該元件可能包含的使用者介面元素 (其子項)。如此一來,Android 即可針對無障礙事件提供更豐富的詳細資訊,讓無障礙服務為使用者提供更實用的回饋。

無障礙服務可透過系統傳遞至服務的 onAccessibilityEvent() 回呼方法的 AccessibilityEvent,取得使用者介面事件的相關資訊。這個物件提供事件的相關詳細資料,包括要執行操作的物件類型、說明文字及其他詳細資料。

  • AccessibilityEvent.getRecordCount()getRecord(int):這些方法可讓您擷取一組 AccessibilityRecord 物件,讓系統將 AccessibilityEvent 傳送給您。這個等級的詳細資料為觸發無障礙服務的事件提供了更多背景資訊。

  • AccessibilityRecord.getSource():這個方法會傳回一個 AccessibilityNodeInfo 物件,讓您要求來自無障礙功能事件的元件檢視版面配置階層 (父項和子項)。這項功能可讓無障礙服務調查事件的完整背景資訊,包括任何封閉檢視區塊或子項檢視區塊的內容和狀態。

Android 平台可讓 AccessibilityService 查詢檢視區塊階層,針對產生事件的 UI 元件及其父項和子項收集相關資訊,方法是在 XML 設定中設定以下這行程式碼:

android:canRetrieveWindowContent="true"

完成後,請使用 getSource() 取得 AccessibilityNodeInfo 物件。只有事件來源的視窗仍為使用中視窗,這個呼叫才會傳回物件。如果不是使用中視窗,系統會傳回空值,並執行相應操作。

在以下範例中,程式碼會在收到事件時執行以下操作:

  1. 立即擷取事件來源的檢視區塊父項。
  2. 在該檢視區塊中,找出做為子檢視區塊的標籤和核取方塊。
  3. 如果找到這些項目,請建立字串向使用者回報該標籤,並指出該標籤是否已勾選。

在掃遍檢視區塊階層的過程中,如果在任何時間點傳回空值,此方法會以靜默方式放棄尋找。

Kotlin

// Alternative onAccessibilityEvent that uses AccessibilityNodeInfo.

override fun onAccessibilityEvent(event: AccessibilityEvent) {

    val source: AccessibilityNodeInfo = event.source ?: return

    // Grab the parent of the view that fires the event.
    val rowNode: AccessibilityNodeInfo = getListItemNodeInfo(source) ?: return

    // Using this parent, get references to both child nodes, the label, and the
    // checkbox.
    val taskLabel: CharSequence = rowNode.getChild(0)?.text ?: run {
        rowNode.recycle()
        return
    }

    val isComplete: Boolean = rowNode.getChild(1)?.isChecked ?: run {
        rowNode.recycle()
        return
    }

    // Determine what the task is and whether it's complete based on the text
    // inside the label, and the state of the checkbox.
    if (rowNode.childCount < 2 || !rowNode.getChild(1).isCheckable) {
        rowNode.recycle()
        return
    }

    val completeStr: String = if (isComplete) {
        getString(R.string.checked)
    } else {
        getString(R.string.not_checked)
    }
    val reportStr = "$taskLabel$completeStr"
    speakToUser(reportStr)
}

Java

// Alternative onAccessibilityEvent that uses AccessibilityNodeInfo.

@Override
public void onAccessibilityEvent(AccessibilityEvent event) {

    AccessibilityNodeInfo source = event.getSource();
    if (source == null) {
        return;
    }

    // Grab the parent of the view that fires the event.
    AccessibilityNodeInfo rowNode = getListItemNodeInfo(source);
    if (rowNode == null) {
        return;
    }

    // Using this parent, get references to both child nodes, the label, and the
    // checkbox.
    AccessibilityNodeInfo labelNode = rowNode.getChild(0);
    if (labelNode == null) {
        rowNode.recycle();
        return;
    }

    AccessibilityNodeInfo completeNode = rowNode.getChild(1);
    if (completeNode == null) {
        rowNode.recycle();
        return;
    }

    // Determine what the task is and whether it's complete based on the text
    // inside the label, and the state of the checkbox.
    if (rowNode.getChildCount() < 2 || !rowNode.getChild(1).isCheckable()) {
        rowNode.recycle();
        return;
    }

    CharSequence taskLabel = labelNode.getText();
    final boolean isComplete = completeNode.isChecked();
    String completeStr = null;

    if (isComplete) {
        completeStr = getString(R.string.checked);
    } else {
        completeStr = getString(R.string.not_checked);
    }
    String reportStr = taskLabel + completeStr;
    speakToUser(reportStr);
}

現在您已擁有完整且可運作的無障礙服務。請嘗試設定與使用者互動的方式,例如新增 Android 的文字轉語音引擎,或使用 Vibrator 提供觸覺回饋。

處理文字

在搭載 Android 8.0 (API 級別 26) 以上版本的裝置中包含多向文字處理功能,可讓無障礙服務更輕鬆地識別及操作畫面上顯示的特定文字單元。

工具提示

Android 9 (API 級別 28) 導入了多項功能,可讓您在應用程式的 UI 中存取 工具提示。請使用 getTooltipText() 讀取工具提示的文字,並使用 ACTION_SHOW_TOOLTIPACTION_HIDE_TOOLTIP 指示 View 的例項顯示或隱藏其工具提示。

提示文字

自 2017 年起,Android 提供多種方法,可與文字型物件的提示文字互動:

  • isShowingHintText()setShowingHintText() 方法可分別指出及設定節點的目前文字內容是否表示節點的提示文字。
  • getHintText() 提供提示文字本身的存取權。即使物件未顯示提示文字,呼叫 getHintText() 也會成功。

畫面上文字字元的位置

在搭載 Android 8.0 (API 級別 26) 以上版本的裝置上,無障礙服務可在 TextView 小工具中判斷每個可見字元的定界框螢幕座標。服務只要呼叫 refreshWithExtraData() 並傳入 EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY 做為第一個引數,並將 Bundle 物件做為第二個引數,即可找到這些座標。執行此方法時,系統會使用 Bundle 物件的可封裝陣列將 Rect 引數填入物件每個 Rect 物件都代表特定半形字元的定界框。

標準化的單側範圍值

部分 AccessibilityNodeInfo 物件使用 AccessibilityNodeInfo.RangeInfo 例項,表示 UI 元素可以採用一個範圍的值。使用 RangeInfo.obtain() 建立範圍時,或使用 getMin()getMax() 擷取範圍的極端值時,請注意,搭載 Android 8.0 (API 級別) 的裝置 26) 以上版本的裝置會以標準化方式呈現單面範圍:

回應無障礙事件

現在,您的服務已經開始執行並監聽事件,請編寫一些程式碼,以便在收到 AccessibilityEvent 時知道該怎麼做。首先,覆寫 onAccessibilityEvent(AccessibilityEvent) 方法。在該方法中,使用 getEventType() 來判斷事件類型,並使用 getContentDescription() 擷取與觸發事件的檢視區塊相關聯的任何標籤文字。

Kotlin

override fun onAccessibilityEvent(event: AccessibilityEvent) {
    var eventText: String = when (event.eventType) {
        AccessibilityEvent.TYPE_VIEW_CLICKED -> "Clicked: "
        AccessibilityEvent.TYPE_VIEW_FOCUSED -> "Focused: "
        else -> ""
    }

    eventText += event.contentDescription

    // Do something nifty with this text, like speak the composed string back to
    // the user.
    speakToUser(eventText)
    ...
}

Java

@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
    final int eventType = event.getEventType();
    String eventText = null;
    switch(eventType) {
        case AccessibilityEvent.TYPE_VIEW_CLICKED:
            eventText = "Clicked: ";
            break;
        case AccessibilityEvent.TYPE_VIEW_FOCUSED:
            eventText = "Focused: ";
            break;
    }

    eventText = eventText + event.getContentDescription();

    // Do something nifty with this text, like speak the composed string back to
    // the user.
    speakToUser(eventText);
    ...
}

其他資源

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

指南

程式碼研究室