Ответ 1
Фокус-группы, основанные на 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
изConstraintLayout
. - Отключает доступ для сгруппированных элементов управления, устанавливая
View.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO)
в каждом представлении. (Это позволяет избежать необходимости делать это вручную.) - При щелчке пользовательский элемент управления представляет конкатенацию текста сгруппированных представлений в каркасе специальных возможностей. Текст, собранный для просмотра, взят из
contentDescription
,getText()
илиhint
. (Это позволяет избежать необходимости делать это вручную. Еще одним преимуществом является то, что он также учитывает любые изменения, внесенные в текст во время работы приложения.)
Оверлейное представление по-прежнему необходимо размещать вручную в XML макета для наложения TextViews
.
Вот примерный макет, показывающий подход ViewGroup
, упомянутый в вопросе, и пользовательское наложение. Левая группа - это традиционный подход ViewGroup
, демонстрирующий использование встроенного ConstraintLayout
; Справа - метод наложения с использованием пользовательского элемента управления. TextView
сверху с надписью "начальный фокус" как раз для того, чтобы захватить начальный фокус для облегчения сравнения двух методов.
С выбранным ConstraintLayout
TalkBack говорит "Исполнитель, Песня, Альбом".
С выбранным наложением пользовательского представления TalkBack также говорит "Исполнитель, Песня, Альбом".
Ниже приведен пример макета и код для пользовательского представления. Предостережение: хотя это пользовательское представление работает для заявленной цели с использованием TextViews
, оно не является надежной заменой традиционному методу. Например: пользовательское наложение будет озвучивать текст типов представлений, расширяющих TextView
, таких как EditText
, в то время как традиционный метод этого не делает.
Смотрите пример проекта на GitHub.
activity_main.xml
<android.support.constraint.ConstraintLayout
android:id="@+id/layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.constraint.ConstraintLayout
android:id="@+id/viewGroup"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:focusable="true"
android:gravity="center_horizontal"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/viewGroupHeading">
<TextView
android:id="@+id/artistText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Artist"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/songText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Song"
app:layout_constraintStart_toStartOf="@+id/artistText"
app:layout_constraintTop_toBottomOf="@+id/artistText" />
<TextView
android:id="@+id/albumText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Album"
app:layout_constraintStart_toStartOf="@+id/songText"
app:layout_constraintTop_toBottomOf="@+id/songText" />
</android.support.constraint.ConstraintLayout>
<TextView
android:id="@+id/artistText2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Artist"
app:layout_constraintBottom_toTopOf="@+id/songText2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toTopOf="@+id/viewGroup" />
<TextView
android:id="@+id/songText2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Song"
app:layout_constraintStart_toStartOf="@id/artistText2"
app:layout_constraintTop_toBottomOf="@+id/artistText2" />
<TextView
android:id="@+id/albumText2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Album"
app:layout_constraintStart_toStartOf="@+id/artistText2"
app:layout_constraintTop_toBottomOf="@+id/songText2" />
<com.example.constraintlayoutaccessibility.AccessibilityOverlay
android:id="@+id/overlay"
android:layout_width="0dp"
android:layout_height="0dp"
android:focusable="true"
app:accessible_group="artistText2, songText2, albumText2, editText2, button2"
app:layout_constraintBottom_toBottomOf="@+id/albumText2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/guideline"
app:layout_constraintTop_toTopOf="@id/viewGroup" />
<android.support.constraint.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
<TextView
android:id="@+id/viewGroupHeading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:importantForAccessibility="no"
android:text="ViewGroup"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView4" />
<TextView
android:id="@+id/overlayHeading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
android:text="Overlay"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toTopOf="@+id/viewGroupHeading" />
<TextView
android:id="@+id/textView4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:text="Initial focus"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
AccessibilityOverlay.java
public class AccessibilityOverlay extends View {
private int[] mAccessibleIds;
public AccessibilityOverlay(Context context) {
super(context);
init(context, null, 0, 0);
}
public AccessibilityOverlay(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs, 0, 0);
}
public AccessibilityOverlay(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr, 0);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public AccessibilityOverlay(Context context, @Nullable AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs, defStyleAttr, defStyleRes);
}
private void init(Context context, @Nullable AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
String accessibleIdString;
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.AccessibilityOverlay,
defStyleAttr, defStyleRes);
try {
accessibleIdString = a.getString(R.styleable.AccessibilityOverlay_accessible_group);
} finally {
a.recycle();
}
mAccessibleIds = extractAccessibleIds(context, accessibleIdString);
}
@NonNull
private int[] extractAccessibleIds(@NonNull Context context, @Nullable String idNameString) {
if (TextUtils.isEmpty(idNameString)) {
return new int[]{};
}
String[] idNames = idNameString.split(ID_DELIM);
int[] resIds = new int[idNames.length];
Resources resources = context.getResources();
String packageName = context.getPackageName();
int idCount = 0;
for (String idName : idNames) {
idName = idName.trim();
if (idName.length() > 0) {
int resId = resources.getIdentifier(idName, ID_DEFTYPE, packageName);
if (resId != 0) {
resIds[idCount++] = resId;
}
}
}
return resIds;
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
View view;
ViewGroup parent = (ViewGroup) getParent();
for (int id : mAccessibleIds) {
if (id == 0) {
break;
}
view = parent.findViewById(id);
if (view != null) {
view.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
}
}
}
@Override
public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
super.onPopulateAccessibilityEvent(event);
int eventType = event.getEventType();
if (eventType == AccessibilityEvent.TYPE_VIEW_SELECTED ||
eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED &&
getContentDescription() == null) {
event.getText().add(getAccessibilityText());
}
}
@NonNull
private String getAccessibilityText() {
ViewGroup parent = (ViewGroup) getParent();
View view;
StringBuilder sb = new StringBuilder();
for (int id : mAccessibleIds) {
if (id == 0) {
break;
}
view = parent.findViewById(id);
if (view != null && view.getVisibility() == View.VISIBLE) {
CharSequence description = view.getContentDescription();
// This misbehaves if the view is an EditText or Button or otherwise derived
// from TextView by voicing the content when the ViewGroup approach remains
// silent.
if (TextUtils.isEmpty(description) && view instanceof TextView) {
TextView tv = (TextView) view;
description = tv.getText();
if (TextUtils.isEmpty(description)) {
description = tv.getHint();
}
}
if (description != null) {
sb.append(",");
sb.append(description);
}
}
}
return (sb.length() > 0) ? sb.deleteCharAt(0).toString() : "";
}
private static final String ID_DELIM = ",";
private static final String ID_DEFTYPE = "id";
}
attrs.xml
Определите пользовательские атрибуты для пользовательского представления наложения.
<resources>
<declare-styleable name="AccessibilityOverlay">
<attr name="accessible_group" format="string" />
</declare-styleable>
</resources>