uso studio quitar propiedades left eliminar constraintlayout constraint como align android accessibility android-constraintlayout talkback

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:

  1. Puede anidar las vistas no relacionadas en otro ViewGroup , con su propia descripción de contenido, o
  2. 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


  1. Establezca el diseño de restricción como enfocable (configurando android: focusable = "true" en el diseño de restricción)

  2. Establecer la descripción del contenido en Diseño de restricción

  3. 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:

  1. Acepta una lista de identificadores que serán agrupados por el control como el ayudante de Group de ConstraintLayout .
  2. Deshabilita la accesibilidad para los controles agrupados configurando View.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO) en cada vista. (Esto evita tener que hacer esto manualmente).
  3. 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 la hint . (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>