如何在ConstraintLayout中创建可访问的焦点组? [英] How to create accessible focus groups in ConstraintLayout?

查看:100
本文介绍了如何在ConstraintLayout中创建可访问的焦点组?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

想象一下,您在 RelativeLayout 中包含一个 LinearLayout ,其中包含3个 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 /相册
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并单击 TextView LinearLayout 中,TalkbackReader将显示 Artist, Song或 Album。



但是您可以使用以下方法将前三个 TextViews 放入焦点组:

 < LinearLayout 
android:focusable = true
...

现在TalkbackReader会读歌手歌曲专辑。



2个不相关的TextViews 仍然会独自一人而不是阅读,这是我想要实现的行为。



(请参阅



在选择了自定义视图覆盖后,话语提示还会说歌手,歌曲,专辑。





下面是示例布局和代码自定义视图。 注意事项:尽管此自定义视图使用 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_horizo​​ntal
app:layout_constraintEnd_toStartOf = @ + id / guideline
app:layout_constraintStart_toStartOf =父项
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 =相册
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 =相册
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 =否
android:text =覆盖
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 =初始焦点
app:layout_constraintEnd_toStartOf = @ + id / guideline
app:layout_constraintStart_toStartOf = @ + id / guideline
app:layout_constraintTop_toTopOf = parent />

< /android.support.constraint.ConstraintLayout>

AccessibilityOverlay.java

 公共类AccessibilityOverlay扩展了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(上下文,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(上下文上下文,@Nullable AttributeSet属性,
int defStyleAttr,int defStyleRes){
super(context,attrs,defStyleAttr,defStyleRes);
init(上下文,属性,defStyleAttr,defStyleRes);
}

private void init(上下文上下文,@ Nullable AttributeSet属性,
int defStyleAttr,int defStyleRes)

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

试试{
accessibleIdString = a.getString(R.styleable.AccessibilityOverlay_accessible_group);
}最终{
a.recycle();
}
mAccessibleIds = extractAccessibleIds(context,accessibleIdString);
}

@NonNull
private int [] extractAccessibleIds(@NonNull上下文上下文,@ Nullable字符串idNameString){
if(TextUtils.isEmpty(idNameString)){
返回新的int [] {};
}
String [] idNames = idNameString.split(ID_DELIM);
int [] resIds = new int [idNames.length];
资源资源= context.getResources();
字符串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();

视图视图;
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()。添加(getAccessibilityText());
}
}

@NonNull
private String getAccessibilityText(){
ViewGroup parent =(ViewGroup)getParent();
视图视图;
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();

//如果视图是EditText或Button或以其他方式从TextView派生
//通过在ViewGroup方法保持
静默时发声内容,则该行为不正确。
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>


Imagine you have a LinearLayout inside a RelativeLayout that contains 3 TextViews with artist, song and album:

<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>        

When you activate the TalkbackReader and click on a TextView in the LinearLayout, the TalkbackReader will read "Artist", "Song" OR "Album" for example.

But you could put those first 3 TextViews into a focus group, by using:

<LinearLayout
    android:focusable="true
    ...

Now the TalkbackReader would read "Artist Song Album".

The 2 unrelated TextViewsstill would be on their own and not read, which is the behaviour I want to achieve.

(See Google codelabs example for reference)

I am now trying to re-create this behaviour with the ConstrainLayout but dont see how.

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

Putting widgets into a "group" does not seem to work:

<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"
    />

So how can I re-create focus-groups for accessibility in the ConstrainLayout?

[EDIT]: It seems to be the case, that the only way to create a solution is to use "focusable=true" on the outer ConstraintLayout and / or "focusable=false" on the views themselves. This has some drawbacks that one should consider when dealing with keyboard navigation / switch-boxes:

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

解决方案

The focus groups based upon ViewGroups still work within ConstraintLayout, so you could replace LinearLayouts and RelativeLayouts with ConstraintLayouts and TalkBack will still work as expected. But, if you are trying to avoid nesting ViewGroups within ConstraintLayout, keeping with the design goal of a flat view hierarchy, here is a way to do it.

Move the TextViews from the focus ViewGroup that you mention directly into the top-level ConstraintLayout. Now we will place a simple transparent View on top of these TextViews using ConstraintLayout constraints. Each TextView will be a member of the top-level ConstraintLayout, so the layout will be flat. Since the overlay is on top of the TextViews, it will receive all touch events before the underlying TextViews. Here is the layout structure:

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

We can now manually specify a content description for the overlay that is a combination of the text of each of the underlying TextViews. To prevent each TextView from accepting focus and speaking its own text, we will set android:importantForAccessibility="no". When we touch the overlay view, we hear the combined text of the TextViews spoken.

The preceding is the general solution but, better yet, would be an implementation of a custom overlay view that will manage things automatically. The custom overlay shown below follows the general syntax of the Group helper in ConstraintLayout and automates much of the processing outlined above.

The custom overlay does the following:

  1. Accepts a list of ids that will be grouped by the control like the Group helper of ConstraintLayout.
  2. Disables accessibility for the grouped controls by setting View.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO) on each view. (This avoids having to do this manually.)
  3. When clicked, the custom control presents a concatenation of the text of grouped views to the accessibility framework. The text collected for a view is either from the contentDescription, getText() or the hint. (This avoids having to do this manually. Another advantage is that it will also pick up any changes made to the text while the app is running.)

The overlay view still needs to be positioned manually within the layout XML to overlay the TextViews.

Here is a sample layout showing the ViewGroup approach mentioned in the question and the custom overlay. The left group is the traditional ViewGroup approach demonstrating the use of an embedded ConstraintLayout; The right is the overlay method using the custom control. The TextView on top labeled "initial focus" is just there to capture the initial focus for ease of comparing the two methods.

With the ConstraintLayout selected, TalkBack speaks "Artist, Song, Album".

With the custom view overlay selected, TalkBack also speaks "Artist, Song, Album".

Below is the sample layout and the code for the custom view. Caveat: Although this custom view works for the stated purpose using TextViews, it is not a robust replacement for the traditional method. For example: The custom overlay will speak the text of view types extending TextView such as EditText while the traditional method does not.

See the sample project on 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
Define the custom attributes for the custom overlay view.

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

这篇关于如何在ConstraintLayout中创建可访问的焦点组?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆