Imaginez que vous avez une LinearLayout
dans une RelativeLayout
qui contient 3 TextViews
avec 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>
Lorsque vous activez TalkbackReader et que vous cliquez sur une TextView
dans la LinearLayout
, TalkbackReader lit par exemple "Artiste", "Morceau" OR "Album".
Mais vous pouvez mettre ces 3 TextViews
premières dans un groupe de discussion en utilisant:
<LinearLayout
Android:focusable="true
...
TalkbackReader se lirait alors comme suit: "Album de chansons d'artiste".
Les 2 unrelated TextViews
still seraient par eux-mêmes et non lus, ce qui est le comportement que je veux obtenir.
(Voir exemple Google Codelabs pour référence)
J'essaie maintenant de recréer ce comportement avec la variable ConstrainLayout
mais je ne vois pas comment.
<ConstraintLayout>
<TextView artist/>
<TextView song/>
<TextView album/>
<TextView unrelated_textview1/>
<TextView unrelated_textview2/>
</ConstraintLayout>
Mettre les widgets dans un "groupe" ne semble pas fonctionner:
<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"
/>
Alors, comment puis-je recréer des groupes de discussion pour l'accessibilité dans la ConstrainLayout
?
[EDIT]: Il semble que le seul moyen de créer une solution consiste à utiliser "focusable = true" sur le ConstraintLayout extérieur et/ou "focusable = false" sur les vues elles-mêmes. . Cela présente certains inconvénients qu’il faut prendre en compte lorsqu’il s’agit de la navigation au clavier/des boîtes de commutation:
https://github.com/googlecodelabs/Android-accessibility/issues/4
Les groupes de discussion basés sur ViewGroups
fonctionnent toujours dans ConstraintLayout
, de sorte que vous puissiez remplacer LinearLayouts
et RelativeLayouts
avec ConstraintLayouts
et TalkBack fonctionnera toujours comme prévu. Mais, si vous essayez d'éviter imbricationViewGroups
dans ConstraintLayout
, tout en respectant l'objectif de conception d'une hiérarchie à vue plane, voici une façon de le faire.
Déplacez la TextViews
du focus ViewGroup
que vous mentionnez directement dans le niveau supérieur ConstraintLayout
. Nous allons maintenant placer un simple View
transparent sur ces TextViews
en utilisant ConstraintLayout
contraintes. Chaque TextView
sera un membre du plus haut niveau ConstraintLayout
, de sorte que la présentation sera plate. Comme la superposition est au-dessus de la TextViews
, elle recevra tous les événements tactiles avant la TextViews
sous-jacente. Voici la structure de mise en page:
<ConstaintLayout>
<TextView>
<TextView>
<TextView>
<View> [overlays the above TextViews]
</ConstraintLayout>
Nous pouvons maintenant spécifier manuellement une description du contenu de la superposition qui est une combinaison du texte de chacun des TextViews
sous-jacents. Pour empêcher chaque TextView
d’accepter la mise au point et de prononcer son propre texte, nous allons définir Android:importantForAccessibility="no"
. Lorsque nous touchons la vue de superposition, nous entendons le texte combiné du TextViews
parlé.
Ce qui précède est la solution générale mais, mieux encore, serait une implémentation d’une vue de superposition personnalisée qui gérera les choses automatiquement. La superposition personnalisée illustrée ci-dessous suit la syntaxe générale de l'assistant Group
dans ConstraintLayout
et automatise une grande partie du traitement décrit ci-dessus.
La superposition personnalisée fait ce qui suit:
Group
de ConstraintLayout
. View.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO)
sur chaque vue. (Cela évite d'avoir à le faire manuellement.)contentDescription
, getText()
ou de hint
. (Cela évite de devoir le faire manuellement. Un autre avantage est qu'il détectera également les modifications apportées au texte pendant l'exécution de l'application.)La vue de superposition doit encore être positionnée manuellement dans le XML de présentation pour superposer la TextViews
.
Voici un exemple de présentation montrant l’approche ViewGroup
mentionnée dans la question et la superposition personnalisée. Le groupe de gauche est l'approche traditionnelle ViewGroup
démontrant l'utilisation d'un ConstraintLayout
intégré; La droite est la méthode de superposition utilisant le contrôle personnalisé. Le TextView
en haut intitulé "focus initial" est juste là pour capturer le focus initial afin de faciliter la comparaison des deux méthodes.
Avec le ConstraintLayout
sélectionné, TalkBack parle "Artiste, chanson, album".
Avec la superposition de vue personnalisée sélectionnée, TalkBack parle également "Artiste, chanson, album".
Vous trouverez ci-dessous un exemple de présentation et le code de la vue personnalisée. Mise en garde: bien que cette vue personnalisée fonctionne dans le but indiqué en utilisant TextViews
, elle ne remplace pas de manière robuste la méthode traditionnelle. Par exemple: la superposition personnalisée exprimera le texte des types de vues étendus TextView
comme EditText
alors que la méthode traditionnelle ne le fait pas.
Voir le exemple de projet sur 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
Définissez les attributs personnalisés pour la vue de superposition personnalisée.
<resources>
<declare-styleable name="AccessibilityOverlay">
<attr name="accessible_group" format="string" />
</declare-styleable>
</resources>
Assurez-vous que la variable ConstraintLayout
est définie sur focusable avec un description du contenu explicite. Assurez-vous également que les enfants TextViews
sont non définis sur focusable, sauf si vous souhaitez qu'ils soient lus indépendamment.
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 vous préférez définir de manière dynamique la description du contenu de ConstraintLayout dans le code, vous pouvez concaténer les valeurs de texte de chaque TextView
pertinente:
String description = tvArtist.getText().toString() + ", "
+ tvSong.getText().toString() + ", "
+ tvAlbum.getText().toString();
constraintLayout.setContentDescription(description);
Lorsque vous activez Talkback, ConstraintLayout se concentre maintenant et lit sa description de contenu.
Capture d'écran avec Talkback affiché en légende:
Voici le code XML complet pour l'exemple ci-dessus. Notez que les attributs de description de contenu et de focus ne sont définis que dans ConstraintLayout parent, et non dans TextViews enfant. Cela a pour effet que TalkBack ne se concentre jamais sur les vues enfants individuelles, mais uniquement sur le conteneur parent (donc, ne lisant que la description du contenu de ce 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>
Éléments de focus imbriqués
Si vous voulez que vos TextViews non liés puissent être centrés indépendamment du parent ConstraintLayout, vous pouvez également définir ces TextViews sur focusable=true
. Cela fera en sorte que ces TextViews puissent être mis au point et lus individuellement, après le ConstraintLayout.
Si vous souhaitez regrouper les TextViews non liées dans une annonce TalkBack unique (distincte de ConstraintLayout), vos options sont limitées:
ViewGroup
, avec sa propre description de contenu, oufocusable=true
uniquement sur le premier élément non associé et définissez sa description de contenu comme une annonce unique pour ce sous-groupe (par exemple, "éléments non associés").L'option 2 serait considérée un peu comme un hack, mais vous permettrait de maintenir une hiérarchie de vues à plat (si vous voulez vraiment éviter l'imbrication).
Mais si vous implémentez plusieurs sous-groupes d'éléments prioritaires, la méthode la plus appropriée consiste à organiser les groupes en tant que ViewGroups imbriqués. Selon la documentation d'accessibilité Android sur groupements naturels :
Pour définir le modèle de mise au point approprié pour un ensemble de contenu associé, placez chaque élément de la structure dans son propre groupe de vues focusable
Définissez la disposition de la contrainte sur focusable (en définissant Android: focusable = "true" dans la présentation de la contrainte)
Définir la description du contenu sur Mise en forme des contraintes
set focusable = "false" pour les vues à ne pas inclure.
Édition basée sur les commentaires Applicable uniquement s'il existe un seul groupe de focus dans la disposition des contraintes.