Навигация с помощью жестов и удобство работы от края до края

1. Введение

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

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

Цель этого практического занятия — научить вас использовать отступы (insets) для предотвращения конфликтов жестов. Кроме того, оно научит вас использовать API исключения жестов (Gesture Exclusion API) для элементов управления, таких как маркеры перетаскивания, которые должны находиться в зонах жестов.

Что вы узнаете

  • Как использовать обработчики событий для элементов представления (inset listeners)
  • Как использовать API исключения жестов
  • Как работает иммерсивный режим при активных жестах

Цель этого практического занятия — сделать ваше приложение совместимым с системными жестами. Ненужные концепции и блоки кода опущены и предоставлены для копирования и вставки.

Что вы построите

Универсальный музыкальный плеер для Android (UAMP) — это пример приложения для воспроизведения музыки на Android, написанного на Kotlin. Вам нужно будет настроить UAMP для навигации с помощью жестов.

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

Что вам понадобится

  • Устройство или эмулятор, работающий под управлением Android 10 или более поздней версии.
  • Android Studio

2. Обзор приложения

Universal Android Music Player (UAMP) — это пример приложения музыкального проигрывателя для Android, написанный на Kotlin. Он поддерживает такие функции, как фоновое воспроизведение, управление фокусом аудио, интеграция с Google Assistant и работу на нескольких платформах, таких как Wear OS, TV и Auto.

Рисунок 1 : Схема потока в UAMP

UAMP загружает музыкальный каталог с удалённого сервера и позволяет пользователю просматривать альбомы и песни. Пользователь нажимает на песню, и она воспроизводится через подключенные колонки или наушники. Приложение не предназначено для работы с системными жестами. Поэтому при запуске UAMP на устройстве с Android 10 или выше на начальном этапе могут возникнуть некоторые проблемы.

3. Подготовка к работе

Чтобы получить пример приложения, клонируйте репозиторий с GitHub и переключитесь на стартовую ветку:

$  git clone https://github.com/googlecodelabs/android-gestural-navigation/

В качестве альтернативы вы можете загрузить репозиторий в виде ZIP-файла, распаковать его и открыть в Android Studio.

Выполните следующие шаги:

  1. Откройте и соберите приложение в Android Studio.
  2. Создайте новое виртуальное устройство и выберите уровень API 29. В качестве альтернативы вы можете подключить реальное устройство, работающее под управлением API уровня 29 или выше.
  3. Запустите приложение. В появившемся списке песни сгруппированы в разделы «Рекомендованные» и «Альбомы» .
  4. Нажмите «Рекомендованные» и выберите песню из списка.
  5. Приложение начинает воспроизведение песни.

Включить навигацию жестами

Если вы запускаете новый экземпляр эмулятора с уровнем API 29, навигация жестами может быть отключена по умолчанию. Чтобы включить навигацию жестами, выберите «Системные настройки» > «Система» > «Системная навигация» > «Навигация жестами» .

Запустите приложение с навигацией жестами.

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

4. Делайте все встык.

Что такое "от края до края"?

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

Рисуйте за панелью навигации

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

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

  1. Панель навигации: Откройте res/values-29/styles.xml и установите navigationBarColor в color/transparent .
  2. Строка состояния: Аналогично установите statusBarColor в color/transparent .

Ознакомьтесь с приведенным ниже примером кода файла res/values-29/styles.xml :

<!-- change navigation bar color -->
<item name="android:navigationBarColor">
    @android:color/transparent
</item>

<!-- change status bar color -->
<item name="android:statusBarColor">
    @android:color/transparent
</item>

Флаги видимости системного пользовательского интерфейса

Также необходимо установить флаги видимости системного пользовательского интерфейса, чтобы указать системе размещать приложение под системными панелями. API-интерфейсы systemUiVisibility класса View позволяют устанавливать различные флаги. Выполните следующие шаги:

  1. Откройте класс MainActivity.kt и найдите метод onCreate() . Получите экземпляр fragmentContainer .
  2. Установите для content.systemUiVisibility следующие значения:

Ознакомьтесь с приведенным ниже примером кода из файла MainActivity.kt :

  val content: FrameLayout = findViewById(R.id.fragmentContainer)
  content.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
            View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
            View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION

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

  1. Запустите приложение и, чтобы перейти к экрану проигрывателя, выберите песню для воспроизведения.
  2. Убедитесь, что элементы управления проигрывателем отображаются под панелью навигации, что затрудняет к ним доступ:

  1. Перейдите в системные настройки, переключитесь обратно в режим навигации с тремя кнопками и вернитесь в приложение.
  2. Убедитесь, что пользоваться элементами управления еще сложнее с помощью трехкнопочной панели навигации: обратите внимание, что панель SeekBar скрыта за панелью навигации, а кнопки воспроизведения/паузы в основном закрыты панелью навигации.
  3. Поэкспериментируйте и изучите всё подробнее. После этого перейдите в системные настройки и вернитесь к навигации жестами:

741ef664e9be5e7f.gif

Приложение теперь отрисовывается от края до края, но есть проблемы с удобством использования, элементы управления приложения конфликтуют и перекрываются, и это необходимо исправить.

5. Вставки

WindowInsets указывают приложению, где системный интерфейс отображается поверх вашего контента, а также какие области экрана системные жесты имеют приоритет над жестами внутри приложения. В Jetpack отступы представлены классами WindowInsets и WindowInsetsCompat . Мы настоятельно рекомендуем использовать WindowInsetsCompat для обеспечения согласованного поведения на всех уровнях API.

Системные вставки и обязательные системные вставки

Ниже перечислены наиболее часто используемые типы вставок в API:

  • Системные отступы окон: они показывают, где отображается системный интерфейс поверх вашего приложения. Мы обсудим, как можно использовать системные отступы, чтобы переместить элементы управления подальше от системных панелей.
  • Системные области для жестов: они возвращают все области для жестов. Любые элементы управления жестами внутри приложения в этих областях могут случайно активировать системные жесты.
  • Обязательные вставки для жестов: они являются подмножеством системных вставок для жестов и не могут быть переопределены. Они указывают области экрана, где поведение системных жестов всегда будет иметь приоритет над жестами внутри приложения.

Используйте отступы для перемещения элементов управления приложения.

Теперь, когда вы знаете больше об API для вставки элементов, вы можете исправить элементы управления приложения, как описано в следующих шагах:

  1. Получите экземпляр playerLayout из экземпляра объекта view .
  2. Добавьте обработчик OnApplyWindowInsetsListener к элементу playerView .
  3. Отодвиньте представление от области жестов: найдите значение системного отступа для нижней части и увеличьте отступ представления на эту величину. Чтобы соответствующим образом обновить отступ представления, добавьте к [значению, связанному с отступом нижней части приложения] [значение, связанное со значением системного отступа для нижней части].

Ознакомьтесь с приведенным ниже примером кода из файла NowPlayingFragment.kt :

playerView = view.findViewById(R.id.playerLayout)
playerView.setOnApplyWindowInsetsListener { view, insets ->
   view.updatePadding(
      bottom = insets.systemWindowInsetBottom + view.paddingBottom
   )
   insets
}
  1. Запустите приложение и выберите песню. Обратите внимание, что элементы управления плеером, похоже, не меняются. Если вы добавите точку останова и запустите приложение в режиме отладки, вы увидите, что обработчик событий не вызывается.
  2. Чтобы исправить это, переключитесь на FragmentContainerView , который автоматически обрабатывает эту проблему. Откройте activity_main.xml и замените FrameLayout на FragmentContainerView .

Ознакомьтесь с приведенным ниже примером кода из файла activity_main.xml :

<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/fragmentContainer"
    tools:context="com.example.android.uamp.MainActivity"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>
  1. Запустите приложение еще раз и перейдите на экран проигрывателя. Элементы управления проигрывателем внизу экрана смещены от области управления жестами.

Теперь элементы управления приложения работают с навигацией жестами, но они перемещаются больше, чем ожидалось. Необходимо устранить эту проблему.

Сохраните текущие отступы и поля.

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

Это происходит потому, что приложение вызывает requestApplyInsets() каждый раз при запуске активности. Даже без этого вызова WindowInsets может быть отправлено несколько раз в любой момент времени в течение жизненного цикла представления.

Текущий InsetListener для playerView работает идеально при первом вызове, когда значение inset bottom добавляется к значению нижнего отступа приложения, объявленному в activity_main.xml . Однако последующие вызовы продолжают добавлять значение inset bottom к уже обновленному нижнему отступу представления.

Для устранения этой проблемы выполните следующие действия:

  1. Запишите начальное значение отступа представления. Создайте новый объект `val` и сохраните в нем начальное значение отступа представления для playerView непосредственно перед кодом обработчика событий.

Ознакомьтесь с приведенным ниже примером кода из файла NowPlayingFragment.kt :

   val initialPadding = playerView.paddingBottom
  1. Используйте это начальное значение для обновления нижнего отступа элемента, что позволит избежать использования текущего значения нижнего отступа приложения.

Ознакомьтесь с приведенным ниже примером кода из файла NowPlayingFragment.kt :

   playerView.setOnApplyWindowInsetsListener { view, insets ->
            view.updatePadding(bottom = insets.systemWindowInsetBottom + initialPadding)
            insets
        }
  1. Запустите приложение снова. Переключайтесь между приложениями и вернитесь на главный экран. После возвращения в приложение элементы управления плеером будут расположены прямо над областью жестов.

Переработан дизайн элементов управления приложения.

Ползунок прокрутки плеера расположен слишком близко к нижней области жестов, из-за чего пользователь может случайно активировать жест "Домой" при завершении горизонтального свайпа. Увеличение отступов может решить проблему, но это также может переместить плеер выше, чем хотелось бы.

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

  1. Откройте fragment_nowplaying.xml . Переключитесь в режим «Дизайн» и выберите ползунок SeekBar в самом низу:

74918dec3926293f.png

  1. Переключиться в режим просмотра кода.
  2. Чтобы переместить SeekBar в верхнюю часть playerLayout , измените layout_constraintTop_toBottomOf элемента SeekBar на parent .
  3. Чтобы привязать другие элементы в playerView к нижней части SeekBar , измените layout_constraintTop_toTopOf с parent на @+id/seekBar для media_button , title и position .

Ознакомьтесь с приведенным ниже примером кода из файла fragment_nowplaying.xml :

<androidx.constraintlayout.widget.ConstraintLayout
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:padding="8dp"
   android:layout_gravity="bottom"
   android:background="@drawable/media_overlay_background"
   android:id="@+id/playerLayout">

   <ImageButton
       android:id="@+id/media_button"
       android:layout_width="@dimen/exo_media_button_width"
       android:layout_height="@dimen/exo_media_button_height"
       android:background="?attr/selectableItemBackground"
       android:scaleType="centerInside"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintTop_toTopOf="@+id/seekBar"
       app:srcCompat="@drawable/ic_play_arrow_black_24dp"
       tools:ignore="ContentDescription" />

   <TextView
       android:id="@+id/title"
       android:layout_width="0dp"
       android:layout_height="wrap_content"
       android:layout_marginTop="8dp"
       android:layout_marginStart="@dimen/text_margin"
       android:layout_marginEnd="@dimen/text_margin"
       android:ellipsize="end"
       android:maxLines="1"
       android:textAppearance="@style/TextAppearance.Uamp.Title"
       app:layout_constraintTop_toTopOf="@+id/seekBar"
       app:layout_constraintLeft_toRightOf="@id/media_button"
       app:layout_constraintRight_toLeftOf="@id/position"
       tools:text="Song Title" />

   <TextView
       android:id="@+id/subtitle"
       android:layout_width="0dp"
       android:layout_height="wrap_content"
       android:layout_marginStart="@dimen/text_margin"
       android:layout_marginEnd="@dimen/text_margin"
       android:ellipsize="end"
       android:maxLines="1"
       android:textAppearance="@style/TextAppearance.Uamp.Subtitle"
       app:layout_constraintTop_toBottomOf="@+id/title"
       app:layout_constraintLeft_toRightOf="@id/media_button"
       app:layout_constraintRight_toLeftOf="@id/position"
       tools:text="Artist" />

   <TextView
       android:id="@+id/position"
       android:layout_width="0dp"
       android:layout_height="wrap_content"
       android:layout_marginTop="8dp"
       android:layout_marginStart="@dimen/text_margin"
       android:layout_marginEnd="@dimen/text_margin"
       android:ellipsize="end"
       android:maxLines="1"
       android:textAppearance="@style/TextAppearance.Uamp.Title"
       app:layout_constraintTop_toTopOf="@+id/seekBar"
       app:layout_constraintRight_toRightOf="parent"
       tools:text="0:00" />

   <TextView
       android:id="@+id/duration"
       android:layout_width="0dp"
       android:layout_height="wrap_content"
       android:layout_marginStart="@dimen/text_margin"
       android:layout_marginEnd="@dimen/text_margin"
       android:ellipsize="end"
       android:maxLines="1"
       android:textAppearance="@style/TextAppearance.Uamp.Subtitle"
       app:layout_constraintTop_toBottomOf="@id/position"
       app:layout_constraintRight_toRightOf="parent"
       tools:text="0:00" />

   <SeekBar
       android:id="@+id/seekBar"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
  1. Запустите приложение и взаимодействуйте с плеером и ползуном прокрутки.

Эти минимальные изменения в дизайне значительно улучшают приложение.

6. API исключения жестов

Исправлены конфликты управления жестами в области «Домой». Область жестов «Назад» также может создавать конфликты с элементами управления приложения. На следующем скриншоте показано, что ползунок прокрутки плеера в данный момент находится как в правой, так и в левой областях жестов «Назад»:

e6d98e94dcf83dde.png

SeekBar автоматически обрабатывает конфликты жестов. Однако вам может потребоваться использовать другие компоненты пользовательского интерфейса, которые вызывают конфликты жестов. В таких случаях вы можете использовать Gesture Exclusion API , чтобы частично исключить жест "назад".

Используйте API исключения жестов

Чтобы создать зону исключения жестов, вызовите метод setSystemGestureExclusionRects() для вашего представления, передав список объектов rect . Эти объекты rect соответствуют координатам исключенных прямоугольных областей. Этот вызов необходимо выполнить в методах onLayout() или onDraw() представления. Для этого выполните следующие шаги:

  1. Создайте новый пакет с именем view .
  2. Для вызова этого API создайте новый класс с именем MySeekBar и расширьте его классом AppCompatSeekBar .

Ознакомьтесь с приведенным ниже примером кода файла MySeekBar.kt :

class MySeekBar @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = android.R.attr.seekBarStyle
) : androidx.appcompat.widget.AppCompatSeekBar(context, attrs, defStyle) {

}
  1. Создайте новый метод с именем updateGestureExclusion() .

Ознакомьтесь с приведенным ниже примером кода файла MySeekBar.kt :

private fun updateGestureExclusion() {

}
  1. Добавьте проверку, чтобы пропустить этот вызов на уровне API 28 или ниже.

Ознакомьтесь с приведенным ниже примером кода файла MySeekBar.kt :

private fun updateGestureExclusion() {
        // Skip this call if we're not running on Android 10+
        if (Build.VERSION.SDK_INT < 29) return
}
  1. Поскольку API исключения жестов имеет ограничение в 200 точек, исключайте только ползунок ползунка. Получите копию границ ползунка и добавьте каждый объект в изменяемый список.

Ознакомьтесь с приведенным ниже примером кода файла MySeekBar.kt :

private val gestureExclusionRects = mutableListOf<Rect>()

private fun updateGestureExclusion() {
    // Skip this call if we're not running on Android 10+
    if (Build.VERSION.SDK_INT < 29) return

    thumb?.also { t ->
        gestureExclusionRects += t.copyBounds()
    }
}
  1. Вызовите функцию systemGestureExclusionRects() с созданными вами списками gestureExclusionRects .

Ознакомьтесь с приведенным ниже примером кода файла MySeekBar.kt :

private val gestureExclusionRects = mutableListOf<Rect>()

private fun updateGestureExclusion() {
    // Skip this call if we're not running on Android 10+
    if (Build.VERSION.SDK_INT < 29) return

    thumb?.also { t ->
        gestureExclusionRects += t.copyBounds()
    }
    // Finally pass our updated list of rectangles to the system
    systemGestureExclusionRects = gestureExclusionRects
}
  1. Вызовите метод updateGestureExclusion() из onDraw() или onLayout() . Переопределите метод onDraw() и добавьте вызов метода updateGestureExclusion .

Ознакомьтесь с приведенным ниже примером кода файла MySeekBar.kt :

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    updateGestureExclusion()
}
  1. Необходимо обновить ссылки SeekBar . Для начала откройте fragment_nowplaying.xml .
  2. Замените SeekBar на com.example.android.uamp.view.MySeekBar .

Ознакомьтесь с приведенным ниже примером кода из файла fragment_nowplaying.xml :

<com.example.android.uamp.view.MySeekBar
    android:id="@+id/seekBar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="parent" />
  1. Чтобы обновить ссылки на SeekBar в NowPlayingFragment.kt , откройте NowPlayingFragment.kt и измените тип positionSeekBar на MySeekBar . Чтобы тип переменной соответствовал типу переменной, измените обобщенные типы SeekBar для вызова findViewById на MySeekBar .

Ознакомьтесь с приведенным ниже примером кода из файла NowPlayingFragment.kt :

val positionSeekBar: MySeekBar = view.findViewById<MySeekBar>(
     R.id.seekBar
).apply { progress = 0 }
  1. Запустите приложение и взаимодействуйте с SeekBar . Если вы по-прежнему сталкиваетесь с конфликтами жестов, вы можете поэкспериментировать и изменить границы большого пальца в MySeekBar . Будьте осторожны, не создавайте зону исключения жестов больше, чем необходимо, поскольку это ограничивает другие потенциальные вызовы исключения жестов и приводит к непоследовательному поведению для пользователя.

7. Поздравляем!

Поздравляем! Вы научились избегать и разрешать конфликты с помощью системных жестов!

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

Теперь вы знаете основные шаги, необходимые для того, чтобы ваши приложения работали с системными жестами!

Дополнительные материалы

Справочная документация