Семантика в Compose

Композиция описывает пользовательский интерфейс вашего приложения и создается путем запуска компонуемых объектов. Композиция представляет собой древовидную структуру, состоящую из составных элементов, описывающих ваш пользовательский интерфейс.

Рядом с композицией существует параллельное дерево, называемое деревом семантики . Это дерево описывает ваш пользовательский интерфейс альтернативным способом, понятным для служб специальных возможностей и платформы тестирования . Службы доступности используют дерево для описания приложения пользователям с конкретными потребностями. Платформа тестирования использует дерево для взаимодействия с вашим приложением и формирования утверждений о нем. Дерево семантики не содержит информации о том, как рисовать составные элементы, но содержит информацию о семантическом значении ваших составных элементов.

Типичная иерархия пользовательского интерфейса и ее семантическое дерево.
Рисунок 1. Типичная иерархия пользовательского интерфейса и ее семантическое дерево.

Если ваше приложение состоит из компонуемых компонентов и модификаторов из основы Compose и библиотеки материалов, дерево семантики заполняется и генерируется автоматически. Однако при добавлении пользовательских низкоуровневых компонуемых объектов вам придется вручную указывать их семантику . Также могут возникнуть ситуации, когда ваше дерево неправильно или не полностью отражает значение элементов на экране, и в этом случае вы можете адаптировать дерево.

Рассмотрим, например, этот пользовательский составной календарь:

Пользовательский календарь, включающий в себя выбираемые элементы дня.
Рис. 2. Пользовательский календарь, который можно составить из выбираемых элементов дня.

В этом примере весь календарь реализован как один низкоуровневый компонуемый объект с использованием компонуемого Layout и рисования непосредственно на Canvas . Если вы больше ничего не сделаете, службы специальных возможностей не получат достаточно информации о содержимом компонуемого объекта и выборе пользователя в календаре. Например, если пользователь щелкает день, содержащий 17, платформа специальных возможностей получает только информацию описания для всего элемента управления календарем. В этом случае служба специальных возможностей TalkBack объявит «Календарь» или, что немного лучше, «Апрельский календарь», и пользователю останется только гадать, какой день был выбран. Чтобы сделать этот составной объект более доступным, вам нужно будет добавить семантическую информацию вручную.

Свойства семантики

Все узлы в дереве пользовательского интерфейса, имеющие определенное семантическое значение, имеют параллельный узел в дереве семантики. Узел в дереве семантики содержит те свойства, которые передают значение соответствующего составного объекта. Например, составной объект Text содержит семантическое свойство text , потому что это значение этого составного объекта. Icon содержит свойство contentDescription (если оно установлено разработчиком), которое передает в тексте значение Icon . Компонуемые объекты и модификаторы, построенные на основе базовой библиотеки Compose, уже задают для вас соответствующие свойства. При необходимости установите или переопределите свойства самостоятельно с помощью модификаторов semantics clearAndSetSemantics . Например, добавьте к узлу пользовательские действия по обеспечению доступности , предоставьте альтернативное описание состояния для переключаемого элемента или укажите, что определенный составной текст следует рассматривать как заголовок .

Чтобы визуализировать дерево семантики, используйте инструмент Layout Inspector или метод printToLog() внутри тестов. Это распечатает текущее дерево семантики внутри Logcat.

class MyComposeTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun MyTest() {
        // Start the app
        composeTestRule.setContent {
            MyTheme {
                Text("Hello world!")
            }
        }
        // Log the full semantics tree
        composeTestRule.onRoot().printToLog("MY TAG")
    }
}

Результатом этого теста будет:

    Printing with useUnmergedTree = 'false'
    Node #1 at (l=0.0, t=63.0, r=221.0, b=120.0)px
     |-Node #2 at (l=0.0, t=63.0, r=221.0, b=120.0)px
       Text = '[Hello world!]'
       Actions = [GetTextLayoutResult]

Рассмотрим, как свойства семантики передают значение составного объекта. Рассмотрим Switch . Вот как это выглядит для пользователя:

Рисунок 3. Переключатель в состояниях «Включено» и «Выключено».

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

Именно для этого и используются свойства семантики. Узел семантики этого элемента Switch содержит следующие свойства, визуализируемые с помощью инспектора макета:

Инспектор макета, показывающий свойства семантики составного переключателя
Рис. 4. Инспектор макета, показывающий семантические свойства составного переключателя.

Role указывает тип элемента. StateDescription описывает, как следует ссылаться на состояние «Включено». По умолчанию это локализованная версия слова «Включено», но ее можно сделать более конкретной (например, «Включено») в зависимости от контекста. ToggleableState — это текущее состояние коммутатора. Свойство OnClick ссылается на метод, используемый для взаимодействия с этим элементом. Полный список свойств семантики см. в объекте SemanticsProperties . Полный список возможных действий доступности можно найти в объекте SemanticsActions .

Отслеживание семантических свойств каждого компонуемого объекта в вашем приложении открывает множество мощных возможностей. Некоторые примеры:

  • Talkback использует свойства для чтения вслух того, что отображается на экране, и позволяет пользователю плавно взаимодействовать с ним. Для составного переключателя Talkback может сказать: «Вкл.; Переключить; дважды коснитесь, чтобы переключиться». Пользователь может дважды коснуться экрана, чтобы выключить переключатель.
  • Платформа тестирования использует свойства для поиска узлов, взаимодействия с ними и создания утверждений. Пример теста для коммутатора может быть следующим:
    val mySwitch = SemanticsMatcher.expectValue(
        SemanticsProperties.Role, Role.Switch
    )
    composeTestRule.onNode(mySwitch)
        .performClick()
        .assertIsOff()

Объединенное и несвязанное дерево семантики

Как упоминалось ранее, каждый компонент в дереве пользовательского интерфейса может иметь ноль или более установленных семантических свойств. Если у составного объекта не установлены свойства семантики, он не включается в дерево семантики. Таким образом, дерево семантики будет содержать только те узлы, которые действительно содержат семантическое значение. Однако иногда, чтобы передать правильное значение того, что показано на экране, также полезно объединить определенные поддеревья узлов и рассматривать их как одно. Таким образом, вы можете рассуждать о наборе узлов в целом, вместо того, чтобы иметь дело с каждым узлом-потомком по отдельности. Как правило, каждый узел в этом дереве представляет собой фокусируемый элемент при использовании служб специальных возможностей.

Примером такого компонуемого объекта является Button . Вы можете рассматривать кнопку как один элемент, даже если она может содержать несколько дочерних узлов:

Button(onClick = { /*TODO*/ }) {
    Icon(
        imageVector = Icons.Filled.Favorite,
        contentDescription = null
    )
    Spacer(Modifier.size(ButtonDefaults.IconSpacing))
    Text("Like")
}

В дереве семантики свойства потомков кнопки объединяются, и кнопка представляется как один листовой узел в дереве:

Объединенное представление семантики одного листа
Рисунок 5. Объединенное представление семантики одного листа.

Составные объекты и модификаторы могут указать, что они хотят объединить свойства семантики своих потомков, вызвав Modifier.semantics (mergeDescendants = true) {} . Установка для этого свойства значения true указывает, что свойства семантики следует объединить. В примере Button составная Button использует внутренний модификатор clickable , который включает в себя этот модификатор semantics . Таким образом, узлы-потомки кнопки объединяются. Прочтите документацию по специальным возможностям, чтобы узнать больше о том, когда следует изменить поведение слияния в компонуемом объекте.

Это свойство установлено у нескольких модификаторов и компонуемых объектов в библиотеках Foundation и Material Compose. Например, clickable и toggleable модификаторы автоматически объединят своих потомков. Кроме того, составной элемент ListItem объединит своих потомков.

Осмотрите деревья

Семантическое дерево на самом деле представляет собой два разных дерева. Существует объединенное дерево семантики, которое объединяет узлы-потомки, если для mergeDescendants установлено значение true . Существует также несвязанное семантическое дерево, в котором слияние не применяется, но сохраняется каждый узел нетронутым. Службы доступности используют неслитое дерево и применяют свои собственные алгоритмы слияния с учетом свойства mergeDescendants . Платформа тестирования по умолчанию использует объединенное дерево.

Вы можете проверить оба дерева с помощью метода printToLog() . По умолчанию, как и в предыдущих примерах, объединенное дерево протоколируется. Чтобы вместо этого распечатать неслитое дерево, установите для параметра useUnmergedTree сопоставителя onRoot() значение true :

composeTestRule.onRoot(useUnmergedTree = true).printToLog("MY TAG")

Инспектор макета позволяет отображать как объединенное, так и несвязанное дерево семантики, выбрав предпочтительное дерево в фильтре представления:

Параметры представления Layout Inspector, позволяющие отображать как объединенное, так и несвязанное семантическое дерево.
Рисунок 6. Параметры представления Layout Inspector, позволяющие отображать как объединенное, так и несвязанное семантическое дерево.

Для каждого узла вашего дерева Инспектор макета отображает как объединенную семантику, так и семантику, установленную для этого узла на панели свойств:

Свойства семантики объединены и установлены
Рисунок 7. Свойства семантики объединены и установлены.

По умолчанию средства сопоставления в среде тестирования используют объединенное дерево семантики. Вот почему вы можете взаимодействовать с Button , сопоставляя текст, показанный внутри нее:

composeTestRule.onNodeWithText("Like").performClick()

Переопределите это поведение, установив для параметра useUnmergedTree сопоставителей значение true , как и в случае с сопоставителем onRoot .

Объединить поведение

Когда составной объект указывает, что его потомки должны быть объединены, как именно происходит это слияние?

Каждое семантическое свойство имеет определенную стратегию слияния. Например, свойство ContentDescription добавляет в список все дочерние значения ContentDescription. Проверьте стратегию слияния семантического свойства, проверив его реализацию mergePolicy в SemanticsProperties.kt . Свойства могут принимать родительское или дочернее значение, объединять значения в список или строку, вообще не разрешать слияние и вместо этого выдавать исключение или использовать любую другую пользовательскую стратегию слияния.

Важное примечание: потомки, которые сами установили mergeDescendants = true не включаются в слияние. Взгляните на пример:

Элемент списка с изображением, текстом и значком закладки.
Рисунок 8. Элемент списка с изображением, текстом и значком закладки.

Вот кликабельный элемент списка. Когда пользователь нажимает строку, приложение переходит на страницу сведений о статье, где пользователь может прочитать статью. Внутри элемента списка есть кнопка для добавления статьи в закладки, которая образует вложенный кликабельный элемент, поэтому кнопка отображается отдельно в объединенном дереве. Остальной контент в строке объединяется:

Объединенное дерево содержит несколько текстов в списке внутри узла «Строка». Необъединенное дерево содержит отдельные узлы для каждого составного текста.
Рисунок 9. Объединенное дерево содержит несколько текстов в списке внутри узла «Строка». Необъединенное дерево содержит отдельные узлы для каждого составного текста.

Адаптируйте дерево семантики

Как упоминалось ранее, вы можете переопределить или очистить определенные свойства семантики или изменить поведение дерева при слиянии. Это особенно актуально, когда вы создаете свои собственные компоненты. Без установки правильных свойств и поведения слияния ваше приложение может быть недоступно, а тесты могут вести себя иначе, чем вы ожидаете. Чтобы узнать больше о некоторых распространенных случаях использования, когда вам следует адаптировать дерево семантики, прочтите документацию по специальным возможностям . Если вы хотите узнать больше о тестировании, ознакомьтесь с руководством по тестированию .

Дополнительные ресурсы

{% дословно %} {% дословно %} {% дословно %} {% дословно %}