android - studio - propiedades constraintlayout
¿Cómo crear grupos de enfoque accesibles en ConstraintLayout? (3)
Imagina que tienes un LinearLayout
dentro de un RelativeLayout
que contiene 3 TextViews
de TextViews
con 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>
Cuando activa el TalkbackReader y hace clic en un TextView
en LinearLayout
, el TalkbackReader leerá "Artista", "Canción" O "Álbum", por ejemplo.
Pero puede colocar esas 3 primeras TextViews
de TextViews
en un grupo de enfoque, usando:
<LinearLayout
android:focusable="true
...
Ahora el TalkbackReader leería "Album de canciones del artista".
Los 2 unrelated TextViews
todavía serían por su cuenta y no leídos, que es el comportamiento que quiero lograr.
(Ver el ejemplo de Google codelabs para referencia)
Ahora estoy intentando recrear este comportamiento con ConstrainLayout
pero no veo cómo.
<ConstraintLayout>
<TextView artist/>
<TextView song/>
<TextView album/>
<TextView unrelated_textview1/>
<TextView unrelated_textview2/>
</ConstraintLayout>
Poner los widgets en un "grupo" no parece funcionar:
<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"
/>
Entonces, ¿cómo puedo volver a crear grupos de enfoque para la accesibilidad en ConstrainLayout
?
[EDITAR]: Parece ser el caso, que la única forma de crear una solución es usar "focusable = true" en el ConstraintLayout externo y / o "focusable = false" en las vistas en sí mismas. Esto tiene algunos inconvenientes que uno debe tener en cuenta al tratar con la navegación del teclado / cajas de interruptores:
https://github.com/googlecodelabs/android-accessibility/issues/4
Establecer Descripción del Contenido
Asegúrese de que ConstraintLayout
esté configurado para enfocarse con una descripción de contenido explícita. Además, asegúrese de que las TextViews
secundarias no estén configuradas para enfocarse, a menos que desee que se lean de forma independiente.
XML
<ConstraintLayout
android:focusable="true"
android:contentDescription="artist, song, album">
<TextView artist/>
<TextView song/>
<TextView album/>
<TextView unrelated 1/>
<TextView unrelated 2/>
</ConstraintLayout>
Java
Si prefieres configurar dinámicamente la descripción del contenido de ConstraintLayout en el código, puedes concatenar los valores de texto de cada TextView
relevante:
String description = tvArtist.getText().toString() + ", "
+ tvSong.getText().toString() + ", "
+ tvAlbum.getText().toString();
constraintLayout.setContentDescription(description);
Resultados de accesibilidad
Cuando active Talkback, ConstraintLayout ahora se enfocará y leerá su descripción de contenido.
Captura de pantalla con Talkback mostrado como título:
Explicación detallada
Aquí está el XML completo para la captura de pantalla de ejemplo anterior. Tenga en cuenta que los atributos enfocados y de la descripción del contenido solo se configuran en el elemento principal de restricción, no en las vistas de texto secundarias. Esto hace que TalkBack nunca se centre en las vistas secundarias individuales, sino solo en el contenedor principal (por lo tanto, leyendo solo la descripción del contenido de ese principal).
<?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>
Elementos de enfoque anidados
Si desea que sus TextViews no relacionadas puedan enfocarse independientemente del elemento principal de Restricción, puede configurar esas TextViews para focusable focusable=true
también. Esto hará que los TextViews se puedan enfocar y leer individualmente, después de ConstraintLayout.
Si desea agrupar las vistas de texto no relacionadas en un anuncio singular de TalkBack (aparte de ConstraintLayout), sus opciones son limitadas:
- Puede anidar las vistas no relacionadas en otro
ViewGroup
, con su propia descripción de contenido, o - Establezca
focusable=true
solo en el primer elemento no relacionado y establezca su descripción de contenido como un anuncio único para ese subgrupo (por ejemplo, "elementos no relacionados").
La opción # 2 se consideraría un poco pirateada, pero le permitiría mantener una jerarquía de vista plana (si realmente quiere evitar el anidamiento).
Pero si está implementando múltiples subgrupos de elementos de enfoque, la forma más apropiada sería organizar los grupos como grupos de vistas anidados. Según la documentación de accesibilidad de Android en agrupaciones naturales :
Para definir el patrón de enfoque adecuado para un conjunto de contenido relacionado, coloque cada parte de la estructura en su propio ViewGroup enfocable
Establezca el diseño de restricción como enfocable (configurando android: focusable = "true" en el diseño de restricción)
Establecer la descripción del contenido en Diseño de restricción
set focusable = "false" para vistas que no se incluirán.
Edición basada en comentarios Solo aplicable si hay un único grupo de enfoque en el diseño de restricciones.
Los grupos de enfoque basados en ViewGroups
todavía funcionan dentro de ConstraintLayout
, por lo que podría reemplazar LinearLayouts
y RelativeLayouts
con ConstraintLayouts
y TalkBack seguirán funcionando como se esperaba. Pero, si está intentando evitar anidar ViewGroups
dentro de ConstraintLayout
, manteniendo el objetivo de diseño de una jerarquía de vista plana, aquí hay una manera de hacerlo.
Mueva las TextViews
de TextViews
desde el TextViews
de TextViews
de enfoque que mencione directamente a ConstraintLayout
nivel superior. Ahora TextViews
una simple View
transparente sobre estas TextViews
con ConstraintLayout
restricción de restricciones. Cada TextView
será miembro de ConstraintLayout
nivel superior, por lo que el diseño será plano. Como la superposición está encima de TextViews
, recibirá todos los eventos táctiles antes de las TextViews
subyacentes. Aquí está la estructura de diseño:
<ConstaintLayout>
<TextView>
<TextView>
<TextView>
<View> [overlays the above TextViews]
</ConstraintLayout>
Ahora podemos especificar manualmente una descripción de contenido para la superposición que es una combinación del texto de cada una de las TextViews
de texto subyacentes. Para evitar que TextView
acepte el enfoque y pronuncie su propio texto, estableceremos android:importantForAccessibility="no"
. Cuando tocamos la vista de superposición, escuchamos el texto combinado de las TextViews
de texto habladas.
Lo anterior es la solución general pero, mejor aún, sería una implementación de una vista de superposición personalizada que administrará las cosas automáticamente. La superposición personalizada que se muestra a continuación sigue la sintaxis general del asistente del Group
en ConstraintLayout
y automatiza gran parte del procesamiento descrito anteriormente.
La superposición personalizada hace lo siguiente:
- Acepta una lista de identificadores que serán agrupados por el control como el ayudante de
Group
deConstraintLayout
. - Deshabilita la accesibilidad para los controles agrupados configurando
View.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO)
en cada vista. (Esto evita tener que hacer esto manualmente). - Cuando se hace clic, el control personalizado presenta una concatenación del texto de vistas agrupadas al marco de accesibilidad. El texto recopilado para una vista es de la
contentDescription
,getText()
o lahint
. (Esto evita tener que hacer esto manualmente. Otra ventaja es que también recogerá los cambios realizados en el texto mientras se ejecuta la aplicación).
La vista de superposición aún debe colocarse manualmente dentro del diseño XML para superponer las TextViews
.
Aquí hay un diseño de muestra que muestra el enfoque ViewGroup
mencionado en la pregunta y la superposición personalizada. El grupo de la izquierda es el enfoque tradicional de ViewGroup
que demuestra el uso de un ConstraintLayout
incrustado; El derecho es el método de superposición utilizando el control personalizado. El TextView
en la parte superior etiquetado como "enfoque inicial" está justo ahí para capturar el enfoque inicial para facilitar la comparación de los dos métodos.
Con ConstraintLayout
seleccionado, TalkBack habla "Artista, canción, álbum".
Con la superposición de vista personalizada seleccionada, TalkBack también habla "Artista, canción, álbum".
A continuación se muestra el diseño de muestra y el código para la vista personalizada. Advertencia: aunque esta vista personalizada funciona para el propósito establecido utilizando TextViews
, no es un sustituto sólido del método tradicional. Por ejemplo: la superposición personalizada expresará el texto de los tipos de vista que extienden TextView
, como EditText
mientras que el método tradicional no lo hace.
Vea el proyecto de muestra en 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
Defina los atributos personalizados para la vista de superposición personalizada.
<resources>
<declare-styleable name="AccessibilityOverlay">
<attr name="accessible_group" format="string" />
</declare-styleable>
</resources>