Управление жестами: обработка конфликтов жестов. Часть 3

Автор оригинала: Chris Banes
  • Перевод
Перевод статьи подготовлен в преддверии старта продвинутого и базового курсов по Android-разработке.




Это третья статья из серии про управление жестами. Вы можете ознакомиться с переводами первой и второй части, если пропустили их.

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

Что мы понимаем под конфликтами жестов? Давайте рассмотрим это на примере. Допустим, у нас есть музыкальный проигрыватель, который позволяет пользователю проматывать текущую песню, перетаскивая SeekBar.



К сожалению, SeekBar находится слишком близко к области жеста возврата на домашний экран, из-за чего начинает отрабатывается жест быстрого переключения на предыдущее приложение (QuickSwitch), что доставляет пользователю неудобства.

То же самое может произойти на любом краю экрана, где располагаются области жестов. Существует множество распространенных примеров, которые могут вызвать конфликты, такие как: Navigation drawers (DrawerLayout), карусели (ViewPager), ползунки (SeekBar), свайп в списках.

Что подводит нас к вопросу «как мы можем это исправить?». Чтобы облегчить этот вопрос, мы создали блок-схему, которая предлагает вам ответ исходя из ситуации.


Вы можете найти PDF версию блок-схемы здесь.

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

1. Необходимо ли приложению скрывать панели навигации и состояния?


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

Возможные причины ответить «да» на этот вопрос:

  • Вы используете флаг WindowManager FLAG_FULLSCREEN. Обратите внимание, что это также можно сделать с помощью атрибута темы android:windowFullscreen или расширить из одного из вариантов темы Theme.XXX.Fullscreen.
  • Вы используете флаг видимости system-ui SYSTEM_UI_FLAG_FULLSCREEN.
  • Вы используете один из флагов видимости system-ui в иммерсивном режиме: SYSTEM_UI_FLAG_IMMERSIVE или SYSTEM_UI_FLAG_IMMERSIVE_STICKY.

Распространенными примерами приложений, которым следует ответить «да» на этот вопрос, являются игры, видеоплееры, средства просмотра фотографий, приложения для рисования.

2. Основной сценарий использования UI предполагает свайпы в/около области жестов?


Этот вопрос выясняет, содержит ли ваш UI какие-либо элементы в/рядом с областями жестов (как “назад”, так и “домой”), которые пользователь должен свайпать.

Игры обычно отвечают «да» в связи с тем, что

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

Помимо игр, распространенные примеры UI, которые должны “да”, это:

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

3. Часто используемые view в/около области жестов?


Надеюсь, это достаточно простой вопрос. Сюда также относятся view, которые покрывают область жестов, а затем распространяются на большую часть экрана, например DrawerLayout или большой ViewPager.

4. View предполагает свайпы/ перетаскивание?


Мы немного меняем тактику и начинаем смотреть на отдельные view. Для любого из view, для которого вы ответили утвердительно на третий вопрос, делаем небольшое уточнение: должен ли пользователь свайпать/ перетаскивать его?

Есть много примеров, где вам нужно ответить «да»: SeekBars, BottomSheet или даже PopupMenu (нужно перетащить, чтобы открыть).

5. View полностью/в целом располагается под областями жестов?


Исходя из четвертого вопроса, мы теперь уточняем, является ли view полностью или в основном расположенным в области жестов.

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

На диаграмме выше вы могли заметить карусель на полную ширину экрана (ViewPager) в качестве примера отрицательного варианта ответа и задаться вопросом, почему этот случай не нужно обрабатывать. Это связано с тем, что области жестов слева/справа сравнительно малы по ширине (по умолчанию: 20dp каждая) по сравнению с шириной view. Типичная ширина экрана вашего телефона в портретной ориентации составляет ~ 360dp, оставляя ~ 320dp свободной области, при которой пользователь не испытывает затруднений (это почти 90% экрана). Даже с внутренними полями/отступами пользователь все равно сможет комфортно прокручивать карусель.

6. Границы view перекрывают какие-либо обязательные области жестов?


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

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

Типичными примерами являются:

  • Немодальные BottomSheet, так как они имеют тенденцию сворачиваться в небольшой перетаскиваемый view внизу экрана.
  • Горизонтально прокручивающаяся карусель в нижней части экрана, например, интерфейс со стикерами.

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

Нет конфликтов, которые нужно было бы обрабатывать


Давайте начнем с самого простого «решения», просто не делайте ничего!

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

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

Перемещение view из областей жестов


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

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


UI музыкального проигрывателя с SeekBar внизу экрана

Но когда пользователь пытается промотать песню, происходит это:


Запись системного жеста, конфликтующего с SeekBar

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



Простое решение


Самое простое решение здесь — добавить дополнительный отступ/поле, чтобы SeekBar переместился вверх из области жеста. Примерно так:



Если мы перетащим SeekBar в этом примере, вы увидите, что мы больше не активируем жест возврата домой:


SeekBar больше не конфликтует с нижним системным жестом.

Для реализации этого нам нужно использовать новые system gesture insets, доступные в API 29 и Jetpack Core library v1.2.0 (в настоящее время в альфе). В примере, мы увеличиваем нижний отступ SeekBar чтобы соответствовать значению нижнего gesture inset:

ViewCompat.setOnApplyWindowInsetsListener(seekBar) { view, insets ->
     // Мы установим для нижнего отступа view значение, взятое из нижнего system gesture insets
    view.updatePadding(
        bottom = insets.systemGestureInsets.bottom
    )
    insets
}

Если вам интересно узнать способы облегчения работы с WindowInsets, вы можете прочитать другую нашу статью по этой теме:

WindowInsets — Слушатели layout

Дальнейшие действия


На этом этапе вы можете решить, что дело уже сделано, и для некоторых layout это вполне может быть конечным решением. Но в нашем примере пользовательский интерфейс визуально регрессировал с большим количеством потерянного пространства под SeekBar. Таким образом, вместо того, чтобы просто вытеснить view наверх, мы можем вместо этого переработать layout, чтобы избежать потери пространства:


SeekBar перемещен в верхнюю часть панели воспроизведения.

Здесь мы переместили SeekBar в верхнюю часть панели воспроизведения, полностью вне области жестов. Это означает, что нам больше не нужно искусственно увеличивать высоту панели для размещения SeekBar.

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

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


В нашей предыдущей статье мы упоминали, что «приложения имеют возможность исключать системные жесты для определенных частей экрана». Приложения делают это с помощью API исключения жестов системы (gesture exclusion APIs), впервые появившемся в Android 10.

Существуют две различные функции, которые система предоставляет для исключения областей жестов: View.setSystemGestureExclusionRects() и Window.setSystemGestureExclusionRects(). Что вам следует использовать — зависит от приложения: если вы используете Android View, система предпочитает view API, в противном случае используйте Window API.

Основное различие между двумя API состоит в том, что Window API ожидает, что любые прямоугольники будут в координатном пространстве окна. Если вы используете view, вы, как правило, вместо этого будете работать в координатном пространстве view. View API заботится о преобразовании между координатными пространствами, то есть вам нужно рассуждать только с точки зрения содержания view.

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

Давайте посмотрим, что происходит, когда пользователь пытается промотать песню, когда «бегунок» SeekBar (круговой перетаскиватель) расположен рядом с одним из краев:


Конфликт SeekBar с областью жеста возврата назад

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

API исключения жестов обычно вызываются из двух мест: onDraw(), когда ваш view отрисовывается, и onLayout() в противном случае. Ваш view передает List<Rect>, содержащий все прямоугольники, которые следует исключить. Как упоминалось ранее, эти прямоугольники должны находиться в собственной системе координат view.

Обычно вы создаете функцию, подобную этой, которая будет вызываться из onLayout() и /или onDraw():

private val gestureExclusionRects = mutableListOf<Rect>()
private fun updateGestureExclusion() {
   // Пропускаем этот вызов, если мы не работаем на Android 10+
   if (Build.VERSION.SDK_INT < 29) return
  // Во-первых, давайте очистим все существующие прямоугольники
   gestureExclusionRects.clear()
      // Теперь давайте выясним, какие области следует исключить. Для SeekBar это будут границы бгунка.
   thumb?.also { t ->
       gestureExclusionRects += t.copyBounds()
   }
   // Если бы у нас были другие элементы около краев в этом view, мы могли бы исключить и их здесь, добавляя их границы в список
   // Наконец, передаем наш обновленный список прямоугольников в систему
   systemGestureExclusionRects = gestureExclusionRects
}

Полный пример можно найти здесь.

После того, как мы добавили это, перемотка около краев работает, как и ожидалось:


SeekBar, работающий в области жеста возврата назад

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

Ограничения


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

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

Поскольку поведение, которое обеспечивает API, может нарушать комфортный юзер экспириенс, система ограничивает его использование: приложения могут исключать только до 200dp на ребро.

Вот некоторые распространенные вопросы, возникающие у разработчиков, когда они слышат это:

Зачем нужно ограничение? Надеюсь, объяснение выше уже натолкнуло вас на причину. Мы считаем, что очень важно сохранять для пользователя возможность последовательно возвращаться назад с бокового свайпа. Последовательно по всему устройству, а не по одному приложению. Это ограничение может показаться слишком ограничивающим, но достаточно всего лишь одного приложения, исключающего весь край экрана, чтобы доставить пользователю неудобства, что приводит либо к удалению приложения, либо к чему-то более радикальному.

Иными словами, система навигации должна быть всегда последовательной и удобной в использовании.

Почему 200dp? Аргумент в пользу 200dp довольно прямой. Как мы упоминали ранее, API исключения жестов предназначены для использования в качестве крайнего случая, поэтому этот лимит был рассчитан как кратная сумма нескольких важных целей касания. Минимальный рекомендуемый размер для сенсорной цели 48dp.4 сенсорных цели × 48dp = 192dp. Добавьте еще немного отступов, и мы получим значение 200dp.

Что делать, если мне нужно исключить более 200dp на ребро? Система исключит только самые нижние 200dp, которые вы запросили.


Система разрешает запрос на общую высоту 200 dp, считая от нижнего края

Мой view за пределами экрана, учитывается ли это лимитом? Нет, система учитывает только исключенные прямоугольники, которые находятся в пределах экрана. Точно так же, если представление частично на экране, учитывается только видимая часть запрошенного прямоугольника.

Погружение в следующий пост


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





OTUS. Онлайн-образование
Цифровые навыки от ведущих экспертов

Комментарии 0

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое