カスタムビューのユーザー補助機能を強化する

アプリでカスタムビュー コンポーネントが必要な場合、そのビューのユーザー補助機能を強化する必要があります。カスタムビューのユーザー補助機能は、このページに記載しているように、次の手順で強化できます。

  • 方向コントローラのクリックを処理する
  • Accessibility API のメソッドを実装する
  • カスタムビュー固有の AccessibilityEvent オブジェクトを送信する
  • ビューの AccessibilityEventAccessibilityNodeInfo を実装する

方向コントローラのクリックを処理する

ほとんどのデバイスでは、方向コントローラを使用してビューをクリックすると、現在フォーカスのあるビューに KeyEventKEYCODE_DPAD_CENTER が送信されます。KEYCODE_DPAD_CENTER は、Android のすべての標準ビューで適切に処理されます。カスタム View コントロールを作成する場合は、このイベントの効果が、タッチスクリーン上でのビューのタップと同じになるようにしてください。

カスタム コントロールでは、KEYCODE_ENTER イベントを KEYCODE_DPAD_CENTER と同じように扱う必要があります。この方法により、フルキーボードからの操作が、ユーザーにとってかなり簡単になります。

Accessibility API のメソッドを実装する

ユーザー補助イベントは、アプリの視覚的インターフェース コンポーネントに対するユーザーの操作に関する通知です。こうした通知を扱うのがユーザー補助サービスで、そのイベントの情報を使用して補足説明のフィードバックやプロンプトを出力します。ユーザー補助メソッドは、View クラスだけでなく、View.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 オブジェクトにデータが入力されます。ユーザー補助サービスはこのオブジェクトを使って、ユーザー補助イベントの受信後にそのイベントを生成したビュー階層を調査し、適切なフィードバックをユーザーに提供します。

下記のサンプルコードでは、ビューで上記の 3 つのメソッドをオーバーライドする方法を示します。

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 の例では、カレンダー全体が 1 つのビューとして実装されているので、デベロッパーが追加の情報を提供しない限り、ビューのコンテンツと、ビュー内でのユーザーの選択に関する十分な情報が、ユーザー補助サービスに提供されません。たとえば、ユーザーが 17 の日にちをクリックしても、ユーザー補助機能のフレームワークが受け取るのは、カレンダー全体のコントロールを説明する情報だけです。この場合、TalkBack ユーザー補助サービスは単に「カレンダー」または「4 月のカレンダー」と通知するだけで、どの日にちが選択されたかユーザーにはわかりません。

このような状況でユーザー補助サービスに十分なコンテキスト情報を提供するために、フレームワークには仮想ビュー階層を指定する方法があります。仮想ビュー階層とは、アプリのデベロッパーが補助的なビュー階層を指定して、画面上に実際に表示される情報により近い構造をユーザー補助サービスに知らせる方法です。この方法により、ユーザー補助サービスはユーザーにさらに役立つコンテキスト情報を提供できます。

そのほか、仮想ビュー階層が必要と考えられる状況としては、ユーザー インターフェースに機能と密接に関連する一連の View コントロールがあって、そのうちの 1 つのコントロールでのアクションが、他の要素の内容に影響を与える場合があります。たとえば、上矢印と下矢印の 2 つのボタンで数を選択するツールです。この場合、ユーザー補助サービスは十分な情報を取得できません。一方のコントロールのアクションによって別のコントロールのコンテンツが変更されますが、そのコントロール間の関係がユーザー補助サービスには明らかではない可能性があるからです。

この状況に対処するには、関連する複数のコントロールとそれらを含むビューをグループにまとめて、このコンテナから仮想ビュー階層を指定し、コントロールが提供する情報と動作を明示します。

ビューの仮想ビュー階層を指定するには、カスタムビュー内またはビューグループ内で getAccessibilityNodeProvider() メソッドをオーバーライドし、AccessibilityNodeProvider の実装を返します。仮想ビュー階層を実装するには、ViewCompat.getAccessibilityNodeProvider() メソッドを含むサポート ライブラリを使用し、AccessibilityNodeProviderCompat を使った実装を提供します。

ユーザー補助サービスへの情報提供とユーザー補助フォーカスの管理を簡略化するには、代わりに ExploreByTouchHelper を実装します。AccessibilityNodeProviderCompat を提供し、setAccessibilityDelegate を呼び出すことでビューの AccessibilityDelegateCompat として接続できます。例として、ExploreByTouchHelperActivity をご覧ください。ExploreByTouchHelper は、子ビュー SimpleMonthView を介して CalendarView などのフレームワーク ウィジェットでも使用されます。

カスタム タッチイベントを処理する

次の例に示すように、カスタムビュー コントロールでは、標準的でないタッチイベント動作が必要になる場合があります。

クリックベースのアクションを定義する

ウィジェットが OnClickListener または OnLongClickListener インターフェースを使用する場合、システムは ACTION_CLICK および ACTION_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_DOWN イベントと ACTION_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() メソッドを使用してユーザー補助イベントを生成し、かつ、このカスタム クリック イベントをユーザーの代わりに実行するユーザー補助サービスのエントリ ポイントを提供しています。