建立專屬的無障礙服務

無障礙服務應用程式可改善使用者介面,協助身心障礙使用者或暫時無法與裝置完全互動的使用者。舉例來說,正在開車、照顧年幼孩童或參加非常高音量派對的使用者,可能需要額外或替代介面上的意見回饋。

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 符合無障礙服務指定的事件篩選參數時 (例如,使用者輕觸按鈕,或聚焦於無障礙服務提供回饋的使用者介面控制項),系統就會呼叫此方法。系統呼叫此方法時,會傳遞相關聯的 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 物件定義無障礙窗格標題,當應用程式的 UI 更新時,您的服務就能辨識。發生 TYPE_WINDOW_STATE_CHANGED 事件時,請使用 getContentChangeTypes() 傳回的類型來判斷視窗的變化方式。舉例來說,架構可偵測窗格何時有新標題或窗格消失。

取得事件詳細資訊

Android 透過 AccessibilityEvent 物件向無障礙服務提供使用者介面互動的相關資訊。在先前的 Android 版本中,無障礙事件中提供的資訊,同時提供關於使用者所選使用者介面控制項的詳細詳細資訊,可提供有限的背景資訊。在許多情況下,缺少的背景資訊資訊可能對於瞭解所選控制項的意義至關重要。

以行事曆或日行程規劃為重要介面的介面範例。如果使用者在星期一至星期五的日子名單中選取了下午 4:00 的時段,但無障礙服務宣布「下午 4 點」,但未公告平日名稱、星期幾或月份名稱,產生的意見回饋就會造成混淆。在這種情況下,對於想安排會議時間的使用者來說,使用者介面控制項的結構定義非常重要。

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

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

  • AccessibilityEvent.getRecordCount()getRecord(int):這些方法可讓您擷取屬於系統傳送給您的 AccessibilityEventAccessibilityRecord 物件組合。此等級的細節為觸發無障礙服務的事件提供更多背景資訊。

  • 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 物件做為第二個引數。執行方法時,系統會以 Rect 物件的可包裝陣列填入 Bundle 引數。每個 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);
    ...
}

其他資源

詳情請參閱下列資源:

指南

程式碼研究室