如何在 ConstraintLayout 中创建可访问的焦点小组?
How to create accessible focus groups in ConstraintLayout?
假设您在 RelativeLayout
中有一个 LinearLayout
,其中包含 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>
当您激活 TalkbackReader 并单击 LinearLayout
中的 TextView
时,TalkbackReader 将读取 "Artist"、"Song" 或 "Album" .
但您可以将前 3 个 TextViews
放入焦点小组,方法是:
<LinearLayout
android:focusable="true
...
现在 TalkbackReader 会读 "Artist Song Album"。
第2个unrelated TextViews
还是自己看,不读,这就是我想要实现的行为。
我现在正在尝试使用 ConstrainLayout
重新创建此行为,但不知道如何。
<ConstraintLayout>
<TextView artist/>
<TextView song/>
<TextView album/>
<TextView unrelated_textview1/>
<TextView unrelated_textview2/>
</ConstraintLayout>
将小部件放入 "group" 似乎不起作用:
<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
中重新创建焦点小组以实现无障碍访问?
[编辑]:
似乎是这样,创建解决方案的唯一方法是在外部 ConstraintLayout 上使用 "focusable=true" 和/或在视图本身上使用 "focusable=false"。这有一些缺点,在处理键盘导航/开关盒时应该考虑:
https://github.com/googlecodelabs/android-accessibility/issues/4
设置内容描述
确保 ConstraintLayout
设置为可通过显式 content description 聚焦。此外,请确保 child TextViews
未 设置为可聚焦,除非您希望它们被独立读出。
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);
辅助功能结果
当您打开 Talkback 时,ConstraintLayout 现在将获得焦点并读出其内容描述。
将对讲显示为标题的屏幕截图:
详细说明
这是上述示例屏幕截图的完整 XML。请注意,可聚焦和内容描述属性仅在 parent ConstraintLayout 中设置,而不在 child TextView 中设置。这会导致 TalkBack 从不关注单个 child 视图,而只关注 parent 容器(因此,只读出 parent 的内容描述)。
<?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 独立于 parent ConstraintLayout 可聚焦,您也可以将这些 TextView 设置为 focusable=true
。这将导致这些 TextView 变得可聚焦并单独读出,在 ConstraintLayout 之后。
如果你想将不相关的 TextView 组合成一个单一的 TalkBack 公告(与 ConstraintLayout 分开),你的选择是有限的:
- 要么将不相关的视图嵌套到另一个
ViewGroup
中,使用其自己的内容描述,要么
- 仅在第一个不相关的项目上设置
focusable=true
,并将其内容描述设置为该 sub-group 的单个公告(例如 "unrelated items")。
选项 #2 会被认为有点 hack,但可以让您保持平面视图层次结构(如果您真的想避免嵌套)。
但是如果您要实现多个 sub-group 焦点项,更合适的方法是将分组组织为嵌套的 ViewGroup。根据 natural groupings 上的 Android 辅助功能文档:
To define the proper focusing pattern for a set of related content,
place each piece of the structure into its own focusable ViewGroup
基于 ViewGroups
的焦点小组在 ConstraintLayout
中仍然有效,因此您可以将 LinearLayouts
和 RelativeLayouts
替换为 ConstraintLayouts
,TalkBack 仍然有效正如预期的那样。但是,如果您试图在 ConstraintLayout
内避免 嵌套 ViewGroups
,以保持平面视图层次结构的设计目标,这里有一种方法可以做到这一点。
将TextViews
从您提到的焦点ViewGroup
直接移动到top-levelConstraintLayout
。现在我们将使用 ConstraintLayout
约束在这些 TextViews
之上放置一个简单的透明 View
。每个 TextView
将成为 top-level ConstraintLayout
的成员,因此布局将是扁平的。由于叠加层位于 TextViews
之上,它将在底层 TextViews
之前接收所有触摸事件。这是布局结构:
<ConstaintLayout>
<TextView>
<TextView>
<TextView>
<View> [overlays the above TextViews]
</ConstraintLayout>
我们现在可以手动为叠加层指定内容描述,该叠加层是每个基础 TextViews
文本的组合。为了防止每个 TextView
接受焦点并说出自己的文本,我们将设置 android:importantForAccessibility="no"
。当我们触摸叠加视图时,我们会听到 TextViews
口语的组合文本。
前面的是一般解决方案,但更好的是,将实现一个自定义覆盖视图,该视图将自动管理事物。下面显示的自定义叠加层遵循 ConstraintLayout
中 Group
助手的一般语法,并自动执行上述大部分处理。
自定义叠加层执行以下操作:
- 接受将由控件分组的 ID 列表,如
ConstraintLayout
的 Group
助手。
- 通过在每个视图上设置
View.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO)
来禁用分组控件的可访问性。 (这避免了必须手动执行此操作。)
- 单击时,自定义控件会向辅助功能框架显示分组视图文本的串联。为视图收集的文本来自
contentDescription
、getText()
或 hint
。 (这避免了必须手动执行此操作。另一个优点是它还会在应用 运行 时获取对文本所做的任何更改。)
覆盖视图仍然需要在布局 XML 中手动定位以覆盖 TextViews
.
这是一个示例布局,显示了问题中提到的 ViewGroup
方法和自定义叠加层。左组是传统的 ViewGroup
方法,演示了嵌入式 ConstraintLayout
的使用;右边是使用自定义控件的叠加方式。顶部标记为 "initial focus" 的 TextView
只是为了捕捉初始焦点,以便于比较两种方法。
选择 ConstraintLayout
后,TalkBack 会朗读 "Artist, Song, Album"。
选择自定义视图叠加层后,TalkBack 也会说话 "Artist, Song, Album"。
下面是示例布局和自定义视图的代码。 警告:虽然此自定义视图使用 TextViews
可用于规定的目的,但它并不是传统方法的可靠替代品。例如:自定义覆盖会读出视图类型扩展 TextView
的文本,例如 EditText
,而传统方法不会。
参见 GitHub 上的 sample project。
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>
将约束布局设置为可聚焦(通过在约束布局中设置android:focusable="true")
将内容描述设置为约束布局
为不包含的视图设置 focusable="false"。
根据评论编辑
仅适用于约束布局中有单个焦点组的情况。
我 运行 最近遇到了同样的问题,我决定使用新的 ConstraintLayout 助手(自 constraintlayout 1.1 起可用)实现一个新的 Class,这样我们就可以像使用它一样使用它我们使用组视图。
该实现是 的简化版本以及他创建一个新视图来处理可访问性的想法。
这是我的实现:
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)
}
}
}
}
}
也可用作要点:
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 组织在一个组中以实现辅助功能目的。您仍然可以添加其他获得焦点并由 ConstraintLayout 内的辅助功能 reader 读取的视图。
视图是透明的运行,但您可以使用常规约束布局属性选择聚焦时显示的区域。
在我的示例中,可访问性组显示在整个 ConstraintLayout 上,但您可以选择通过修改 app:"layout_constraint..."
属性将其与部分或所有引用视图对齐。
编辑:正如@Mel' 在评论中所建议的那样,我更新了 ConstraintLayoutAccessibilityHelper
class 以确保在辅助功能事件。
Android 引入了 android:screenReaderFocusable
以在约束布局中对内容进行分组。这将适用于上述情况。但需要 API 等级 27.
https://developer.android.com/guide/topics/ui/accessibility/principles#content-groups
仅限 XML。
对于我的特殊情况,默认情况下我将视图分组在可访问性中,并将“对于可访问性字段重要”设置为是。
android: importantForAccessibility="yes"
这什么也没做,
但是当我进入每个视图并分别设置 importantForAccessibility 时
android: importantForAccessibility="yes"
如果你想公布
android: importantForAccessibility="no"
如果您不想公布
--这解决了我的问题。
假设您在 RelativeLayout
中有一个 LinearLayout
,其中包含 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>
当您激活 TalkbackReader 并单击 LinearLayout
中的 TextView
时,TalkbackReader 将读取 "Artist"、"Song" 或 "Album" .
但您可以将前 3 个 TextViews
放入焦点小组,方法是:
<LinearLayout
android:focusable="true
...
现在 TalkbackReader 会读 "Artist Song Album"。
第2个unrelated TextViews
还是自己看,不读,这就是我想要实现的行为。
我现在正在尝试使用 ConstrainLayout
重新创建此行为,但不知道如何。
<ConstraintLayout>
<TextView artist/>
<TextView song/>
<TextView album/>
<TextView unrelated_textview1/>
<TextView unrelated_textview2/>
</ConstraintLayout>
将小部件放入 "group" 似乎不起作用:
<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
中重新创建焦点小组以实现无障碍访问?
[编辑]: 似乎是这样,创建解决方案的唯一方法是在外部 ConstraintLayout 上使用 "focusable=true" 和/或在视图本身上使用 "focusable=false"。这有一些缺点,在处理键盘导航/开关盒时应该考虑:
https://github.com/googlecodelabs/android-accessibility/issues/4
设置内容描述
确保 ConstraintLayout
设置为可通过显式 content description 聚焦。此外,请确保 child TextViews
未 设置为可聚焦,除非您希望它们被独立读出。
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);
辅助功能结果
当您打开 Talkback 时,ConstraintLayout 现在将获得焦点并读出其内容描述。
将对讲显示为标题的屏幕截图:
详细说明
这是上述示例屏幕截图的完整 XML。请注意,可聚焦和内容描述属性仅在 parent ConstraintLayout 中设置,而不在 child TextView 中设置。这会导致 TalkBack 从不关注单个 child 视图,而只关注 parent 容器(因此,只读出 parent 的内容描述)。
<?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 独立于 parent ConstraintLayout 可聚焦,您也可以将这些 TextView 设置为 focusable=true
。这将导致这些 TextView 变得可聚焦并单独读出,在 ConstraintLayout 之后。
如果你想将不相关的 TextView 组合成一个单一的 TalkBack 公告(与 ConstraintLayout 分开),你的选择是有限的:
- 要么将不相关的视图嵌套到另一个
ViewGroup
中,使用其自己的内容描述,要么 - 仅在第一个不相关的项目上设置
focusable=true
,并将其内容描述设置为该 sub-group 的单个公告(例如 "unrelated items")。
选项 #2 会被认为有点 hack,但可以让您保持平面视图层次结构(如果您真的想避免嵌套)。
但是如果您要实现多个 sub-group 焦点项,更合适的方法是将分组组织为嵌套的 ViewGroup。根据 natural groupings 上的 Android 辅助功能文档:
To define the proper focusing pattern for a set of related content, place each piece of the structure into its own focusable ViewGroup
基于 ViewGroups
的焦点小组在 ConstraintLayout
中仍然有效,因此您可以将 LinearLayouts
和 RelativeLayouts
替换为 ConstraintLayouts
,TalkBack 仍然有效正如预期的那样。但是,如果您试图在 ConstraintLayout
内避免 嵌套 ViewGroups
,以保持平面视图层次结构的设计目标,这里有一种方法可以做到这一点。
将TextViews
从您提到的焦点ViewGroup
直接移动到top-levelConstraintLayout
。现在我们将使用 ConstraintLayout
约束在这些 TextViews
之上放置一个简单的透明 View
。每个 TextView
将成为 top-level ConstraintLayout
的成员,因此布局将是扁平的。由于叠加层位于 TextViews
之上,它将在底层 TextViews
之前接收所有触摸事件。这是布局结构:
<ConstaintLayout>
<TextView>
<TextView>
<TextView>
<View> [overlays the above TextViews]
</ConstraintLayout>
我们现在可以手动为叠加层指定内容描述,该叠加层是每个基础 TextViews
文本的组合。为了防止每个 TextView
接受焦点并说出自己的文本,我们将设置 android:importantForAccessibility="no"
。当我们触摸叠加视图时,我们会听到 TextViews
口语的组合文本。
前面的是一般解决方案,但更好的是,将实现一个自定义覆盖视图,该视图将自动管理事物。下面显示的自定义叠加层遵循 ConstraintLayout
中 Group
助手的一般语法,并自动执行上述大部分处理。
自定义叠加层执行以下操作:
- 接受将由控件分组的 ID 列表,如
ConstraintLayout
的Group
助手。 - 通过在每个视图上设置
View.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO)
来禁用分组控件的可访问性。 (这避免了必须手动执行此操作。) - 单击时,自定义控件会向辅助功能框架显示分组视图文本的串联。为视图收集的文本来自
contentDescription
、getText()
或hint
。 (这避免了必须手动执行此操作。另一个优点是它还会在应用 运行 时获取对文本所做的任何更改。)
覆盖视图仍然需要在布局 XML 中手动定位以覆盖 TextViews
.
这是一个示例布局,显示了问题中提到的 ViewGroup
方法和自定义叠加层。左组是传统的 ViewGroup
方法,演示了嵌入式 ConstraintLayout
的使用;右边是使用自定义控件的叠加方式。顶部标记为 "initial focus" 的 TextView
只是为了捕捉初始焦点,以便于比较两种方法。
选择 ConstraintLayout
后,TalkBack 会朗读 "Artist, Song, Album"。
选择自定义视图叠加层后,TalkBack 也会说话 "Artist, Song, Album"。
下面是示例布局和自定义视图的代码。 警告:虽然此自定义视图使用 TextViews
可用于规定的目的,但它并不是传统方法的可靠替代品。例如:自定义覆盖会读出视图类型扩展 TextView
的文本,例如 EditText
,而传统方法不会。
参见 GitHub 上的 sample project。
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>
将约束布局设置为可聚焦(通过在约束布局中设置android:focusable="true")
将内容描述设置为约束布局
为不包含的视图设置 focusable="false"。
根据评论编辑 仅适用于约束布局中有单个焦点组的情况。
我 运行 最近遇到了同样的问题,我决定使用新的 ConstraintLayout 助手(自 constraintlayout 1.1 起可用)实现一个新的 Class,这样我们就可以像使用它一样使用它我们使用组视图。
该实现是
这是我的实现:
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)
}
}
}
}
}
也可用作要点: 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 组织在一个组中以实现辅助功能目的。您仍然可以添加其他获得焦点并由 ConstraintLayout 内的辅助功能 reader 读取的视图。
视图是透明的运行,但您可以使用常规约束布局属性选择聚焦时显示的区域。
在我的示例中,可访问性组显示在整个 ConstraintLayout 上,但您可以选择通过修改 app:"layout_constraint..."
属性将其与部分或所有引用视图对齐。
编辑:正如@Mel' 在评论中所建议的那样,我更新了 ConstraintLayoutAccessibilityHelper
class 以确保在辅助功能事件。
Android 引入了 android:screenReaderFocusable
以在约束布局中对内容进行分组。这将适用于上述情况。但需要 API 等级 27.
https://developer.android.com/guide/topics/ui/accessibility/principles#content-groups
仅限 XML。
对于我的特殊情况,默认情况下我将视图分组在可访问性中,并将“对于可访问性字段重要”设置为是。
android: importantForAccessibility="yes"
这什么也没做,
但是当我进入每个视图并分别设置 importantForAccessibility 时
android: importantForAccessibility="yes"
如果你想公布
android: importantForAccessibility="no"
如果您不想公布
--这解决了我的问题。