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

アプリでカスタムビュー コンポーネントが必要な場合、そのビューのユーザー補助機能を強化するために、追加の作業が必要になります。カスタムビューのユーザー補助機能を強化する主な作業は次のとおりです。

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

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

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

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

ユーザー補助イベントは、アプリ内の視覚的インターフェース コンポーネントに対するユーザーの操作に関する通知です。こうした通知を扱うのがユーザー補助サービスで、そのイベントの情報を使用して補足説明のフィードバックやプロンプトを出力します。Android 4.0(API レベル 14)以上では、ユーザー補助イベントを生成するメソッドが拡張され、Android 1.6(API レベル 4)で導入されていた AccessibilityEventSource インターフェースよりも詳しい情報が提供されるようになりました。拡張されたユーザー補助メソッドは、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() メソッドを呼び出して、上記のユーザー補助メソッドを実装する必要があります。この方法の例については、Android サポート ライブラリ(リビジョン 5 以上)の AccessibilityDelegateSupportActivity のサンプル(<sdk>/extras/android/support/v4/samples/Support4Demos/ 内)をご覧ください。

どちらの場合も、カスタムビュー クラスに以下のユーザー補助メソッドを実装する必要があります。

これらのメソッドの実装について詳しくは、ユーザー補助イベントを実装する方法に関する説明をご覧ください。

ユーザー補助イベントを送信する

カスタムビューの詳細に応じて、AccessibilityEvent オブジェクトの送信が必要になるタイミングや、対象となる、デフォルト実装では処理されないイベントが異なります。View クラスには以下のイベントタイプのデフォルト実装が提供されています。

注: Hover イベントは、タッチガイド機能と関連しています。タッチイベント機能は、こうしたイベントをトリガーとして使って、ユーザー インターフェース要素の音声プロンプトを提供します。

通常、カスタムビューの内容が変わるたびに、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() を使用することで上記の 3 つのメソッドをオーバーライドする方法を示します。このサンプルコードでは、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);
        }
    }
}

これらのメソッドは、カスタムビュー クラス内に直接実装できます。この方法の別の例については、Android サポート ライブラリ(リビジョン 5 以上)の AccessibilityDelegateSupportActivity のサンプル(<sdk>/extras/android/support/v4/samples/Support4Demos/ 内)をご覧ください。

ユーザー補助コンテキストをカスタマイズして提供する

Android 4.0(API レベル 14)ではフレームワークが拡張され、ユーザー補助イベントを生成するユーザー インターフェース コンポーネントのビュー階層を、ユーザー補助サービスが調べられるようになりました。この拡張により、ユーザー補助サービスは今までよりはるかに豊富なコンテキスト情報を提供して、ユーザーをサポートできます。

場合によっては、ユーザー補助サービスがビュー階層から十分な情報を取得できないことがあります。一例を挙げると、カレンダーの操作のように、クリック可能な別個の領域が複数あるようなカスタム インターフェース コントロールの場合です。この場合、クリック可能なサブセクションはビュー階層を構成してはいないため、サービスは適切な情報を取得できません。

図 1. 日にちの要素を選択可能なカスタム カレンダーのビュー

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

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

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

ビューの仮想ビュー階層を指定するには、カスタムビュー内またはビューグループ内で getAccessibilityNodeProvider() メソッドをオーバーライドし、AccessibilityNodeProvider の実装を返します。このユーザー補助機能の実装例については、ApiDemos サンプル プロジェクト内の AccessibilityNodeProviderActivity をご覧ください。Android 1.6 以降と互換性のある仮想ビュー階層を実装するには、ViewCompat.getAccessibilityNodeProvider() メソッドを含むサポート ライブラリを使用し、AccessibilityNodeProviderCompat を使った実装を提供します。

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

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

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

ウィジェットが OnClickListener または OnLongClickListener インターフェースを使用する場合、システムは ACTION_CLICK および ACTION_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_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
                // 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() をオーバーライドして仮想ビュー階層を実装する必要があります。