Android top sheet behavior

Android: выдвигающийся экран снизу

Данная статья является переводом статьи Emrullah Luleci, а также её продолжения.

Нижний экран (Здесь и далее под «нижним экраном/слоем» будет подразумеваться элемент bottom sheet — прим. пер.) — компонент, выезжающий снизу экрана, который используется для отображения дополнительного контента. Подробнее об этом элементе можно узнать на официальной сайте посвященном материальному дизайну.

Зависимости

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

Создайте класс наследник от AppCompatActivity:

Создание макетов

Содержимое нижнего экрана

Для удобства воспользуемся макетами. Назовем файл с нижним слоем bottom_sheet.xml.

behavior_peekHeight: Определяет высоту видимой части.

behavior_hideable: Определяет, может ли нижний экран скрываться свайпом вниз.

Container view

Создайте CoordinatorLayout в качестве корневого вью. Добавьте в него прямым наследником bottom_sheet.xml. Элементы app_bar и activity_bottom_sheet_content не имеют прямого отношения к нижнему экрану, поэтому их можно заменить или удалить.

На данном этапе нижний экран должен работать примерно так:

Динамическое управление

Поведением и свойствами нижнего экрана можно также управлять динамически с помощью Java.

Прикрепление элементов к нижнему экрану

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

Добавим Floating Action Button в макет созданный выше. Новый компонент должен являться непосредственным наследником CoordinatorLayout также как и bottom_sheet. Для прикрепления элемента к нижнему экрану необходимо добавить app:layout_anchor с id вью нижнего экрана, а также app:layout_anchorGravity со значением top|end.

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

Скрытие плавающей кнопки при скроле

Для скрытия кнопки при скроле необходимо добавить слушатель к нижнему экрану и отображать/скрывать кнопку. Для начала найдем необходимые вью:

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

Результат обоих вариантов можно увидеть ниже:

Источник

Перехватываем всё с помощью CoordinatorLayout Behavior

Вы не продвинетесь далеко в изучении Android Design Support Library, не столкнувшись с CoordinatorLayout. Множество View из Design Library требуют CoordinatorLayout. Но почему? Сам по себе CoordinatorLayout делает не так уж и много, если использовать его с View, входящими в состав Android фреймворка, то он будет работать, как обычный FrameLayout. Так откуда берётся вся его магия? Вот где на сцену выходит CoordinatorLayout.Behavior. Подключив Behavior к дочерней View у CoordinatorLayout, вы сможете перехватывать касания, оконные вставки (window insets), изменения размеров и макета (measurement и layout), а также вложенную прокрутку. Design Library широко использует Behavior чтобы добавить силу большинству функционалу, которую вы видите.

Создание Behavior

Создать Behavior довольно просто: наследуем наш класс от Behavior:

Обратите внимание на generic тип, указанный в этом классе. В данном случае мы указываем, что можем подключить FancyBehavior к любой View. Однако, если вы хотите позволить подключать ваш Behavior к определённому типу View, вы можете написать так:

Это может спасти вас от приведения к нужному вам подтипу View большого количества параметров в методах – просто и удобно.

Есть методы, чтобы сохранить как временные данные Behavior.setTag()/Behavior.getTag(), так и сохранить состояние экземпляра Behavior с помощью onSaveInstanceState()/onRestoreInstanceState(). Я призываю вас создавать Behavior как можно более “лёгкими”, но эти методы позволяют создавать Behavior с возможностью сохранения состояния.

Подключение Behavior

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

Программное подключение Behavior

Когда вы думаете о Behavior как о чём-то дополнительно подключённом к каждой View внутри CoordinatorLayout, вас не должно удивить (если вы читали наш пост о макетах) то, что Behavior на самом деле хранится в LayoutParams каждой View – вот почему Behavior должен быть объявлен у View внутри CoordinatorLayout, так как только у этих View есть конкретный подтип LayoutParams, который может хранить Behavior.

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

Читайте также:  Печать документов с android
Подключение Behavior через XML

Конечно, если постоянно делать всё через код – это может вызвать беспорядок. Как и в случае с большинством пользовательских LayoutParams, есть соответствующий layout_ атрибут, чтобы сделать всё тоже самое. В нашем случае это layout_behavior атрибут:

Здесь, в отличии от программного способа, будет всегда вызван конструктор FancyBehavior(Context context, AttributeSet attrs). Зато, в качестве бонуса, вы можете объявить любые другие пользовательские атрибуты и извлечь их из XML AttributeSet, это важно, если вы хотите (а вы захотите) дать возможность другим разработчикам настраивать функционал вашего Behavior через XML.

Примечание: аналогично с соглашением наименования layout_ атрибутов, которые родительский класс должен уметь анализировать и понимать, используйте префикс behavior_ для любых атрибутов, используемых внутри Behavior.

Автоматическое подключение Behavior

Если вы создаёте свою View, которой нужен свой Behavior (как было в случае с большинством компонентов в Design Library), тогда вы скорее всего захотите подключить Behavior по умолчанию, без постоянного ручного подключения через код или XML. Чтобы сделать это, нужно лишь добавить простую аннотацию сверху класса вашей View:

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

Перехват касаний

Как только вы подключите Behavior, вы готовы к действиям. Одно из способностей Behavior – это перехват касаний.

Без CoordinatorLayout, это обычно вовлекало подклассы каждой ViewGroup, как сказано в Managing Touch Events training. Однако в случае с CoordinatorLayout, он передаст вызовы метода onInterceptTouchEvent() методу onInterceptTouchEvent() вашего Behavior, позволяя вашему Behavior перехватывать касания. Если вернуть в этом методе true, то ваш Behavior получит все последующие касания с помощью метода onTouchEvent() – всё это происходит в тайне от View. К примеру, так работает SwipeDismissBehavior с любой View.

Есть другой, более сложный случай перехвата касаний – блокировка любого взаимодействия. Достаточно вернуть в методе blocksInteractionBelow() true. Скорее всего вы захотите как-то визуально показать, что взаимодействие заблокировано (чтобы пользователи не подумали, что приложение сломано) – вот почему стандартный функционал blocksInteractionBelow() зависит от значения getScrimOpacity(). Возвращая значение не равное нулю, разом рисуем цвет (getScrimColor(), чёрный по умолчанию) поверх View и отключаем взаимодействия касанием. Удобно.

Перехват оконных вставок

Скажем, вы читали блог Why would I want to fitsSystemWindows?. Там мы подробно обсуждали что делает fitsSystemWindows, но всё сводилось к представлению оконных вставок (window insets), необходимых чтобы избежать рисования под системными окнами (такими как status bar и navigation bar). Behavior может проявить себя и тут. Если ваша View fitsSystemWindows=“true”, то любой подключенный Behavior получит вызов метода onApplyWindowInsets(), в приоритете над самой View.

Примечание: в большинстве случаев, если ваш Behavior не обрабатывает оконные вставки целиком, он должен передать эти вставки с помощью ViewCompat.dispatchApplyWindowInsets(), чтобы убедиться что все дочерние View получат возможность увидеть WindowInsets.

Перехват Measurement и Layout

Это два ключевых компонента в том, как Android рисует View. Поэтому, имеет смысл, что Behavior, как перехватчик всех событий, так же первым получит уведомления об изменении размера и макета путём вызова методов onMeasureChild() и onLayoutChild() .

К примеру, давайте возьмём любой generic ViewGroup и добавим к нему maxWidth. Как показано в классе MaxWidthBehavior.java.

Создание generic Behavior, который сможет работать с любыми View, весьма удобно. Но помните, что вы можете упростить себе жизнь, обдумав будет ли Behavior использоваться только внутри вашего приложения (не все Behavior поголовно должны быть generic!).

Понимание зависимостей между View

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

Behavior может сделать View зависимыми друг от друга двумя разными путями: когда View связана (anchor) с другой View (подразумеваемая зависимость) или когда вы явно возвращаете true в методе layoutDependsOn().

Связка возникает, когда ваша View внутри CoordinatorLayout использует layout_anchor атрибут. Этот атрибут, совмещённый с layout_anchorGravity, позволяет вам эффективно связать положение двух View вместе. К примеру, вы можете связать FloatingActionButton с AppBarLayout, тогда FloatingActionButton.Behavior будет использовать неявную зависимость, чтобы спрятать FAB, если AppBarLayout будет прокручена за граница экрана.

В любом случае, ваш Behavior получит вызовы метода onDependentViewRemoved(), когда зависимая View была удалена и onDependentViewChanged(), всякий раз как зависимая View была изменена (изменился размер или позиция).

Большинство классного функционала Design Library работает благодаря возможности связки View вместе. Возьмите к примеру взаимодействие между FloatingActionButton и Snackbar. Behavior у FAB зависит от добавленных в CoordinatorLayout экземпляров Snackbar, затем, используя вызов метода onDependentViewChanged(), перемещает FAB выше, чтобы не допустить перекрытие Snackbar.

Читайте также:  How to update android version

Примечание: когда вы добавляете зависимость, View всегда будет размещена после зависимой View, независимо от последовательности в разметке.

Вложенная прокрутка

О, вложенная прокрутка. Я лишь слегка коснусь этой темы в посте. Нужно иметь в виду о нескольких вещах:

  1. Вы не должны объявлять зависимости от View c вложенной прокруткой. Каждая View внутри CoordinatorLayout может получить события вложенной прокрутки.
  2. Вложенная прокрутка может возникнуть не только у View внутри CoordinatorLayout, но и у любой дочерней View (например, “ребёнок” “ребёнка” “ребёнка” у CoordinatorLayout).
  3. Я буду называть это вложенной прокруткой, но на самом деле под ним будет пониматься и просто прокрутка и быстрая прокрутка (fling).

События вложенной прокрутки начинаются с метода onStartNestedScroll(). Вы получите оси прокрутки (горизонтальная или вертикальная – можем легко игнорировать прокрутку в определённом направлении) и должны вернуть true, чтобы получить дальнейшие события прокрутки.

После того как вы вернёте true в методе onStartNestedScroll(), вложенная прокрутка работает в два этапа:

  • onNestedPreScroll() вызывается перед тем как прокручиваемая View получила событие прокрутки и позволяет вашему Behavior обработать часть или всю прокрутку (последний int[] параметр – это “исходящий” параметр, где вы можете указать какую часть прокрутки обработал Behavior).
  • onNestedScroll() вызывается как только прокручиваемая View была прокручена. Вы получите значение насколько View была прокручена и необработанные значения.

Есть аналогичные методы и для быстрой прокрутки (но метод обработки pre-fling должен обрабатывать либо всю прокрутку, либо не обрабатывать вовсе).

Когда вложенная (либо быстрая) прокрутка заканчиваются, вы получите вызов метода onStopNestedScroll(). Это обозначает окончание прокрутки и ожидание нового вызова метода onStartNestedScroll() перед тем как начнётся новая прокрутка.

Возьмите к примеру случай, когда вы хотите спрятать FloatingActionButton, когда прокручиваем вниз и показать FAB, когда прокручиваем вверх. Для этого нужно переопределить методы onStartNestedScroll() и onNestedScroll(), как видно в ScrollAwareFABBehavior.

И это только начало

Если каждая отдельная часть у Behavior лишь интересна, то когда они все вместе – начинается магия. Я очень рекомендую посмотреть исходный код Design Library, чтобы узнать больше возможностей. Расширение для Chrome Android SDK Search всё ещё одно из моих любимых ресурсов для исследования кода AOSP (Android Open Source Project). Помимо этого вы можете найти последние версии исходного кода по пути /extras/android/m2repository.

Дайте мне знать, как вы используете полученные основы о том, что может Behavior хэштегом #BuildBetterApps.

Присоединяйтесь к обсуждению на Google+ и подписывайтесь на Android Development Patterns Collection для ещё большей информации!

Источник

Android TopSheet — Implementation

BottomSheet is a famous Material Design component which has become ubiquitous in Android apps these days. Google has provided a good working implementation of the bottom sheet via the Material Design Library.

At Taskito, we heavily use the Bottom Sheet to provide meaningful interactions. With more features, comes more complexity related to navigation. There are a lot of apps which use the bottom sheet to show menu options.

Tapping a menu item in the Toolbar and showing a bottom sheet breaks the user flow. At the moment, user’s attention is concentrated near the top of the screen. When the bottom sheet comes up, the user has to shift attention to the bottom and move the thumb as well.

😤 Having to constantly change the attention is not ideal. It could be frustrating for the user that they now have to move their thumb all the way to the bottom.

Top Sheet

Taskito does not use menu popup views and to keep the UI uniform throughout the app, we decided to add a Top Sheet.

A Top Sheet is similar to the Bottom Sheet. Instead of transitioning from the bottom, A top sheet transitions down from the top and sticks to the top of the screen.

It is not defined in the Material Design and may go against the guideline. But they are guidelines after all. Here’s how it looks 👇👇.

This is a tiny GIF of one of the Top Sheet Dialog implemented in Taskito. It looks pretty smooth and behaves very similar to the Bottom Sheet Dialog. Read on to know how we implemented it.

🛑 Don’t reinvent the wheel

There are a few hacky implementation available for Top Sheet. I tried one of them and it didn’t work as expected. So I borrowed its implementation idea. Instead of trying to come up with a new solution, I checked the implementation of BottomSheet. It’s not as confusing as it looks.

Читайте также:  Iphone 5s android что это такое

I copied the necessary code verbatim and started probing around. Adding print statements can be quite helpful sometimes. Here’s how the Bottom Sheet is implemented.

Bottom Sheet Implementation

The bottom sheet works only within a CoordinatorLayout. So if you want a BottomSheetDialog, you create a CoordinatorLayout and put your content inside it.

BottomSheetBehavior is the core. It defines when to expand, when to collapse, different states, etc.

BottomSheetBehavior

It may look confusing but it’s well written and easy to understand. There are 3 main methods that require your attention.

onLayoutChild: It’s invoked when you add the bottom sheet content to the parent CoordinatorLayout. It’s usually called only once at the beginning. Determine where to put the bottom sheet content on the screen/parent.

onNestedPreScroll: Called when user has started to scroll/drag the sheet. This method detects overscroll and stops it (stop user from expanding the sheet if it’s already in the expanded state).

onStopNestedScroll: Called when the user stops dragging the sheet. This method determines the new state (collapsed, expanded) based on the dragging position and finalizes the state if necessary. (if user has dragged the sheet below collapsed offset, change state to collapsed and collapse / hide the sheet).

There’s other important code about animations and stuff, but it’s not a concern at the moment.

Read on to get more details about these methods. Please refer to the illustration below.

  • H parent – Height of the bottom sheet parent.
  • H peek – Peek height of the bottom sheet. The height that should be shown when the bottom sheet is added/shown.
  • O collapse = H parent — H peek – Collapse offset. If the sheet is dragged below it, it should be collapsed/dismissed.

Everything is measured from top-left corner as the origin.

Android Bottom Sheet behavior explained

📏 All these methods calculate child’s top (distance from top of the screen/parent) based on drag and state.

onLayoutChild:

Find content top » top child = H parent — H peek. Offset the child top by the given value.

onNestedPreScroll:

Find the new content top » new_top child = top child + d y.

onStopNestedScroll:

This is the method that determines the final state of the bottom sheet. In the simplest terms, it compares the current top position of the child to determine the next state (expanded, collapsed, etc) and starts animation to finalize it.

In the simplest terms,

Top Sheet Implementation

🎯 My goal was to replicate this behavior, but for a sheet that transitions from the top. It turns out that we need even less calculations to do this.

I copied BottomSheetBehavior class and renamed it to TopSheetBehavior, BottomSheetDialog to TopSheetDialog and also copied the bottom sheet dialog layout file for the top sheet dialog.

Build » Run » It completely replicates bottom sheet behavior. Now, towards fiddling.

In the BottomSheetBehavior, the content was laid out based on its top position. In TopSheetBehavior, it will be laid out based on its bottom position which is easier to determine.

Android Top Sheet Behavior explained

Here, the height of the parent does not matter that much. Collapsed offset is simply the peek height of the bottom sheet.

O collapsed = H peek

onLayoutChild:

Find content bottom »

  • bottom child = H child — H peek.
  • top child = — H peek

Offset the child top by the given value.

onNestedPreScroll:

Similar to the bottom sheet, we find the new bottom and just reverse the conditions. new_bottom child = bottom child — d y.

onStopNestedScroll:

Determine the final state of the top sheet. We compare the current bottom position of the child to determine the next state (expanded, collapsed, etc) and starts animation to finalize it.

In the simplest terms,

This was a very simplified explanation of the BottomSheet and the TopSheet behavior. There are other states and cases to consider: Collapsed, Hidden, Half expanded, Expanded.

I couldn’t fit all the explanation in this post. I have also not created a Github repository or library of this fiddling.

✅ To recap, I used the existing implementation of Bottom Sheet and tweaked it to create Top Sheet implementation in Android.

You can find the relevant code & the brief guide on Github — Top Sheet Implementation for Android. Feel free to modify the code to fit your needs.

If you liked the content, you can support Taskito.

Источник

Оцените статью