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

1. Введение

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

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

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

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

  • Как использовать вставленные прослушиватели для представлений
  • Как использовать API исключения жестов
  • Как ведет себя режим погружения, когда жесты активны

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

Что ты построишь

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

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

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

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

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

Универсальный музыкальный проигрыватель Android (UAMP) — это пример приложения музыкального проигрывателя для Android, написанного на Kotlin. Он поддерживает такие функции, как фоновое воспроизведение, управление фокусом звука, интеграцию с Ассистентом и несколько платформ, таких как Wear, 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 сообщает приложению, где системный пользовательский интерфейс отображается поверх вашего контента, а также какие области экрана. Системные жесты имеют приоритет над жестами в приложении. Вставки представлены классом WindowInsets и классом WindowInsetsCompat в Jetpack . Мы настоятельно рекомендуем использовать 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 отлично работает в первый раз, когда вы добавляете сумму нижнего значения вставки к значению нижнего заполнения приложения, объявленному в activity_main.xml . Однако последующие вызовы продолжают добавлять нижнее значение вставки к нижнему отступу уже обновленного представления.

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

  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 с родительского на @+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 dp, исключите только большой палец панели поиска. Получите копию границ панели поиска и добавьте каждый объект в изменяемый список.

Просмотрите следующий пример кода 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. Поздравления

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

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

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

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

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