在無障礙介面下使用自訂檢視區塊

如果您的應用程式需要自訂檢視區塊元件,則需要執行一些額外的工作以使檢視區塊更便於存取。以下是改善自訂檢視區塊無障礙功能的主要工作:

處理方向控制器點擊次數

在大多數裝置上,使用方向控制器按一下檢視區塊,就會向當前聚焦的檢視區塊傳送具有 KEYCODE_DPAD_CENTERKeyEvent。所有標準 Android 檢視區塊已能夠妥善處理 KEYCODE_DPAD_CENTER。建立自訂 View 控制項時,請確保在輕觸觸控螢幕上的檢視區塊時,該事件會產生相同效果。

自訂控制項也應將 KEYCODE_ENTER 事件視為與 KEYCODE_DPAD_CENTER 相同。這種方法可讓使用者更輕鬆地從完整鍵盤進行互動。

實作 Accessibility API 方法

無障礙功能事件是關於使用者與應用程式中視覺介面元件互動的訊息。無障礙服務會處理這些訊息,並使用這些事件中的資訊產生補充意見回饋和提示。在 Android 4.0 (API 級別 14) 及以上版本中,產生無障礙功能事件的方法已得到擴展,能夠比 AccessibilityEventSourceAndroid 1.6 (API 級別 4) 中引入的介面提供更詳細的資訊。擴展的無障礙方法是 View 類別和 View.AccessibilityDelegate 類別的一部分。方法如下:

sendAccessibilityEvent()
(API 級別 4) 當使用者對檢視區塊執行動作時,系統會呼叫此方法。這個事件會以使用者動作類型進行分類,例如 TYPE_VIEW_CLICKED。一般來說,若非建立自訂檢視區塊,您無需實作此方法。
sendAccessibilityEventUnchecked()
(API 級別 4) 當呼叫代碼需要直接控制要在裝置上啟用之無障礙功能的檢查 (AccessibilityManager.isEnabled()) 時,就可以使用這個方法。如果實作此方法,無論實際系統設定如何,您都必須在當做無障礙功能已啟用的情況下執行呼叫。一般來說,您無需為自訂檢視區塊實作此方法。
dispatchPopulateAccessibilityEvent()
(API 級別 4) 當您的自訂檢視區塊產生無障礙功能事件時,系統會呼叫此方法。從 API 級別 14 開始,這個方法的預設實作方式會針對該檢視區塊呼叫 onPopulateAccessibilityEvent(),然後針對該檢視區塊中的每個子項呼叫 dispatchPopulateAccessibilityEvent() 方法。如要在「低於」Android 4.0 (API 級別 14) 的修訂版本中支援無障礙服務,您「必須」覆寫此方法,並針對自訂檢視區塊將說明文字填入 getText(),也就是無障礙服務 (例如 TalkBack) 的朗讀內容。
onPopulateAccessibilityEvent()
(API 等級 14) 此方法會為檢視區塊設定 AccessibilityEvent 的語音文字提示。如果檢視區塊是產生無障礙功能事件的檢視區塊子項,也會呼叫此方法。

注意:如果透過這種方式修改文字以外的其他屬性,有可能會覆寫其他方法設定的屬性。使用此方法修改無障礙功能事件的屬性時,應僅限於修改文字內容,並使用 onInitializeAccessibilityEvent() 方法修改事件的其他屬性。

注意:如果該事件的實作方式完全覆寫輸出文字,且不允許版面配置的其他部分修改內容,則請勿在程式碼中呼叫此方法的上層實作。

onInitializeAccessibilityEvent()
(API 級別 14) 系統會呼叫此方法,以取得檢視區塊狀態的其他相關資訊 (文字內容除外)。如果自訂檢視區塊提供簡單 TextViewButton 以外的互動式控制項,建議您覆寫此方法,並將檢視區塊的其他相關資訊設定到使用此方法的事件中,例如密碼欄位類型、核取方塊類型或提供使用者互動或意見回饋的狀態。如要覆寫這個方法,則必須呼叫其上層實作項目,然後只修改上層類別未設定的屬性。
onInitializeAccessibilityNodeInfo()
(API 級別 14) 此方法為無障礙服務提供檢視區塊狀態的相關資訊。預設的 View 實作具備一組標準檢視屬性,但如果您的自訂檢視區塊提供簡單 TextViewButton 以外的互動控制項,則應覆寫此方法,並將檢視區塊的其他相關資訊設定到由此方法控制的 AccessibilityNodeInfo 物件中。
onRequestSendAccessibilityEvent()
(API 級別 14) 當檢視區塊的子項產生 AccessibilityEvent 時,系統會呼叫這個方法。這個步驟可讓上層檢視區塊修改無障礙功能事件的額外資訊。僅當您的自訂檢視區塊能提供子項檢視區塊,且上層檢視區塊可以提供對無障礙服務非常實用的無障礙功能事件背景資訊時,您才能採用這個方法。

如要為自訂檢視區塊支援無障礙方法,請採用下列其中一種方法:

  • 如果您的應用程式指定 Android 4.0 (API 級別 14) 及以上版本,請直接在自訂檢視區塊類別中覆寫並實作上述無障礙方法。
  • 如果您的自訂檢視區塊與 Android 1.6 (API 級別 4) 及以上版本相容,請在專案中新增 Android 支援資料庫修訂版本 5 或以上版本。然後,在您的自訂檢視區塊類別中,呼叫 ViewCompat.setAccessibilityDelegate() 方法來實作上述無障礙方法。如需這個方法的範例,請參閱 (<sdk>/extras/android/support/v4/samples/Support4Demos/) 中的 Android 支援資料庫 (修訂版本 5 或以上版本) 範例 AccessibilityDelegateSupportActivity

無論是哪一種情況,都應當為自訂檢視區塊類別實作下列無障礙方法:

如要進一步瞭解如何實作這些方法,請參閱填入無障礙功能事件

傳送無障礙功能事件

視乎自訂檢視區塊的具體細節,可能需要在不同時間傳送 AccessibilityEvent 物件,或是針對未由預設實作方式處理的事件傳送該物件。View 類別會為以下事件類型提供預設實作:

注意:遊標懸停事件與「輕觸探索」功能相關聯,該功能會使用這些事件做為觸發條件,為使用者介面元素提供音訊提示。

一般來說,您需要在自訂檢視區塊的內容變更時傳送 AccessibilityEvent。舉例來說,如果您實作自訂滑桿,允許使用者按下向左鍵或向右鍵選取數值,則每當滑桿值變更時,自訂檢視區塊就應當發出 TYPE_VIEW_TEXT_CHANGED 類型事件。下列程式碼範例示範如何使用 sendAccessibilityEvent() 方法報告這個事件。

Kotlin

override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
    return when(keyCode) {
        KeyEvent.KEYCODE_DPAD_LEFT -> {
            currentValue--
            sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED)
            true
        }
        ...
    }
}

Java

@Override
public boolean onKeyUp (int keyCode, KeyEvent event) {
    if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
        currentValue--;
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
        return true;
    }
    ...
}

填入無障礙功能事件

每個 AccessibilityEvent 都有一組用來說明檢視區塊目前狀態的必要屬性。這些屬性包括檢視區塊的類別名稱、內容說明和檢查狀態等資訊。AccessibilityEvent 參考說明文件中說明了各事件類型所需的具體屬性。View 實作會為這些屬性提供預設值。系統會自動提供大部分屬性的值 (包括類別名稱和事件時間戳記)。如果建立自訂檢視區塊元件,您必須提供關於檢視區塊內容和特性的部分資訊。這些資訊可以是簡單的按鈕標籤,但也可以包含您要新增至事件的其他狀態資訊。

如要向具有自訂檢視區塊的無障礙服務提供資訊,您至少要實作 dispatchPopulateAccessibilityEvent()。系統會呼叫這個方法來要求 AccessibilityEvent 的資訊,並讓自訂檢視區塊與 Android 1.6 (API 級別 4) 及以上版本的無障礙服務相容。下列程式碼範例演示了此方法的基本實作。

Kotlin

override fun dispatchPopulateAccessibilityEvent(event: AccessibilityEvent): Boolean {
    // Call the super implementation to populate its text to the event, which
    // calls onPopulateAccessibilityEvent() on API Level 14 and up.
    return super.dispatchPopulateAccessibilityEvent(event).let { completed ->

        // In case this is running on a API revision earlier that 14, check
        // the text content of the event and add an appropriate text
        // description for this custom view:
        if (text?.isNotEmpty() == true) {
            event.text.add(text)
            true
        } else {
            completed
        }
    }
}

Java

@Override
public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
    // Call the super implementation to populate its text to the event, which
    // calls onPopulateAccessibilityEvent() on API Level 14 and up.
    boolean completed = super.dispatchPopulateAccessibilityEvent(event);

    // In case this is running on a API revision earlier that 14, check
    // the text content of the event and add an appropriate text
    // description for this custom view:
    CharSequence text = getText();
    if (!TextUtils.isEmpty(text)) {
        event.getText().add(text);
        return true;
    }
    return completed;
}

對於 Android 4.0 (API 級別 14) 及以上版本,請使用 onPopulateAccessibilityEvent()onInitializeAccessibilityEvent() 方法填入或修改 AccessibilityEvent 中的資訊。請專門使用 onPopulateAccessibilityEvent() 方法新增或修改事件的文字內容,TalkBack 之類的無障礙服務會將文字內容轉換為音訊提示。使用 onInitializeAccessibilityEvent() 方法填入事件的其他資訊,例如檢視區塊的選取狀態。

此外,實作 onInitializeAccessibilityNodeInfo() 方法。無障礙服務會使用由這個方法填入的 AccessibilityNodeInfo 物件,來調查在接收該事件後產生無障礙功能事件的檢視區塊階層,以便取得更詳細的背景資訊,並向使用者提供適當的意見回饋。

下方程式碼範例展示如何使用 ViewCompat.setAccessibilityDelegate() 覆寫這三種方法。請注意,這個程式碼範例要求將 API 級別 4 (修訂版本 5 或以上版本) 的 Android 支援資料庫新增至專案。

Kotlin

ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {

    override fun onPopulateAccessibilityEvent(host: View, event: AccessibilityEvent) {
        super.onPopulateAccessibilityEvent(host, event)
        // We call the super implementation to populate its text for the
        // event. Then we add our text not present in a super class.
        // Very often you only need to add the text for the custom view.
        if (text?.isNotEmpty() == true) {
            event.text.add(text)
        }
    }

    override fun onInitializeAccessibilityEvent(host: View, event: AccessibilityEvent) {
        super.onInitializeAccessibilityEvent(host, event);
        // We call the super implementation to let super classes
        // set appropriate event properties. Then we add the new property
        // (checked) which is not supported by a super class.
        event.isChecked = isChecked()
    }

    override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
        super.onInitializeAccessibilityNodeInfo(host, info)
        // We call the super implementation to let super classes set
        // appropriate info properties. Then we add our properties
        // (checkable and checked) which are not supported by a super class.
        info.isCheckable = true
        info.isChecked = isChecked()
        // Quite often you only need to add the text for the custom view.
        if (text?.isNotEmpty() == true) {
            info.text = text
        }
    }
})

Java

ViewCompat.setAccessibilityDelegate(new AccessibilityDelegateCompat() {
    @Override
    public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
        super.onPopulateAccessibilityEvent(host, event);
        // We call the super implementation to populate its text for the
        // event. Then we add our text not present in a super class.
        // Very often you only need to add the text for the custom view.
        CharSequence text = getText();
        if (!TextUtils.isEmpty(text)) {
            event.getText().add(text);
        }
    }
    @Override
    public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
        super.onInitializeAccessibilityEvent(host, event);
        // We call the super implementation to let super classes
        // set appropriate event properties. Then we add the new property
        // (checked) which is not supported by a super class.
        event.setChecked(isChecked());
    }
    @Override
    public void onInitializeAccessibilityNodeInfo(View host,
            AccessibilityNodeInfoCompat info) {
        super.onInitializeAccessibilityNodeInfo(host, info);
        // We call the super implementation to let super classes set
        // appropriate info properties. Then we add our properties
        // (checkable and checked) which are not supported by a super class.
        info.setCheckable(true);
        info.setChecked(isChecked());
        // Quite often you only need to add the text for the custom view.
        CharSequence text = getText();
        if (!TextUtils.isEmpty(text)) {
            info.setText(text);
        }
    }
}

您可以直接在自訂檢視區塊類別中實作這些方法。如需有關該方法的其他範例,請參閱 (<sdk>/extras/android/support/v4/samples/Support4Demos/) 中的 Android 支援資料庫 (修訂版本 5 或以上版本) 範例 AccessibilityDelegateSupportActivity

提供自訂的無障礙環境

在 Android 4.0 (API 級別 14) 中,我們強化了相關架構,允許無障礙服務檢查產生無障礙功能服務的使用者介面元件包含的檢視區塊階層。這項強化功能可讓無障礙服務提供更豐富的相關背景資訊,以幫助使用者。

在某些情況下,無障礙服務可能無法從檢視區塊階層取得足夠的資訊。例如,包含兩個或以上單獨可點擊區域的自訂介面控制項,如日曆控制項。在這種情況下,由於可點擊的子區段不屬於檢視區塊階層,因此服務無法取得足夠的資訊。

圖 1.含有可選取日期元素的自訂日曆檢視區塊。

在圖 1 所示的範例中,整個日曆將作為單一檢視區塊實作,如果您不執行任何其他操作,無障礙服務將無法收到有關檢視區塊內容和使用者選取內容的足夠資訊。舉例來說,如果使用者點選包含 17 的日期,無障礙功能架構只會收到整個日曆控制項的說明資訊。在這種情況下,TalkBack 無障礙服務只會說出「日曆」音訊,較好的情況下會說出「4 月份日曆」,而使用者無法知道到底選取了哪一天。

為在此類情境下為無障礙服務提供足夠的背景資訊,架構提供了一種方法來指定虛擬檢視區塊階層。應用程式開發人員可以透過虛擬檢視區塊階層,為無障礙服務提供更能符合畫面實際資訊的補充檢視區塊階層。如此一來,無障礙服務可為使用者提供更實用的背景資訊。

另一種可能需要虛擬檢視區塊階層的情況是,使用者介面包含一組控制項 (檢視區塊),其中具有密切相關的函式,對一個控制項執行動作會影響一或多個元素的內容 (例如具有獨立向上和向下按鈕的數字挑選器)。在這種情況下,由於一個控制項上的動作會變更另一個控制項上的內容,且服務無法辨明這些控制項之間的關係,因此無障礙服務將無法取得充足的資訊。為因應這種情況,請將內含檢視區塊的控制項分組在一起,並提供來自這個容器的虛擬檢視區塊階層,以明確呈現控制項提供的資訊和行為。

如要為檢視區塊提供虛擬檢視區塊階層,請覆寫自訂檢視區塊的 getAccessibilityNodeProvider() 方法,或檢視區塊群組並傳回 AccessibilityNodeProvider 的實作。您可以將支援資料庫ViewCompat.getAccessibilityNodeProvider() 方法搭配使用,提供 AccessibilityNodeProviderCompat 的實作項目,以便實作與 Android 1.6 及以上版本相同的虛擬檢視區塊階層。

為了簡化無障礙服務資訊提供及管理無障礙工具的許多層面,建議您實作 ExploreByTouchHelper,也就是提供 AccessibilityNodeProviderCompat附加在資料檢視的 AccessibilityDelegateCompat。如需範例,請參閱 ExploreByTouchHelperActivityCalendarView 這類架構小工具也會透過其子項檢視 SimpleMonthView 使用 ExploreByTouchHelper

處理自訂觸控事件

自訂檢視區塊控制項可能需要非標準觸控事件行為,如以下範例所示。

定義點擊型動作

如果您的小工具使用 OnClickListenerOnLongClickListener 或介面,系統會處理 ACTION_CLICKACTION_LONG_CLICK 動作。但是,如果應用程式使用依賴於 OnTouchListener 介面的自訂化小工具,您就需要針對點擊型無障礙動作定義自訂處理常式。方法是為每個動作呼叫 replaceAccessibilityAction() 方法,如以下程式碼片段所示:

Kotlin

// Assumes that the widget is designed to select text when tapped and select
// all text when long-tapped. In its strings.xml file, this app has set
// "select" to "Select" and "select_all" to "Select all", respectively.
ViewCompat.replaceAccessibilityAction(
            WIDGET,
            ACTION_CLICK,
            context.getString(R.string.select)
) { view, commandArguments ->
    selectText()
}

ViewCompat.replaceAccessibilityAction(
            WIDGET,
            ACTION_LONG_CLICK,
            context.getString(R.string.select_all)
) { view, commandArguments ->
    selectAllText()
}

Java

// Assumes that the widget is designed to select text when tapped and select
// all text when long-tapped. In its strings.xml file, this app has set
// "select" to "Select" and "select_all" to "Select all", respectively.
ViewCompat.replaceAccessibilityAction(WIDGET, ACTION_CLICK,
        context.getString(R.string.select),
        (view, commandArguments) -> {
            selectText();
        });

ViewCompat.replaceAccessibilityAction(WIDGET, ACTION_LONG_CLICK,
        context.getString(R.string.select_all),
        (view, commandArguments) -> {
            selectAllText();
        });

建立自訂點擊事件

自訂控制項可以使用 onTouchEvent(MotionEvent) 事件監聽器偵測 ACTION_DOWNACTION_UP 事件,並觸發特殊點擊事件。為維持與無障礙服務的相容性,處理該自訂點擊事件的程式碼必須執行以下操作:

  1. 針對已解讀的點擊動作產生適當的 AccessibilityEvent
  2. 啟用無障礙服務,為無法使用觸控螢幕的使用者執行自訂點擊動作。

為有效處理這些需求,您的程式碼應覆寫 performClick() 方法,此過程必須呼叫該方法的上層實作方式,然後執行點擊事件要求的任何動作。偵測到自訂點擊動作時,該程式碼應呼叫 performClick() 方法。以下程式碼範例對這一模式進行了示範。

Kotlin

class CustomTouchView(context: Context) : View(context) {

    var downTouch = false

    override fun onTouchEvent(event: MotionEvent): Boolean {
        super.onTouchEvent(event)

        // Listening for the down and up touch events
        return when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                downTouch = true
                true
            }

            MotionEvent.ACTION_UP -> if (downTouch) {
                downTouch = false
                performClick() // Call this method to handle the response, and
                // thereby enable accessibility services to
                // perform this action for a user who cannot
                // click the touchscreen.
                true
            } else {
                false
            }

            else -> false  // Return false for other touch events
        }
    }

    override fun performClick(): Boolean {
        // Calls the super implementation, which generates an AccessibilityEvent
        // and calls the onClick() listener on the view, if any
        super.performClick()

        // Handle the action for the custom click here

        return true
    }
}

Java

class CustomTouchView extends View {

    public CustomTouchView(Context context) {
        super(context);
    }

    boolean downTouch = false;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);

        // Listening for the down and up touch events
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downTouch = true;
                return true;

            case MotionEvent.ACTION_UP:
                if (downTouch) {
                    downTouch = false;
                    performClick(); // Call this method to handle the response, and
                                    // thereby enable accessibility services to
                                    // perform this action for a user who cannot
                                    // click the touchscreen.
                    return true;
                }
        }
        return false; // Return false for other touch events
    }

    @Override
    public boolean performClick() {
        // Calls the super implementation, which generates an AccessibilityEvent
        // and calls the onClick() listener on the view, if any
        super.performClick();

        // Handle the action for the custom click here

        return true;
    }
}

上述模式使用 performClick() 方法產生無障礙功能事件,並為無障礙服務提供進入點,以代表使用者執行此自訂點擊事件,從而確保自訂點擊事件與無障礙服務相容。

注意:如果您的自訂檢視區塊有不同的可點擊區域 (例如自訂日曆檢視區塊),則必須覆寫自訂檢視區塊中的 getAccessibilityNodeProvider(),實作虛擬檢視區塊階層,以便與無障礙服務相容。