如何在ConstraintLayout中创建可访问的焦点组?

ham*_*314 20 android accessibility talkback android-constraintlayout

想象一下,你有一个LinearLayout里面RelativeLayout包含3个TextViews具有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>        
Run Code Online (Sandbox Code Playgroud)

当您激活TalkbackReader并单击其中的a TextViewLinearLayout,TalkbackReader将显示"艺术家","歌曲"或"专辑".

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

<LinearLayout
    android:focusable="true
    ...
Run Code Online (Sandbox Code Playgroud)

现在,TalkbackReader将阅读"艺术家歌曲专辑".

2 unrelated TextViews仍然是他们自己而不是阅读,这是我想要实现的行为.

(请参阅Google codelabs示例以供参考)

我现在正试图重新创建这种行为,ConstrainLayout但没有看到如何.

<ConstraintLayout>
    <TextView artist/>
    <TextView song/>
    <TextView album/>
    <TextView unrelated_textview1/>
    <TextView unrelated_textview2/>
</ConstraintLayout>
Run Code Online (Sandbox Code Playgroud)

将小部件放入"组"似乎不起作用:

<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"
    />
Run Code Online (Sandbox Code Playgroud)

那么如何重新创建焦点组以实现可访问性ConstrainLayout呢?

[编辑]: 似乎是这样,创建解决方案的唯一方法是在外部ConstraintLayout上使用"focusable = true"和/或在视图本身上使用"focusable = false".这有一些在处理键盘导航/开关盒时应该考虑的缺点:

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

Che*_*amp 16

重点人群应根据ViewGroups内仍然工作ConstraintLayout,所以你可以替换LinearLayouts,并RelativeLayoutsConstraintLayouts和对讲仍然会正常工作。但是,如果您试图避免在 内嵌套 ,以保持平面视图层次结构的设计目标,这里有一种方法可以做到。ViewGroupsConstraintLayout

将直接TextViewsViewGroup您提到的焦点移到顶级ConstraintLayout. 现在我们将使用约束View在这些之上放置一个简单的透明体。每个都将是顶级的成员,因此布局将是平面的。由于叠加层位于 之上,它将在底层 之前接收所有触摸事件。这是布局结构:TextViewsConstraintLayoutTextViewConstraintLayoutTextViewsTextViews

<ConstaintLayout>
    <TextView>
    <TextView>
    <TextView>
    <View> [overlays the above TextViews]
</ConstraintLayout>
Run Code Online (Sandbox Code Playgroud)

我们现在可以手动为覆盖层指定一个内容描述,它是每个底层TextViews. 为了防止每个人都TextView接受焦点并说出自己的文本,我们将设置android:importantForAccessibility="no". 当我们触摸覆盖视图时,我们会听到TextViews口语的组合文本。

前面是通用解决方案,但更好的是,将是一个自定义覆盖视图的实现,它将自动管理事物。下面显示的自定义覆盖遵循Grouphelper 中的一般语法,ConstraintLayout 并自动执行上面概述的大部分处理。

自定义叠加执行以下操作:

  1. 接受一个 id 列表,这些 id 将被控件分组,就像 的Group助手一样ConstraintLayout
  2. 通过View.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO)在每个视图上设置禁用分组控件的可访问性。(这避免了必须手动执行此操作。)
  3. 单击时,自定义控件将分组视图的文本串联呈现给可访问性框架。收集一个视图中的文字是无论是从contentDescriptiongetText()hint。(这避免了必须手动执行此操作。另一个优点是它还可以在应用程序运行时获取对文本所做的任何更改。)

覆盖视图仍然需要在布局 XML 中手动定位以覆盖TextViews.

这是一个示例布局,显示ViewGroup了问题中提到的方法和自定义叠加层。左组是ViewGroup展示嵌入式使用的传统方法ConstraintLayout;右边是使用自定义控件的overlay方法。在TextView标有“最初的重点在”上面是就在那里捕捉到最初的重点为便于两种方法比较。

随着ConstraintLayout选择,话语提示说“艺术家,歌曲,专辑”。

在此处输入图片说明

选择自定义视图叠加层后,TalkBack 还会说出“艺术家、歌曲、专辑”。

在此处输入图片说明

下面是自定义视图的示例布局和代码。警告:尽管此自定义视图使用 可用于所述目的TextViews,但它并不是传统方法的可靠替代品。例如:自定义覆盖将延伸说话视图类型的文本TextView本身EditText,而传统的方法则没有。

请参阅GitHub 上的示例项目

活动_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>
Run Code Online (Sandbox Code Playgroud)

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";
}
Run Code Online (Sandbox Code Playgroud)

attrs.xml
定义自定义叠加视图的自定义属性。

<resources>  
    <declare-styleable name="AccessibilityOverlay">  
        <attr name="accessible_group" format="string" />  
    </declare-styleable>  
</resources>
Run Code Online (Sandbox Code Playgroud)


Jul*_*zul 7

我最近遇到了同样的问题,我决定使用新的 ConstraintLayout 帮助程序(自约束布局 1.1 起可用)实现一个新类,以便我们可以像使用 Group 视图一样使用它。

该实现是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 { id ->
                val view: View? = constraintLayoutParent.getViewById(id)
                // Adds this View to the Accessibility Event only if it is currently visible
                if (view?.isVisible == true) {
                    view.onPopulateAccessibilityEvent(event)
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

也可作为要点提供:https : //gist.github.com/JulienArzul/8068d43af3523d75b72e9d1edbfb4298

您可以像使用 Group 一样使用它:

<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>
Run Code Online (Sandbox Code Playgroud)

出于可访问性目的,此示例将 TextView 和 ImageView 组织在一个组中。您仍然可以添加其他获得焦点并由 ConstraintLayout 内的辅助功能阅读器读取的视图。

视图是透明的,但您可以使用常规约束布局属性选择聚焦时显示的区域。
在我的示例中,可访问性组显示在完整的 ConstraintLayout 上,但您可以选择通过修改app:"layout_constraint..."属性将其与部分或全部引用的视图对齐。

编辑:正如@Mel' 在评论中所建议的,我更新了ConstraintLayoutAccessibilityHelper该类以确保在辅助功能事件中只添加了可见的视图。