提高自訂檢視區塊的無障礙程度

如果您的應用程式需要自訂檢視區塊元件,請務必改善檢視區塊的無障礙程度。下述步驟可提高自訂檢視區塊的無障礙程度:

  • 處理方向控制器的點擊動作。
  • 實作無障礙 API 方法。
  • 傳送自訂檢視區塊的專屬 AccessibilityEvent 物件。
  • 為檢視區塊填入 AccessibilityEventAccessibilityNodeInfo

處理方向控制器的點擊動作

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

自訂控制項須將 KEYCODE_ENTER 事件視為與 KEYCODE_DPAD_CENTER 相同,讓使用者更輕鬆全程使用鍵盤。

實作無障礙 API 方法

無障礙功能事件是關於使用者與應用程式視覺介面元件互動的訊息。無障礙服務會處理這些訊息,並使用這些事件中的資訊產生補充回饋和提示。無障礙方法屬於 ViewView.AccessibilityDelegate 類別,列舉如下:

dispatchPopulateAccessibilityEvent()
當自訂檢視區塊產生無障礙功能事件時,系統會呼叫這個方法。這個方法的預設實作方式會為檢視區塊呼叫 onPopulateAccessibilityEvent(),然後為檢視區塊中的每個子項呼叫 dispatchPopulateAccessibilityEvent() 方法。
onInitializeAccessibilityEvent()
如要針對並非只含文字內容的檢視區塊取得其他狀態資訊,系統會呼叫此方法。如果自訂檢視區塊提供的互動控制項不只是 TextViewButton,請覆寫此方法,並使用此方法設定檢視區塊的其他資訊,例如密碼欄位類型、核取方塊類型,或為事件提供使用者互動或回饋的狀態。覆寫此方法時,請呼叫其上層實作方式,並只修改上層類別未設定的屬性。
onInitializeAccessibilityNodeInfo()
此方法會為無障礙服務提供檢視區塊狀態的資訊。預設的 View 實作方式具備一組標準檢視區塊屬性,但如果您的自訂檢視區塊提供的互動控制項不只是 TextViewButton,則請覆寫此方法,並將檢視區塊的其他資訊設定到由此方法控制的 AccessibilityNodeInfo 物件中。
onPopulateAccessibilityEvent()
此方法會為檢視區塊設定 AccessibilityEvent 的語音文字提示。如果檢視區塊是產生無障礙功能事件該檢視區塊的子項,系統也會呼叫此方法。
onRequestSendAccessibilityEvent()
當檢視區塊的子項產生 AccessibilityEvent 時,系統會呼叫此方法。這個步驟可讓上層檢視區塊以額外資訊修改無障礙功能事件。請只在以下情況實作這個方法:自訂檢視區塊可提供子檢視區塊,且上層檢視區塊可提供對無障礙服務非常實用的無障礙功能事件背景資訊。
sendAccessibilityEvent()
當使用者對檢視區塊執行動作時,系統會呼叫此方法。這種事件會依使用者動作類型分類,例如 TYPE_VIEW_CLICKED。一般來說,只要自訂檢視區塊內容有所變更,就必須傳送 AccessibilityEvent
sendAccessibilityEventUnchecked()
當呼叫程式碼需要直接控制用於檢查裝置是否已啟用無障礙功能的 AccessibilityManager.isEnabled() 時,系統會使用此方法。實作此方法時,無論系統設定為何,請一律視為無障礙功能已啟用並執行呼叫。一般來說,您不需要為自訂檢視區塊實作此方法。

如要支援無障礙功能,請直接在自訂檢視區塊類別中覆寫並實作上述無障礙方法。

請至少為自訂檢視區塊類別實作下列無障礙方法:

  • dispatchPopulateAccessibilityEvent()
  • onInitializeAccessibilityEvent()
  • onInitializeAccessibilityNodeInfo()
  • onPopulateAccessibilityEvent()

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

傳送無障礙功能事件

視自訂檢視區塊的具體情況而定,可能需要在不同時間傳送 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 實作方式會為這些必要屬性提供預設值。系統會自動提供大部分的值,包括類別名稱和事件時間戳記。如要建立自訂檢視區塊元件,您必須提供關於檢視區塊內容和特性的資訊。這些資訊可以像按鈕標籤一樣簡單,也可以包含您想加入事件的其他狀態資訊。

如要填入或修改 AccessibilityEvent 中的資訊,請使用 onPopulateAccessibilityEvent()onInitializeAccessibilityEvent() 方法。請特別使用 onPopulateAccessibilityEvent() 方法新增或修改事件的文字內容,這些文字內容會由 TalkBack 等無障礙服務轉換為音訊提示。使用 onInitializeAccessibilityEvent() 方法則可填入事件的其他資訊,例如檢視區塊的選取狀態。

此外,請實作 onInitializeAccessibilityNodeInfo() 方法。無障礙服務會使用由這個方法填入的 AccessibilityNodeInfo 物件,調查在接收後產生無障礙功能事件的檢視區塊階層,並向使用者提供適當的回饋。

以下程式碼範例說明如何在檢視區塊中覆寫這三種方法:

Kotlin

override fun onPopulateAccessibilityEvent(event: AccessibilityEvent?) {
    super.onPopulateAccessibilityEvent(event)
    // Call the super implementation to populate its text for the
    // event. Then, add text not present in a super class.
    // You typically only need to add the text for the custom view.
    if (text?.isNotEmpty() == true) {
        event?.text?.add(text)
    }
}

override fun onInitializeAccessibilityEvent(event: AccessibilityEvent?) {
    super.onInitializeAccessibilityEvent(event)
    // Call the super implementation to let super classes
    // set appropriate event properties. Then, add the new checked
    // property that is not supported by a super class.
    event?.isChecked = isChecked()
}

override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo?) {
    super.onInitializeAccessibilityNodeInfo(info)
    // Call the super implementation to let super classes set
    // appropriate info properties. Then, add the checkable and checked
    // properties that are not supported by a super class.
    info?.isCheckable = true
    info?.isChecked = isChecked()
    // You typically only need to add the text for the custom view.
    if (text?.isNotEmpty() == true) {
        info?.text = text
    }
}

Java

@Override
public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
    super.onPopulateAccessibilityEvent(event);
    // Call the super implementation to populate its text for the
    // event. Then, add the text not present in a super class.
    // You typically 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(AccessibilityEvent event) {
    super.onInitializeAccessibilityEvent(event);
    // Call the super implementation to let super classes
    // set appropriate event properties. Then, add the new checked
    // property that is not supported by a super class.
    event.setChecked(isChecked());
}

@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
    super.onInitializeAccessibilityNodeInfo(info);
    // Call the super implementation to let super classes set
    // appropriate info properties. Then, add the checkable and checked
    // properties that are not supported by a super class.
    info.setCheckable(true);
    info.setChecked(isChecked());
    // You typically only need to add the text for the custom view.
    CharSequence text = getText();
    if (!TextUtils.isEmpty(text)) {
        info.setText(text);
    }
}

您可以直接在自訂檢視區塊類別中實作這些方法。

提供自訂的無障礙環境

無障礙服務可以針對產生無障礙功能事件的使用者介面元件,檢查其中所含的檢視區塊階層,讓無障礙服務提供更豐富的背景資訊,藉此協助使用者。

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

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

在圖 1 的範例中,系統是以單一檢視區塊實作整個日曆,因此除非開發人員提供額外資訊,否則無障礙服務無法透過檢視區塊充分接收有關檢視區塊內容和使用者選取項目的資訊。舉例來說,如果使用者點選標示為 17 的日期,無障礙架構只會接收整個日曆控制項的說明資訊。在這種情況下,TalkBack 無障礙服務會說出「日曆」或「4 月日曆」,使用者便無法知道所選取的日期。

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

以下情況也可能需要虛擬檢視區塊階層:使用者介面包含一組 View 控制項,且其中的函式密切相關,只要對某一控制項執行動作,就會影響一或多個元素的內容。這類例子包括具有獨立向上和向下按鈕的數字挑選器。在這種情況下,對控制項執行的動作會變更另一個控制項的內容,無障礙服務可能無法明確區分這些控制項之間的關係,因此無法取得充足的資訊。

為因應這種情況,請將內含檢視區塊的控制項分入同一組,並從此容器提供虛擬檢視區塊階層,明確呈現控制項提供的資訊和行為。

如要為檢視區塊提供虛擬檢視區塊階層,請覆寫自訂檢視區塊或檢視區塊群組的 getAccessibilityNodeProvider() 方法,並傳回 AccessibilityNodeProvider 的實作方式。您可以將支援資料庫與 ViewCompat.getAccessibilityNodeProvider() 方法搭配使用,並透過 AccessibilityNodeProviderCompat 提供實作方式,藉此實作虛擬檢視區塊階層。

如要簡化為無障礙服務提供資訊及管理無障礙服務焦點的工作,可以改為實作 ExploreByTouchHelper。這可以提供 AccessibilityNodeProviderCompat,並透過呼叫 setAccessibilityDelegate 附加至檢視區塊的 AccessibilityDelegateCompat。如需範例,請參閱 ExploreByTouchHelperActivityCalendarView 這類架構小工具也會透過子項檢視區塊 SimpleMonthView 使用 ExploreByTouchHelper

處理自訂觸控事件

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

定義點選型動作

如果小工具使用 OnClickListenerOnLongClickListener 介面,系統可以自行處理 ACTION_CLICKACTION_LONG_CLICK 動作。如果應用程式使用的小工具比較傾向自訂性質,需透過 OnTouchListener 介面運作,您就要定義點選型無障礙動作的自訂處理常式,方法是為每個動作呼叫 replaceAccessibilityAction() 方法,如以下程式碼片段所示:

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    ...

    // Assumes that the widget is designed to select text when tapped, and selects
    // all text when tapped and held. In its strings.xml file, this app sets
    // "select" to "Select" and "select_all" to "Select all".
    ViewCompat.replaceAccessibilityAction(
        binding.textSelectWidget,
        ACTION_CLICK,
        getString(R.string.select)
    ) { view, commandArguments ->
        selectText()
    }

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

Java

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...

    // Assumes that the widget is designed to select text when tapped, and select
    // all text when tapped and held. In its strings.xml file, this app sets
    // "select" to "Select" and "select_all" to "Select all".
    ViewCompat.replaceAccessibilityAction(
            binding.textSelectWidget,
            ACTION_CLICK,
            getString(R.string.select),
            (view, commandArguments) -> selectText());

    ViewCompat.replaceAccessibilityAction(
            binding.textSelectWidget,
            ACTION_LONG_CLICK,
            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
                // enable accessibility services to
                // perform this action for a user who can't
                // tap 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
                                    // enable accessibility services to
                                    // perform this action for a user who can't
                                    // tap 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() 方法產生無障礙功能事件,並為無障礙服務提供進入點,代表使用者執行此自訂點擊事件。這樣一來,即可協助確保自訂點擊事件與無障礙服務相容。