web-dev-qa-db-ja.com

ConstraintLayoutでアクセス可能なフォーカスグループを作成する方法

3つのLinearLayoutartist, song and albumを含むRelativeLayout内にTextViewsがあるとします。

<RelativeLayout
    ...
    <LinearLayout
        Android:id="@id/text_view_container"
        Android:layout_width="warp_content"
        Android:layout_height="wrap_content"
        Android:orientation="vertical">

        <TextView
            Android:id="@id/artist"
            Android:layout_width="wrap_content"
            Android:layout_height="wrap_content"
            Android:text="Artist"/>

        <TextView
            Android:id="@id/song"
            Android:layout_width="wrap_content"
            Android:layout_height="wrap_content"
            Android:text="Song"/>

        <TextView
            Android:id="@id/album"
            Android:layout_width="wrap_content"
            Android:layout_height="wrap_content"
            Android:text="album"/>
    </LinearLayout>

    <TextView
        Android:id="@id/unrelated_textview1/>
    <TextView
        Android:id="@id/unrelated_textview2/>
    ...
</RelativeLayout>        

TalkbackReaderをアクティブにしてTextViewLinearLayoutをクリックすると、TalkbackReaderは「アーティスト」、「曲」OR「アルバム」などのようになります。 。

ただし、次のコマンドを使用すると、最初の3 TextViewsをフォーカスグループに入れることができます。

<LinearLayout
    Android:focusable="true
    ...

これで、TalkbackReaderは「アーティストソングアルバム」を読み上げます。

2 unrelated TextViewsstillは単独で読み込まれません。これは私が実現したい動作です。

(参考として Google codelabsの例 を参照)

私はConstrainLayoutでこの動作を再現しようとしていますが、方法はわかりません。

<ConstraintLayout>
    <TextView artist/>
    <TextView song/>
    <TextView album/>
    <TextView unrelated_textview1/>
    <TextView unrelated_textview2/>
</ConstraintLayout>

ウィジェットを「グループ」に入れても機能しないようです。

<Android.support.constraint.Group
    Android:id="@+id/group"
    Android:layout_width="wrap_content"
    Android:layout_height="wrap_content"
    Android:focusable="true"
    Android:importantForAccessibility="yes"
    app:constraint_referenced_ids="artist,song,album"
    />

では、ConstrainLayoutでアクセシビリティ用のフォーカスグループを再作成するにはどうすればよいですか?

[EDIT]:ソリューションを作成する唯一の方法は、外側のConstraintLayoutで "focusable = true"を使用することであるようです/またはビュー自体の "focusable = false"。これには、キーボードナビゲーション/スイッチボックスを扱うときに考慮すべきいくつかの欠点があります。

https://github.com/googlecodelabs/Android-accessibility/issues/4

23
hamena314

ViewGroupsに基づくフォーカスグループは引き続きConstraintLayout内で機能するため、LinearLayoutsおよびRelativeLayoutsConstraintLayoutsに置き換えても、TalkBackは機能します予想通り。しかし、nestingViewGroups内のConstraintLayoutを避けようとする場合は、フラットビュー階層の設計目標に合わせて、ここに方法がありますそれを行うには。

直接言及しているフォーカスTextViewsからViewGroupをトップレベルのConstraintLayoutに移動します。次に、View制約を使用して、これらのTextViewsの上に単純な透明ConstraintLayoutを配置します。各TextViewはトップレベルのConstraintLayoutのメンバーになるため、レイアウトはフラットになります。オーバーレイはTextViewsの上にあるため、基礎となるTextViewsの前にすべてのタッチイベントを受け取ります。レイアウト構造は次のとおりです。

_<ConstaintLayout>
    <TextView>
    <TextView>
    <TextView>
    <View> [overlays the above TextViews]
</ConstraintLayout>
_

基礎となる各TextViewsのテキストの組み合わせであるオーバーレイのコンテンツの説明を手動で指定できるようになりました。各TextViewがフォーカスを受け入れて独自のテキストを読み上げるのを防ぐために、_Android:importantForAccessibility="no"_を設定します。オーバーレイビューにタッチすると、TextViewsを組み合わせたテキストが読み上げられます。

上記は一般的なソリューションですが、物事を自動的に管理するカスタムオーバーレイビューの実装がより良いでしょう。以下に示すカスタムオーバーレイは、GroupConstraintLayoutヘルパーの一般的な構文に従い、上で概説した処理の多くを自動化します。

カスタムオーバーレイは次のことを行います。

  1. GroupConstraintLayoutヘルパーのようなコントロールによってグループ化されるIDのリストを受け入れます。
  2. 各ビューでView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO)を設定して、グループ化されたコントロールのアクセシビリティを無効にします。 (これにより、これを手動で行う必要がなくなります。)
  3. カスタムコントロールをクリックすると、グループ化されたビューのテキストの連結がアクセシビリティフレームワークに表示されます。ビュー用に収集されるテキストは、contentDescriptiongetText()またはhintのいずれかです。 (これにより、これを手動で行う必要がなくなります。もう1つの利点は、アプリの実行中にテキストに加えられた変更もすべて取得できることです。)

TextViewsをオーバーレイするには、オーバーレイビューをレイアウトXML内に手動で配置する必要があります。

これは、質問で言及されたViewGroupアプローチとカスタムオーバーレイを示すサンプルレイアウトです。左側のグループは、埋め込みViewGroupの使用を示す従来のConstraintLayoutアプローチです。右は、カスタムコントロールを使用したオーバーレイメソッドです。上部の「初期フォーカス」というラベルの付いたTextViewは、2つの方法を簡単に比較できるように初期フォーカスをキャプチャするためのものです。

ConstraintLayoutを選択すると、TalkBackは「アーティスト、曲、アルバム」を話します。

enter image description here

カスタムビューオーバーレイを選択すると、TalkBackは「アーティスト、曲、アルバム」も読み上げます。

enter image description here

以下は、カスタムビューのサンプルレイアウトとコードです。 警告:このカスタムビューはTextViewsを使用して指定された目的で機能しますが、従来の方法に代わる堅牢なものではありません。例:カスタムオーバーレイは、TextViewなどのEditTextを拡張するビュータイプのテキストを読み上げますが、従来のメソッドは読み上げません。

GitHubの サンプルプロジェクト を参照してください。

activity_main.xml

_<Android.support.constraint.ConstraintLayout 
    Android:id="@+id/layout"
    Android:layout_width="match_parent"
    Android:layout_height="match_parent">

    <Android.support.constraint.ConstraintLayout
        Android:id="@+id/viewGroup"
        Android:layout_width="0dp"
        Android:layout_height="wrap_content"
        Android:focusable="true"
        Android:gravity="center_horizontal"
        app:layout_constraintEnd_toStartOf="@+id/guideline"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/viewGroupHeading">

        <TextView
            Android:id="@+id/artistText"
            Android:layout_width="wrap_content"
            Android:layout_height="wrap_content"
            Android:text="Artist"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            Android:id="@+id/songText"
            Android:layout_width="wrap_content"
            Android:layout_height="wrap_content"
            Android:layout_marginTop="16dp"
            Android:text="Song"
            app:layout_constraintStart_toStartOf="@+id/artistText"
            app:layout_constraintTop_toBottomOf="@+id/artistText" />

        <TextView
            Android:id="@+id/albumText"
            Android:layout_width="wrap_content"
            Android:layout_height="wrap_content"
            Android:layout_marginTop="16dp"
            Android:text="Album"
            app:layout_constraintStart_toStartOf="@+id/songText"
            app:layout_constraintTop_toBottomOf="@+id/songText" />

    </Android.support.constraint.ConstraintLayout>

    <TextView
        Android:id="@+id/artistText2"
        Android:layout_width="wrap_content"
        Android:layout_height="wrap_content"
        Android:text="Artist"
        app:layout_constraintBottom_toTopOf="@+id/songText2"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="@+id/guideline"
        app:layout_constraintTop_toTopOf="@+id/viewGroup" />

    <TextView
        Android:id="@+id/songText2"
        Android:layout_width="wrap_content"
        Android:layout_height="wrap_content"
        Android:layout_marginTop="16dp"
        Android:text="Song"
        app:layout_constraintStart_toStartOf="@id/artistText2"
        app:layout_constraintTop_toBottomOf="@+id/artistText2" />

    <TextView
        Android:id="@+id/albumText2"
        Android:layout_width="wrap_content"
        Android:layout_height="wrap_content"
        Android:layout_marginTop="16dp"
        Android:text="Album"
        app:layout_constraintStart_toStartOf="@+id/artistText2"
        app:layout_constraintTop_toBottomOf="@+id/songText2" />

    <com.example.constraintlayoutaccessibility.AccessibilityOverlay
        Android:id="@+id/overlay"
        Android:layout_width="0dp"
        Android:layout_height="0dp"
        Android:focusable="true"
        app:accessible_group="artistText2, songText2, albumText2, editText2, button2"
        app:layout_constraintBottom_toBottomOf="@+id/albumText2"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@id/guideline"
        app:layout_constraintTop_toTopOf="@id/viewGroup" />

    <Android.support.constraint.Guideline
        Android:id="@+id/guideline"
        Android:layout_width="wrap_content"
        Android:layout_height="wrap_content"
        Android:orientation="vertical"
        app:layout_constraintGuide_percent="0.5" />

    <TextView
        Android:id="@+id/viewGroupHeading"
        Android:layout_width="wrap_content"
        Android:layout_height="wrap_content"
        Android:layout_marginTop="16dp"
        Android:importantForAccessibility="no"
        Android:text="ViewGroup"
        Android:textAppearance="@style/TextAppearance.AppCompat.Medium"
        Android:textStyle="bold"
        app:layout_constraintEnd_toStartOf="@+id/guideline"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView4" />

    <TextView
        Android:id="@+id/overlayHeading"
        Android:layout_width="wrap_content"
        Android:layout_height="wrap_content"
        Android:importantForAccessibility="no"
        Android:text="Overlay"
        Android:textAppearance="@style/TextAppearance.AppCompat.Medium"
        Android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="@+id/guideline"
        app:layout_constraintTop_toTopOf="@+id/viewGroupHeading" />

    <TextView
        Android:id="@+id/textView4"
        Android:layout_width="wrap_content"
        Android:layout_height="wrap_content"
        Android:layout_marginStart="8dp"
        Android:layout_marginTop="8dp"
        Android:layout_marginEnd="8dp"
        Android:text="Initial focus"
        app:layout_constraintEnd_toStartOf="@+id/guideline"
        app:layout_constraintStart_toStartOf="@+id/guideline"
        app:layout_constraintTop_toTopOf="parent" />

</Android.support.constraint.ConstraintLayout>
_

AccessibilityOverlay.Java

_public class AccessibilityOverlay extends View {
    private int[] mAccessibleIds;

    public AccessibilityOverlay(Context context) {
        super(context);
        init(context, null, 0, 0);
    }

    public AccessibilityOverlay(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs, 0, 0);
    }

    public AccessibilityOverlay(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs, defStyleAttr, 0);
    }

    @TargetApi(Build.VERSION_CODES.Lollipop)
    public AccessibilityOverlay(Context context, @Nullable AttributeSet attrs,
                                int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context, attrs, defStyleAttr, defStyleRes);
    }

    private void init(Context context, @Nullable AttributeSet attrs,
                      int defStyleAttr, int defStyleRes) {
        String accessibleIdString;

        TypedArray a = context.getTheme().obtainStyledAttributes(
            attrs,
            R.styleable.AccessibilityOverlay,
            defStyleAttr, defStyleRes);

        try {
            accessibleIdString = a.getString(R.styleable.AccessibilityOverlay_accessible_group);
        } finally {
            a.recycle();
        }
        mAccessibleIds = extractAccessibleIds(context, accessibleIdString);
    }

    @NonNull
    private int[] extractAccessibleIds(@NonNull Context context, @Nullable String idNameString) {
        if (TextUtils.isEmpty(idNameString)) {
            return new int[]{};
        }
        String[] idNames = idNameString.split(ID_DELIM);
        int[] resIds = new int[idNames.length];
        Resources resources = context.getResources();
        String packageName = context.getPackageName();
        int idCount = 0;
        for (String idName : idNames) {
            idName = idName.trim();
            if (idName.length() > 0) {
                int resId = resources.getIdentifier(idName, ID_DEFTYPE, packageName);
                if (resId != 0) {
                    resIds[idCount++] = resId;
                }
            }
        }
        return resIds;
    }

    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();

        View view;
        ViewGroup parent = (ViewGroup) getParent();
        for (int id : mAccessibleIds) {
            if (id == 0) {
                break;
            }
            view = parent.findViewById(id);
            if (view != null) {
                view.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
            }
        }
    }

    @Override
    public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
        super.onPopulateAccessibilityEvent(event);

        int eventType = event.getEventType();
        if (eventType == AccessibilityEvent.TYPE_VIEW_SELECTED ||
            eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED &&
                getContentDescription() == null) {
            event.getText().add(getAccessibilityText());
        }
    }

    @NonNull
    private String getAccessibilityText() {
        ViewGroup parent = (ViewGroup) getParent();
        View view;
        StringBuilder sb = new StringBuilder();

        for (int id : mAccessibleIds) {
            if (id == 0) {
                break;
            }
            view = parent.findViewById(id);
            if (view != null && view.getVisibility() == View.VISIBLE) {
                CharSequence description = view.getContentDescription();

                // This misbehaves if the view is an EditText or Button or otherwise derived
                // from TextView by voicing the content when the ViewGroup approach remains
                // silent.
                if (TextUtils.isEmpty(description) && view instanceof TextView) {
                    TextView tv = (TextView) view;
                    description = tv.getText();
                    if (TextUtils.isEmpty(description)) {
                        description = tv.getHint();
                    }
                }
                if (description != null) {
                    sb.append(",");
                    sb.append(description);
                }
            }
        }
        return (sb.length() > 0) ? sb.deleteCharAt(0).toString() : "";
    }

    private static final String ID_DELIM = ",";
    private static final String ID_DEFTYPE = "id";
}
_

attrs.xml
カスタムオーバーレイビューのカスタム属性を定義します。

_<resources>  
    <declare-styleable name="AccessibilityOverlay">  
        <attr name="accessible_group" format="string" />  
    </declare-styleable>  
</resources>
_
7
Cheticamp

最近同じ問題に遭遇し、新しいConstraintLayoutヘルパー(constraintlayout 1.1以降で利用可能)を使用して新しいクラスを実装し、グループビューを使用するのと同じように使用できるようにすることにしました。

実装は Cheticampの答え の簡略化されたバージョンであり、アクセシビリティを処理する新しいビューを作成するという彼のアイデアです。

これが私の実装です:

package com.julienarzul.Android.accessibility

import Android.content.Context
import Android.os.Build
import Android.util.AttributeSet
import Android.view.View
import Android.view.accessibility.AccessibilityEvent
import androidx.constraintlayout.widget.ConstraintHelper
import androidx.constraintlayout.widget.ConstraintLayout

class ConstraintLayoutAccessibilityHelper
@JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintHelper(context, attrs, defStyleAttr) {

    init {
        importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            isScreenReaderFocusable = true
        } else {
            isFocusable = true
        }
    }

    override fun updatePreLayout(container: ConstraintLayout) {
        super.updatePreLayout(container)

        if (this.mReferenceIds != null) {
            this.setIds(this.mReferenceIds)
        }

        mIds.forEach {
            container.getViewById(it)?.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
        }
    }

    override fun onPopulateAccessibilityEvent(event: AccessibilityEvent) {
        super.onPopulateAccessibilityEvent(event)

        val constraintLayoutParent = parent as? ConstraintLayout
        if (constraintLayoutParent != null) {
            event.text.clear()

            mIds.forEach {
                constraintLayoutParent.getViewById(it)?.onPopulateAccessibilityEvent(event)
            }
        }
    }
}

Gistとしても利用可能: https://Gist.github.com/JulienArzul/8068d43af3523d75b72e9d1edbfb4298

グループを使用するのと同じ方法で使用します。

<androidx.constraintlayout.widget.ConstraintLayout
    Android:layout_width="match_parent"
    Android:layout_height="match_parent">

    <TextView
        Android:id="@+id/myTextView"
        />

    <ImageView
        Android:id="@+id/myImageView"
        />

    <com.julienarzul.Android.accessibility.ConstraintLayoutAccessibilityHelper
        Android:layout_width="0dp"
        Android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:constraint_referenced_ids="myTextView,myImageView" />

</androidx.constraintlayout.widget.ConstraintLayout>

このサンプルでは、​​アクセシビリティを目的として、TextViewとImageViewを1つのグループに編成しています。フォーカスを取得し、ConstraintLayout内のアクセシビリティリーダーによって読み取られる他のビューを追加することもできます。

ビューは透明ですが、通常の制約レイアウト属性を使用して、フォーカスされたときに表示される領域を選択できます。
私の例では、アクセシビリティグループは完全なConstraintLayoutの上に表示されますが、app:"layout_constraint..."属性を変更することにより、参照ビューの一部またはすべてに揃えることができます。

5
Julien Arzul

コンテンツの説明を設定

ConstraintLayoutが明示的に content description でフォーカス可能に設定されていることを確認してください。また、子TextViewsnotにフォーカス可能に設定されていることを確認してください。

[〜#〜] xml [〜#〜]

<ConstraintLayout
  Android:focusable="true"
  Android:contentDescription="artist, song, album">

    <TextView artist/>
    <TextView song/>
    <TextView album/>
    <TextView unrelated 1/>
    <TextView unrelated 2/>

</ConstraintLayout>

Java

コードでConstraintLayoutのコンテンツの説明を動的に設定する場合は、関連する各TextViewのテキスト値を連結できます。

String description = tvArtist.getText().toString() + ", " 
    + tvSong.getText().toString() + ", "
    + tvAlbum.getText().toString();

constraintLayout.setContentDescription(description);

アクセシビリティの結果

トークバックをオンにすると、ConstraintLayoutがフォーカスされ、コンテンツの説明を読み上げます。

キャプションとしてトークバックが表示されたスクリーンショット:

Accessibility test screen-shot

詳細説明

上記のスクリーンショットの完全なXMLは次のとおりです。 focusableおよびcontent description属性は、親のConstraintLayoutでのみ設定され、子のTextViewでは設定されないことに注意してください。これにより、TalkBackは個々の子ビューに焦点を合わせるのではなく、親コンテナのみに焦点を合わせます(したがって、その親のコンテンツの説明のみを読み上げます)。

<?xml version="1.0" encoding="utf-8"?>
<Android.support.constraint.ConstraintLayout xmlns:Android="http://schemas.Android.com/apk/res/Android"
    xmlns:app="http://schemas.Android.com/apk/res-auto"
    xmlns:tools="http://schemas.Android.com/tools"
    Android:layout_width="match_parent"
    Android:layout_height="match_parent"
    Android:contentDescription="artist, song, album"
    Android:focusable="true"
    tools:context=".MainActivity">

    <TextView
        Android:id="@+id/text1"
        style="@style/TextAppearance.AppCompat.Display1"
        Android:layout_width="wrap_content"
        Android:layout_height="wrap_content"
        Android:text="Artist"
        app:layout_constraintBottom_toTopOf="@+id/text2"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        Android:id="@+id/text2"
        style="@style/TextAppearance.AppCompat.Display1"
        Android:layout_width="wrap_content"
        Android:layout_height="wrap_content"
        Android:text="Song"
        app:layout_constraintBottom_toTopOf="@+id/text3"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/text1" />

    <TextView
        Android:id="@+id/text3"
        style="@style/TextAppearance.AppCompat.Display1"
        Android:layout_width="wrap_content"
        Android:layout_height="wrap_content"
        Android:text="Album"
        app:layout_constraintBottom_toTopOf="@id/text4"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/text2" />

    <TextView
        Android:id="@+id/text4"
        style="@style/TextAppearance.AppCompat.Display1"
        Android:layout_width="wrap_content"
        Android:layout_height="wrap_content"
        Android:text="Unrelated 1"
        app:layout_constraintBottom_toTopOf="@id/text5"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/text3" />

    <TextView
        Android:id="@+id/text5"
        style="@style/TextAppearance.AppCompat.Display1"
        Android:layout_width="wrap_content"
        Android:layout_height="wrap_content"
        Android:text="Unrelated 2"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/text4" />
</Android.support.constraint.ConstraintLayout>

ネストされたフォーカスアイテム

無関係なTextViewを親のConstraintLayoutから独立してフォーカス可能にしたい場合は、それらのTextViewをfocusable=trueに設定することもできます。これにより、これらのTextViewがフォーカス可能になり、ConstraintLayoutの後に後に個別に読み取られます。

関係のないTextViewを1つのTalkBackアナウンス(ConstraintLayoutとは別)にグループ化する場合、オプションは制限されます。

  1. 無関係なビューを別のViewGroupにネストし、独自のコンテンツの説明を付けるか、
  2. 最初の無関係なアイテムにのみfocusable=trueを設定し、そのコンテンツの説明をそのサブグループの単一のアナウンス(たとえば、「無関係なアイテム」)として設定します。

オプション#2は少しハックと考えられますが、フラットなビュー階層を維持できます(ネストを避けたい場合)。

ただし、フォーカス項目の複数のサブグループ化を実装する場合、より適切な方法は、グループ化をネストされたViewGroupとして編成することです。 Android natural groupings のアクセシビリティに関するドキュメント)によると:

関連コンテンツのセットに適切なフォーカスパターンを定義するには、構造の各部分を独自のフォーカス可能なViewGroupに配置します

2
hungryghost
  1. 制約レイアウトをフォーカス可能として設定します(制約レイアウトでAndroid:focusable = "true"を設定することにより)

  2. コンテンツの説明を制約レイアウトに設定

  3. 含めないビューにはfocusable = "false"を設定します。

コメントに基づいて編集制約レイアウトに単一のフォーカスグループがある場合にのみ適用されます。

0
mayank1513