想象一下,LinearLayout
中有一个RelativeLayout
,其中TextViews
包含3 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并点击TextView
中的LinearLayout
时,TalkbackReader会以“艺术家”,“歌曲”或“相册”为例。
但您可以使用以下方法将前3个TextViews
放入焦点小组:
<LinearLayout
android:focusable="true
...
现在,TalkbackReader会读取“艺术家歌曲专辑”。
2 unrelated TextViews
仍然是自己的,而不是阅读,这是我想要实现的行为。
我现在正尝试使用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
?
[编辑]: 似乎是这种情况,创建解决方案的唯一方法是在外部ConstraintLayout上使用“focusable = true”和/或在视图本身上使用“focusable = false”。这有一些在处理键盘导航/开关盒时应该考虑的缺点:
https://github.com/googlecodelabs/android-accessibility/issues/4
答案 0 :(得分:4)
我最近遇到了一个相同的问题,我决定使用新的ConstraintLayout帮助器(从constraintlayout 1.1开始可用)实现一个新的Class,以便我们可以像使用组视图一样使用它。
该实现是Cheticamp's answer的简化版本,他的想法是创建一个可以处理可访问性的新View。
这是我的实现方式:
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)
}
}
}
}
也可以作为要点: 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内添加其他焦点并且由Accessibility阅读器读取的视图。
视图是透明的,但是您可以使用常规约束布局属性来选择聚焦时显示的区域。
在我的示例中,可访问性组显示在完整的ConstraintLayout上,但是您可以通过修改app:"layout_constraint..."
属性来选择将其与部分或全部参考视图对齐。
答案 1 :(得分:2)
基于ViewGroups
的焦点组仍然可以在ConstraintLayout
内工作,因此您可以将LinearLayouts
和RelativeLayouts
替换为ConstraintLayouts
,并且TalkBack仍可以按预期工作。但是,如果您尝试避免在ViewGroups
中使用嵌套 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
的组合文字。
前面是一般解决方案,但更好的是将是自定义覆盖视图的实现,该视图将自动管理事物。下面显示的自定义叠加层遵循Group
中ConstraintLayout
助手的一般语法,并自动执行上面概述的许多处理。
自定义叠加层执行以下操作:
Group
的{{1}}助手。 ConstraintLayout
来禁用分组控件的可访问性。 (这避免了必须手动执行此操作。)View.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO)
,contentDescription
或getText()
。 (这避免了必须手动执行此操作。另一个优点是,它还将在应用程序运行时提取对文本所做的任何更改。) 仍然需要在布局XML中手动放置叠加视图以叠加hint
。
这是一个示例布局,显示了问题中提到的TextViews
方法和自定义叠加层。左组是传统的ViewGroup
方法,展示了嵌入式ViewGroup
的使用;右边是使用自定义控件的叠加方法。顶部标有“初始焦点”的ConstraintLayout
可以捕获初始焦点,以便于比较这两种方法。
选择TextView
后,“话语提示”会说“歌手,歌曲,专辑”。
在选择了自定义视图叠加层后,“话语提示”还会说“歌手,歌曲,专辑”。
下面是示例布局和自定义视图的代码。 注意事项:尽管此自定义视图使用ConstraintLayout
可以达到指定的目的,但它并不是传统方法的可靠替代。例如:自定义叠加层将说出TextViews
之类的视图类型文本,例如TextView
,而传统方法则不会。
请参阅GitHub上的sample project。
activity_main.xml
EditText
AccessibilityOverlay.java
<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>
attrs.xml
为自定义叠加层视图定义自定义属性。
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";
}
答案 2 :(得分:1)
确保将ConstraintLayout
设置为可显式content description聚焦。另外,请确保子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);
当您打开“话语提示”时,ConstraintLayout现在将成为焦点并读出其内容描述。
以对讲显示为字幕的屏幕截图:
以下是上述示例屏幕快照的完整XML。请注意,focusable和content description属性仅在父ConstraintLayout中设置,而不在子TextViews中设置。这导致“话语提示”从不只关注单个子视图,而只关注父容器(因此,仅读取该父容器的内容描述)。
<?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之后的 中单独读取。
如果要将不相关的TextViews分组为单个的TalkBack公告(与ConstraintLayout分开),则您的选择受到限制:
ViewGroup
中,或者focusable=true
,并将其内容描述设置为该子组的单个公告(例如“不相关的项目”)。选项#2可能会被认为有点不合时宜,但可以让您保持平面视图层次结构(如果您确实想避免嵌套)。
但是,如果要实现焦点项目的多个子分组,则更合适的方法是将分组组织为嵌套的ViewGroup。根据{{3}}上的Android可访问性文档:
要为一组相关内容定义适当的聚焦模式, 将结构的每个部分放入自己的可聚焦ViewGroup
答案 3 :(得分:1)
Android引入了android:screenReaderFocusable
来按约束布局对内容进行分组。这将适用于上述情况。但是需要API级别27。
https://developer.android.com/guide/topics/ui/accessibility/principles#content-groups
答案 4 :(得分:0)
将约束布局设置为可聚焦(通过在约束布局中设置android:focusable =“ true”)
将内容描述设置为“约束布局”
为不包含的视图设置focusable =“ false”。
基于评论进行编辑 仅在约束布局中只有一个焦点组的情况下适用。