Управление жестами: Обработка визуальных перекрытий. Часть 2

Автор оригинала: Chris Banes
  • Перевод
В преддверии старта продвинутого курса по Android-разработке продолжаем делиться с вами серией полезных переводов.





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

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

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

Insets


Термин insets, как правило, вызывает страх у Android – разработчиков, так как когда-то во времена Android Lollipop они обязательно пытались использовать рабочую области строки состояния. Например, вот этот старый вопрос на StackOverflow имеет очень большое количество просмотров.

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

В Android insets представлены классом WindowInsets, а в AndroidX -классом WindowInsetsCompat. Начиная с Android Q у нас появляются 5 типов insets, которые мы можем использовать при создании экранов приложения. Какой именно тип inset использовать зависит от ситуации, поэтому давайте рассмотрим каждый тип по отдельности и разберемся.

System window inset


Метод: getSystemWindowInsets()

System window inset – наиболее распространенный тип inset на сегодняшний день. Они существуют с API 1 в различных формах и оказываются в иерархии view всякий раз, когда UI системы отображается поверх вашего приложения (по оси z). Частые примеры – строка состояния и панель навигации, а иногда и экранная клавиатура (IME).

Давайте рассмотрим пример, в котором вам придется использовать system window insets. У нас уже есть FloatingActionButton (FAB), которая расположена в нижнем углу экрана с отступом в 16dp (в соответствии с гайдлайнами).


FAB в приложении Google I/O, прежде чем оно преобразится в edge-to-edge

После того, как мы выполним 1 и 2 шаги из прошлой статьи, наши view окажутся за панелью навигации:


FAB в приложении Google I/O, после растягивания на весь экран

Теперь вы видите, что ваше расписание конференций располагается за панелью навигации, и это именно то, чего мы добиваемся – создание более иммерсивного опыта. Более подробно о том, как работать со списками/сетками мы рассмотрим позже.

Вернемся к примеру. Теперь вы видите, что FAB спрятана, а это в свою очередь значит, что пользователь не сможет на нее нажать. Именно этого конфликта отображения мы хотим избежать. Пример выглядит яснее, когда мы используем кнопочную навигацию (как на картинке), поскольку так панель располагается выше. В навигации жестами с динамической адаптацией цвета это сработает, но помните, что система может переключиться на полупрозрачный scrim в любой момент времени, что может нарушить опыт взаимодействия.
Сейчас хороший момент, чтобы сказать вам о том, как важно тестировать свое приложение во всех режимах навигации.

Итак, как же нам справиться с этим визуальным конфликтом? В этот момент в игру вступают system window insets. Они сообщат вам, где в вашей иерархии view расположены системные панели, и эти значения вы сможете использовать для перемещения view подальше от системных панелей.

В приведенном выше примере FAB располагается рядом с нижним правым краем, поэтому мы можем использовать значения systemWindowInsets.bottom и systemWindowInsets.right, чтобы увеличить отступы view с каждой стороны, дабы переместить его подальше от панели навигации.

Как только мы это сделаем, мы получим следующее:


О том, как это реализуется мы поговорим немного позже.

TL; DR: System window insets лучше всего подходят для перемещения/создания отступов интерактивных views, которые не должны быть закрыты системными панелями.

Tappable element insets


Метод: getTappableElementInsets()

Дальше по списку у нас tappable element insets, которые только появились в Android Q. Они очень похожи на system window insets выше, однако они реагируют на варьирующуюся видимость панели навигации.

TL;DR: что касается tappable element insets: вы можете вообще их проигнорировать и использовать вместо них system window insets. Вы можете перейти к разделу Gesture Insets ниже или продолжить чтение.

Tappable element insets определяют минимальные insets, которые нужно применить в интерактивным (tappable) view. «Минимальные» в этом случае означает, что примененное значение все еще может привести к конфликту с системными панелями. Этим они и отличаются от system window insets, которые всегда нацелены на избежание конфликтов с системными панелями.

Давайте используем наш пример с FloatingActionButton, чтобы показать разницу в значениях:


Розовым цветом показаны границы панели навигации. Зеленым – границы FAB с конкретным отступом от нижнего поля.



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

Мы могли заметить, что tappable element insets и system gesture insets ведут себя одинаково, когда устройство находится в режиме кнопочной навигации. Ключевая разница становится видна, когда устройство использует управление жестами и у него включена динамическая адаптация цвета. В этом случае панель навигации прозрачна, а это значит, что в ней теоретически можно расположить интерактивные views, поэтому отступ от низа равен 0.

Несмотря на то, что insets не имеют представления о том, где должны быть расположены views, поэтому используя tappable element insets в теории вы можете получить нечто подобное:



Получилось не идеально, поскольку view располагается очень близко к панели навигации, что будет не очень удобно пользователю.

На практике же почти все варианты использования tappable element insets лучше обрабатываются с помощью system window insets.

Gesture insets


Методы: getSystemGestureInsets() и getMandatorySystemGestureInsets()

Следующий тип insets, который мы рассмотрим – это gesture insets, добавленные в версии Android Q. Напоминаю, что Android Q представляет новый режим управления жестами, который позволяет пользователю управлять устройством с помощью двух сенсорных жестов, которые можно выполнить следующим образом:

  1. Проведите пальцем горизонтально от одного из краев дисплея. Это запустит действие возврата.
  2. Проведите пальце вверх от нижнего края дисплея. Это позволит пользователю перейти на свой домашний экран или к последним использованным приложениям.


Демонстрация управления жестами в Android Q

System gesture insets отражают области окна, в которых системные жесты имеют приоритет над жестами-касаниями в вашем приложении. Возможно, вы заметили, что выше я указал два метода. Это связано с тем, что на самом деле существует два типа system gesture insets: один из них хранит все области жестов, а второй – подмножество, содержащее обязательные из system gesture insets.

System gesture insets


Для начала у нас есть system gesture insets. Они содержат все области на экране, где системные жесты имеют приоритет над жестами вашего приложения. В Android Q это значит, что insets будут выглядеть примерно так, то есть содержать отступ от нижнего края для жеста возврата к домашнему экрану, отступы слева и справа для жеста «назад»:

       0
    +--------------+
    |              |
    |   System     |
 40 |   Gesture    |  40
    |   Insets     |
    |              |
    +--------------+
           60

Когда вам пригодятся system gesture insets? Эти insets указывают, где жесты системы имеют приоритет, поэтому вы можете использовать их для проактивного перемещения любых view, которым требуется жест свайпа.

Примерами могут послужить выдвигающийся снизу экран, свайпы в играх, карусели (по типу ViewPager). В целом, вы можете использовать эти insets для перемещения/создания отступов от краев экрана.

Обязательные system gesture insets


Обязательные system gesture insets являются подмножеством system gesture insets и содержат только области, которые нельзя убрать из приложения (например, название). Мы заглянули немного вперед в тему следующей статьи, где мы будем говорить об обработке конфликтов жестов, но в целях понимания текущей статьи просто знайте, что приложения могут убрать системные жесты из некоторых областей экрана.

Обязательные system gesture insets указывают на области экрана, в которых системные жесты имеют приоритет всегда и являются обязательными. На Android Q единственной обязательной областью на данный момент является область жеста возврата на домашний экран в нижней части экрана. Это нужно, чтобы пользователь всегда мог выйти из приложения.
Взглянув на пример gesture insets устройства на Android Q, вы увидите следующее:

        0                              0  
    +--------------+               +--------------+
    |              |               |   Mandatory  |
    |   System     |               |   System     |
 40 |   Gesture    | 40          0 |   Gesture    | 0
    |   Insets     |               |   Insets     |
    |              |               |              |
    +--------------+               +--------------+
           60                             60

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

Stable insets


Метод: getStableInsets()

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

Stable insets относятся к system window insets, но они обозначают, где интерфейс системы может отображаться поверх вашего приложения, а не где он отображается в принципе. Stable insets в основном используются, когда интерфейс системы настроен таким образом, что его видимость может быть включена либо выключена, например, при использовании режимов lean back или immersive (например, игры, просмотр фотографий и видеоплееры).

Обработка insets


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

Основным методом доступа к WindowInsets является метод setOnApplyWindowInsetsListener. Давайте посмотрим на пример view, к которому мы хотим добавить отступы, чтобы он не отображался за панелью навигации:

ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
    v.updatePadding(bottom = insets.systemWindowInsets.bottom)
    // Return the insets so that they keep going down the view hierarchy
    insets
}

Здесь мы просто настраиваем нижний отступ view на значение отступа от низа system window inset.

Примечание: Если вы делаете это на ViewGroup, вероятно, вы захотите установить android:clipToPadding="false". Это связано с тем, что все виды clip drawing по умолчанию без отступов. Этот атрибут обычно используется с RecyclerView, который мы рассмотрим подробнее в следующей статье.

Убедитесь, что ваша функция прослушивания идемпотентична. Если она вызывается несколько раз с одинаковыми insets, результат каждый раз должен быть идентичным. Пример неидемпотентичной функции приведен ниже:

ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
    v.updatePadding(bottom = v.paddingBottom + insets.systemWindowInsets.bottom)
    insets
}

Здесь не следовало увеличивать (ala +=) в отступе view каждый раз, когда вызывается функция прослушивания. Передача insets окна может случиться в любой момент и несколько раз в течение жизни иерархии views.

Jetpack


Еще одна вещь, которую я рекомендую помнить про insets, это использование класса WindowInsetsCompat из Jetpack, независимо от вашей минимальной версии SDK. WindowInsets API улучшалось и расширялось на протяжении многих лет, а версия compat обеспечивает согласованность API и поведение на всех уровнях API.

Там, где затрагиваются новые типы insets, доступные в Android Q, метод compat представляет собой набор значений, которые являются валидными для хост-устройства на всех уровнях API. Чтобы получить доступ к новым API в Android X обновите его до androidx.core:core:1.2.0-xxx (сейчас в alpha) или выше. Здесь вы можете найти самую последнюю версию.

Пойдем еще дальше


Методы о которых я упоминал выше – это самый простой способ использования WindowInsets[Compat] API, но они могут сделать ваш код слишком длинным и шаблонным. Ранее я уже писал статью, в которой подробно описывал методы, позволяющие резко повышать эргономику обработки оконных insets с помощью биндинга адаптеров. Здесь вы можете прочитать ее.

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

На этом все. Ждем всех на бесплатном вебинаре, в рамках которого наш специалист подробно расскажет о курсе.
OTUS. Онлайн-образование
Цифровые навыки от ведущих экспертов

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

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

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