建立專屬的無障礙服務

無障礙服務是一個應用程式,提供使用者介面增強效果,可協助身心障礙使用者,或是暫時無法與裝置全面互動的使用者。例如,正在開車、照顧幼童,或參加極度喧囂派對的使用者,可能需要其他介面或替代介面的回饋。

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

注意: 只有用於幫助身心障礙使用者與您的應用程式互動,才能使用平台層級無障礙服務。

從 Android 1.6 版 (API 等級 4) 開始,您可以建構和部署無障礙服務,並且在 Android 4.0 版 (API 級別 14) 中已有顯著改善。Android 支援資料庫 也已隨著 Android 4.0 版本更新,以支援這些自 Android 1.6 以上版本的更進階無障礙功能。我們鼓勵以打造廣泛相容的無障礙服務為目標的開發人員,建議他們使用「支援資料庫」並開發應用程式,讓 Android 4.0 中推出更進階的無障礙功能。

建立無障礙服務

無障礙服務可與一般應用程式搭配使用,或做為獨立的 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() {
    }

...
}

如果已為這項服務建立了一個新專案,並且不打算讓應用程式與這項服務建立關聯,則可將啟動條件活動類別從來源中移除。

資訊清單宣告和權限

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

無障礙服務宣告

您必須在資訊清單的 application 元素中加入 service 元素 (而非 activity 元素),系統才會將其視為無障礙服務。此外,在 service 元素中,您還必須加入無障礙服務意圖篩選器。為與 Android 4.1 以上版本相容,資訊清單也必須新增 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>

在 Android 1.6 (API 等級 4) 以上版本上部署的所有無障礙服務都需要這些宣告。

無障礙服務設定

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

從 Android 4.0 版開始,您可以在資訊清單中加入一個 <meta-data> 元素,藉此參照設定檔,方便您為無障礙服務設定所有選項,如以下範例所示:

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

這個中繼資料元素是指您在應用程式的資源目錄 (<project_dir>/res/xml/accessibility_service_config.xml) 中建立的 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
        // won't be passed to this service.
        eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED or AccessibilityEvent.TYPE_VIEW_FOCUSED

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

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

        // Default services are invoked only if no package-specific ones are present
        // for the type of AccessibilityEvent generated. This service *is*
        // application-specific, so the flag isn't necessary. If this was a
        // general-purpose service, it would be worth considering 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
    // won't be passed to this service.
    info.eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED |
            AccessibilityEvent.TYPE_VIEW_FOCUSED;

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

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

    // Default services are invoked only if no package-specific ones are present
    // for the type of AccessibilityEvent generated. This service *is*
    // application-specific, so the flag isn't necessary. If this was a
    // general-purpose service, it would be worth considering 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() - (可選) 當系統即將關閉無障礙服務時,會呼叫這個方法。使用這個方法可以執行任何一次性的關機程序,包括取消配置使用者意見回饋系統服務,例如音訊管理員或裝置震動功能。

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

註冊無障礙事件

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

  • 套件名稱:指定您希望服務處理無障礙功能事件的應用程式套件名稱。如果略過這個參數,無障礙服務會被視為適用於任何應用程式的無障礙事件。您可以在無障礙服務設定檔中使用 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() 註冊回呼。

注意:這項功能僅適用於提供軟體轉譯導覽區域的裝置。服務必須一律使用 isAccessibilityButtonAvailable(),並根據導入 onAvailabilityChanged() 的無障礙工具按鈕可用性回應變更。如此一來,即使無法使用無障礙工具按鈕或變得無法使用,使用者仍然可以隨時存取服務的功能。

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

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_FINGERPRINT 權限和 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"
    ... />

MyFingerprintGestureService.java

Kotlin

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

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 開始。

為使用者執行操作

從 Android 4.0 版 (API 級別 14) 開始,無障礙服務可以代表使用者執行各種操作,包括變更輸入焦點和選取 (啟用) 使用者介面元素。在 Android 4.1 版 (API 等級 16) 中,操作範圍已擴大,包括捲動清單以及與文字欄位互動。無障礙服務也可以執行全域動作,例如前往主畫面、按下「返回」(Back) 按鈕、開啟通知畫面和最近使用的應用程式清單。Android 4.1 版還納入了新的焦點類型「Accessibilty Focus」(無障礙焦點) ,讓所有可見元素都能透過無障礙服務選取。

這些新功能可讓無障礙服務開發人員建立替代導覽模式 (例如 手勢操作),並為身心障礙使用者提供更完善的 Android 裝置控管功能。

監聽手勢

無障礙服務可以監聽特定手勢,並透過代表使用者執行操作而作出回應。這項功能已新增在 Android 4.1 版 (API 級別 16) 中,需要您的無障礙服務要求啟用「Explore by Touch」(輕觸探索) 功能。將服務的 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;
    }
    ...
}

如果您的服務要求啟用「Explore by Touch」(輕觸探索) 功能後,使用者就必須啟用這項功能 (如果尚未啟用)。啟用這項功能後,您的服務可透過服務的 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 開始。

使用無障礙動作

無障礙服務可以代表使用者執行操作,讓應用程式互動更輕鬆、更有效率。Android 4.0 版 (API 級別 14) 中已新增無障礙服務執行操作的功能,並以 Android 4.1 版 (API 等級 16) 進行了大幅擴充。

為了代表使用者執行操作,無障礙服務必須 註冊 從幾個或多個應用程式接收事件,並在 服務設定檔 中將 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

            // take action 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

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

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

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

使用焦點類型

Android 4.1 版 (API 等級 16) 推出一種名為「無障礙焦點」 的新型使用者介面焦點。無障礙服務可透過這種類型的焦點選取任何可見的使用者介面元素,並對其執行操作。這種焦點類型不同於廣為人知的「輸入焦點」,後者可確定當使用者輸入字元、按下鍵盤上的「Enter」鍵,或是按下 D-pad 控制項的中間按鈕時,畫面上使用者介面元素接收哪些輸入內容。

「無障礙焦點」與「輸入焦點」完全無關。事實上,使用者介面中的某一個元素可能具有輸入焦點功能,而另一個元素則具有無障礙焦點。無障礙焦點旨在提供無障礙服務,採用讓使用者與畫面上任何可見元素互動的方法,無論該元素是否可從系統的角度聚焦輸入。您可以透過測試無障礙手勢,查看執行中的無障礙焦點。如要進一步瞭解如何測試這項功能,請參閱 測試手勢操作

注意: 當某個元素能夠進行這種類型聚焦功能時,使用「無障礙焦點」的無障礙服務負責同步處理目前的輸入焦點。未將「輸入焦點」與「無障礙焦點」同步處理的服務,可能會在應用程中引起問題,因為這些應用程式在執行某些操作時,希望輸入焦點位於特定位置。

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

收集資訊

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

取得事件詳細資訊

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

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

Android 4.0 版根據檢視區塊階層撰寫無障礙事件,大幅增加無障礙服務可取得有關使用者介面互動的資訊量。檢視區塊階層是指包含元件的使用這介面元件 (其上層) 與該元件可能包含的使用者介面元素 (其下層) 的組合。如此一來,Android 系統即可針對無障礙事件提供更加豐富的詳細資訊,使無障礙服務能夠為使用者提供更實用的意見回饋。

無障礙服務透過系統傳遞至服務的 onAccessibilityEvent() 回呼方法的 AccessibilityEvent 取得有關使用者介面事件的資訊。這個物件提供有關事件的詳細資料,包括要執行操作的物件類型、說明文字及其他詳細資料。從 Android 4.0 版開始 (也能透過「支援資料庫」中的 AccessibilityEventCompat 物件支援之前版本),您可以使用下列這些呼叫取得事件的其他相關資訊:

取得視窗變更詳細資訊

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

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

收集無障礙功能節點的詳細資訊

這是選擇性步驟,但非常實用。Android 平台可讓 AccessibilityService 查詢檢視區塊階層、收集產生事件的 UI 元件及其上下層的相關資訊。為此,請務必在 XML 設定中設定以下這行程式碼:

android:canRetrieveWindowContent="true"

完成後,請使用 getSource() 取得 AccessibilityNodeInfo 物件。只有事件來源的視窗仍為使用中視窗,這個呼叫才會傳回物件。否則,系統會傳回空值,因此「執行相應操作」。以下範例是程式碼片段,當其接收事件時執行以下操作:

  1. 立即擷取事件來源的上層
  2. 在該檢視畫面中,請查找下層檢視畫面的標籤和核取方塊
  3. 如果找到這些物件,請建立一個字串回報給使用者,指出標籤及是否已勾選標籤。
  4. 當掃遍檢視區塊階層時,如果在任何時間點傳回空值,此方法會靜默地放棄查找。

Kotlin

// Alternative onAccessibilityEvent, that uses AccessibilityNodeInfo

override fun onAccessibilityEvent(event: AccessibilityEvent) {

    val source: AccessibilityNodeInfo = event.source ?: return

    // Grab the parent of the view that fired 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 or not it's complete, based on
    // the text inside the label, and the state of the check-box.
    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 fired 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 or not it's complete, based on
    // the text inside the label, and the state of the check-box.
    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 的執行個體顯示或隱藏其工具提示。

提示文字

Android 8.0 版 (API 級別 26) 包含多種方法,可與文字型物件的提示文字互動:

畫面上文字半形字元的位置

在搭載 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);
    ...
}

其他資源

如要進一步瞭解,請充分運用以下資源:

Codelab