Как создать доступные фокус-группы в ConstraintLayout?

Представь, что у тебя есть LinearLayout внутри RelativeLayout который содержит 3 TextViews с 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 все равно был бы сам по себе и не читал, что такое поведение я хочу добиться.

(См. Пример Google Codelabs для справки)

Я сейчас пытаюсь воссоздать это поведение с 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?

[EDIT]: кажется, что единственный способ создать решение - это использовать "focusable = true" для внешнего ConstraintLayout и / или "focusable = false" для самих представлений. Это имеет некоторые недостатки, которые следует учитывать при работе с клавиатурой / переключателями:

https://github.com/googlecodelabs/android-accessibility/issues/4

8 ответов

Фокус-группы, основанные на 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 и автоматизирует большую часть обработки, описанной выше.

Пользовательский оверлей делает следующее:

  1. Принимает список идентификаторов, которые будут сгруппированы по элементу управления, как Group помощник ConstraintLayout,
  2. Отключает доступ для сгруппированных элементов управления, устанавливая View.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO) на каждом взгляде. (Это позволяет избежать необходимости делать это вручную.)
  3. При щелчке пользовательский элемент управления представляет конкатенацию текста сгруппированных представлений в каркасе специальных возможностей. Текст, собранный для просмотра, либо из 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>

Недавно я столкнулся с той же проблемой, и я решил реализовать новый класс с помощью новых помощников ConstraintLayout (доступных начиная с limitintlayout 1.1), чтобы мы могли использовать его так же, как мы используем представление группы.

Реализация является упрощенной версией ответа Четикампа и его идеи создания нового представления, которое будет обрабатывать доступность.

Вот моя реализация:

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.

Представление прозрачно, но вы можете выбрать область, на которой оно отображается, когда сфокусировано, используя обычные атрибуты макета ограничения.
В моем примере группа специальных возможностей отображается поверх полной ConstraintLayout, но вы можете настроить ее для соответствия некоторым или всем ссылочным представлениям, изменив app:"layout_constraint..." атрибутов.

Задать описание содержимого

Убедитесь, что ConstraintLayout устанавливается для фокусировки с явным описанием содержимого. Также убедитесь, что ребенок TextViews не установлены для фокусировки, если вы не хотите, чтобы они считывались независимо.

XML

<ConstraintLayout
  android:focusable="true"
  android:contentDescription="artist, song, album">

    <TextView artist/>
    <TextView song/>
    <TextView album/>
    <TextView unrelated 1/>
    <TextView unrelated 2/>

</ConstraintLayout>

Джава

Если вы предпочитаете динамически задавать описание содержимого ConstraintLayout в коде, вы можете объединить текстовые значения из каждого соответствующего TextView:

String description = tvArtist.getText().toString() + ", " 
    + tvSong.getText().toString() + ", "
    + tvAlbum.getText().toString();

constraintLayout.setContentDescription(description);

Результаты доступности

Когда вы включите Talkback, ConstraintLayout теперь сфокусируется и прочитает описание его содержимого.

Снимок экрана с Talkback отображается в виде заголовка:

Снимок экрана теста доступности

Детальное объяснение

Вот полный XML-код для приведенного выше снимка экрана. Обратите внимание, что атрибуты focusable и content description устанавливаются только в родительском ConstraintLayout, а не в дочерних TextViews. Это заставляет TalkBack никогда не фокусироваться на отдельных дочерних представлениях, а только на родительском контейнере (таким образом, считывая только описание содержимого этого родителя).

<?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>

Вложенные предметы фокуса

Если вы хотите, чтобы ваши несвязанные TextViews были фокусируемыми независимо от родительского ConstraintLayout, вы можете установить эти TextViews в focusable=true также. Это приведет к тому, что эти TextViews станут фокусируемыми и будут считываться индивидуально после ConstraintLayout.

Если вы хотите сгруппировать несвязанные TextViews в отдельное объявление TalkBack (отдельно от ConstraintLayout), ваши возможности ограничены:

  1. Либо вложите несвязанные взгляды в другое ViewGroup, с собственным описанием контента, или
  2. Задавать focusable=true только для первого несвязанного элемента и задайте его описание контента в качестве одного объявления для этой подгруппы (например, "несвязанные элементы").

Вариант № 2 будет считаться чем-то вроде взлома, но он позволит вам поддерживать иерархию плоского представления (если вы действительно хотите избежать вложения).

Но если вы реализуете несколько подгрупп фокусирующих элементов, более подходящим способом было бы организовать группировки как вложенные ViewGroups. Согласно документации о доступности Android для естественных группировок:

Чтобы определить правильный шаблон фокусировки для набора связанного содержимого, поместите каждый фрагмент структуры в свою собственную фокусируемую ViewGroup.

Представлен Android android:screenReaderFocusableдля группировки содержимого в макете ограничения. Это будет работать в вышеупомянутом случае. Но требует API 27 уровня.

https://developer.android.com/guide/topics/ui/accessibility/principles

ТОЛЬКО в формате XML.

В моей конкретной ситуации представления были сгруппированы по доступности по умолчанию, и я установил для поля «важно для доступности» значение «да». это ничего не дало,

Но когда я перешел к КАЖДОМУ представлению и соответственно установил ImportantForAccessibility

android: importantForAccessibility="yes"если вы хотите, чтобы это было объявлено

android: importantForAccessibility="no"если вы не хотите, чтобы об этом объявляли - это решило мою проблему.

Я сделал версию ответа Жюльена Арзула, которая не требует ручной установки ограничений для невидимого представления. Класс расширяет класс Layer ConstraintLayout, который автоматически адаптируется для отображения по указанным идентификаторам представлений.

          import android.content.Context
    import android.util.AttributeSet
    import android.view.View
    import android.view.accessibility.AccessibilityEvent
    import androidx.constraintlayout.helper.widget.Layer
    import androidx.constraintlayout.widget.ConstraintLayout
    import androidx.core.view.isVisible
    
    /**
     * This class can be used inside ConstraintLayouts to aggregate a particular group of its children into a single accessibility
     * focus group so they are all read together, in the same swipe stop.
     * This creates an invisible view that is drawn above all the specified children and receives focus in their place, reading all
     * of their descriptions in sequence.
     * The children's ids should be specified just like in a ConstraintLayout's Group, with app:constraint_referenced_ids.
     */
    
    class ConstraintLayoutFocusGroup @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
    ) : Layer(context, attrs, defStyleAttr) {
    
        init {
            importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
            isScreenReaderFocusable = true
        }
    
        override fun updatePreLayout(container: ConstraintLayout) {
            super.updatePreLayout(container)
    
            if (this.mReferenceIds != null) {
                this.setIds(this.mReferenceIds)
            }
    
            val children = mIds.map { container.getViewById(it) }
            makeNotImportantForAccessibility(children)
        }
    
        override fun onPopulateAccessibilityEvent(event: AccessibilityEvent) {
            super.onPopulateAccessibilityEvent(event)
    
            val constraintLayoutParent = parent as? ConstraintLayout
            if (constraintLayoutParent != null) {
                event.text.clear()
    
                val children = mIds.map { constraintLayoutParent.getViewById(it) }
                populateWithChildrenInfo(event, children)
            }
        }
    
        private fun makeNotImportantForAccessibility(views: Iterable<View?>) {
            views.filterNotNull().forEach { child ->
                child.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
            }
        }
    
        private fun populateWithChildrenInfo(event: AccessibilityEvent, views: Iterable<View?>) {
            views.filterNotNull().forEach { view ->
                if (view.isVisible) {
                    view.onPopulateAccessibilityEvent(event)
                }
            }
        }
    }

Однако в последнее время я столкнулся с проблемой (которая также случается с версией Жюльена), и если у вас есть решение, дайте мне знать.

Начиная с версии 13 Talkback имеет функцию, которая распознает текст/значки/изображения без описаний контента и выполняет распознавание текста на них, говоря то, что он распознал в представлениях, поэтому с помощью этого решения Talkback сначала читает описание нашего невидимого представления, как мы хотели, но после этого он также читает содержимое представлений мы пытались скрыть от Talkback с помощью важногоForAccessibility="no".

В итоге он говорит что-то вроде: « textViewOneText, textViewTwoText; Обнаружено: text textViewOneText textViewTwoText ».

  1. Установите макет ограничения как фокусируемый (установив android:focusable="true" в макете ограничения)

  2. Установить описание содержимого в макет ограничения

  3. set focusable="false" для представлений, которые не должны быть включены.

Редактировать на основе комментариев Применяется только в том случае, если в макете ограничений есть одна фокус-группа.

Я последовал решению @Victor Raft, и оно отлично сработало, как он и сказал. Однако я также столкнулся с той же проблемой, о которой он говорит в своем ответе. Наконец-то мне удалось найти решение/обойти эту проблему.

Чтобы добавить некоторый контекст, проблема заключалась в том, что TalkBack версии 13 автоматически распознавал тексты/изображения/значки и читал их, если они не помечены. Поэтому моя работа заключалась в том, чтобы установить contentDescription пользовательского представления.

Теперь это будет работать, только если взять textViews и добавить их в contentDescription этого нового наложения/настраиваемого представления. Ниже я приведу обновленный мной метод, в котором есть мои изменения. Надеюсь, это поможет!

      override fun onPopulateAccessibilityEvent(event: AccessibilityEvent) {
    super.onPopulateAccessibilityEvent(event)
    var contentDescription = ""
    val constraintLayoutParent = parent as? ConstraintLayout
    if (constraintLayoutParent != null) {
        event.text.clear()

        mIds.forEach { id ->
            val view: View? = constraintLayoutParent.getViewById(id)
            // Adds this View to the Accessibility Event only if it is currently visible
            if (view?.isVisible == true) {
                view.onPopulateAccessibilityEvent(event)
            }
            (view as? TextView)?.text?.let {
                contentDescription += it
            }
        }
        this.contentDescription = contentDescription
    }
}
Другие вопросы по тегам