FloatingActionMode — панель контекстних дій для Android

Контекстні дії з елементами списку широко використовуються з Android-додатках. Досить зручно виділити кілька елементів або всі елементи списку і застосувати якусь дію до всіх виділених елементів відразу. Видалити, наприклад.
В Android-додатках для цього може використовуватися
ActionMode
, який дозволяє відобразити доступні дії над виділеними елементами поверх
Toolbar
. Там же можна показувати користувачеві скільки елементів виділено в поточний момент або іншу корисну інформацію. Це зручно і добре виглядає, але в деяких випадках інформація, що відображається на самому
Toolbar
, може бути важлива і приховувати її не хотілося б. Наприклад, там може бути ім'я і фото користувача, список повідомлень з яким відображається у списку. При виділенні деяких повідомлень корисно було б бачити ім'я користувача, якому ці повідомлення адресовані.
У цьому випадку можна відображати панель контекстних дій з елементами списку поверх самого списку, не загороджуючи
Toolbar
. Про створення такої панелі контекстних дій я і розповім в цій статті.
Розробляється CustomView — панель контекстних дій я назвав
FloatingActionMode
або просто
FAM
.
Art
FloatingActionMode
під час роботи

Відео — приклад роботи з FloatingActionMode
XML-атрибути
налаштування
FAM
через файл-розмітки для нього визначено кілька спеціальних атрибутів, які також можуть бути змінені програмно:
  • opened
    визначає буде чи
    FAM
    відкритий при створенні. (
    false
    за замовчуванням)
  • content_res
    LayoutRes
    , який представляє контент
    FAM
    (кілька кнопок, наприклад).
    View
    , створене з
    content_res
    додається до
    FAM
    як дочірнє
    View
    . Контент може бути змінений програмно під час роботи програми, тому
    FAM
    може бути вказаний атрибут
    android:animateLayoutChanges="true"
    для анімованого зміни контенту. (за замовчуванням контенту немає)
  • can_close
    визначає буде чи
    FAM
    мати кнопку для закриття. (
    true
    за замовчуванням)
  • close_icon
    DrawableRes
    кнопки закриття. (значення за замовчуванням — хрестик)
  • can_drag
    визначає буде чи
    FAM
    мати кнопку для перетягування. (
    true
    за замовчуванням)
  • drag_icon
    DrawableRes
    кнопки перетягування. (є значення за замовчуванням)
  • can_dismiss
    визначає буде чи
    FAM
    закриватися, якщо користувач потягне його по горизонталі досить далеко (
    true
    за замовчуванням)
  • dismiss_threshold
    це порогове значення зсуву по горизонталі починаючи з якого
    FAM
    буде закритий, коли користувач відпустить
    drag_button
    . Тобто, якщо (
    getTranslationX
    /
    getWidth
    ) >
    dismissThreshold
    ,
    FAM
    буде закритий. (
    0.4 f
    за замовчуванням)
  • minimize_direction
    визначає напрямок, в якому буде переміщатися
    FAM
    при згортанні. Цей атрибут може мати наступні значення (
    nearest
    за замовчуванням):
    • none
      FAM
      не буде переміщатися при згортанні
    • top
      FAM
      буде переміщатися до верхньої межі батька (виключаючи відступи) під час згортання
    • bottom
      FAM
      буде переміщатися до нижньої межі батька (виключаючи відступи) під час згортання
    • nearest
      FAM
      буде переміщатися до найближчої (верхньої або нижньої) кордоні батьків (виключаючи відступи) під час згортання
  • animation_duration
    визначає тривалість анімації згортання/розгортання. (
    400
    мс за замовчуванням)
FAM
має
OnCloseListener
, який дозволяє виконати певну дію при закритті
FAM
користувачем (зняти виділення з елементів списку, наприклад).
Основні дії
Основними діями з
FAM
є відкриття/закриття і згортання/розгортання. При відкритті він з'являється і розгортається, а при закритті згортається і зникає.
Розгортання
FAM
супроводжується анімацією, в процесі якої він переміщається від верхнього або нижнього краю батьківського
ViewGroup
(цей край задається атрибутом
minimize_direction
) свого положення, задану файлом розмітки. Анімація задається наступним чином:
animate()
.scaleY(1f)
.scaleX(1f)
.translationY(calculateArrangeTranslationY())
.alpha(1f)

При згортанні анімація виконується "у зворотний бік":
animate()
.scaleY(0.5 f)
.scaleX(0.5 f)
.translationY(calculateMinimizeTranslationY())
.alpha(0.5 f)

Методи
calculateArrangeTranslationY()
та
calculateMinimizeTranslationY()
дозволяють обчислити
translationY
для розгорнутого та згорнутого станів відповідно c урахуванням того, куди перетягнув
FAM
користувач, атрибута
minimize_direction
і відступів знизу і зверху, про які буде розказано далі.
Закриття та перетягування
Для правильної і красивої роботи
FAM
має кнопки (
ImageView
), з допомогою яких користувач може закрити режим контекстних дій або перемістити в іншу частину екрана по вертикалі (якщо він загороджує потрібний елемент списку). Також
FAM
може бути закритий, якщо потягти його в бік по горизонталі (swipe to dismiss).
FAM
представляє собою
LinearLayout
, який при створенні додаються кнопки для закриття (
drag_button
) і перетягування (
close_button
). Можливість закривати/перетягувати
FAM
може бути включено/вимкнено під час роботи програми, тому
LinearLayout
, що містить кнопки, має атрибут
android:animateLayoutChanges="true"
.
Розмітка FAM
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="?attr/actionBarSize"
android:animateLayoutChanges="true"
android:layout_gravity="center_vertical">

<ImageView
android:id="@+id/close_button"
android:layout_width="?attr/actionBarSize"
android:layout_height="?attr/actionBarSize"
android:layout_gravity="center_vertical"
android:background="@drawable/image_button_background"
android:scaleType="center"
android:src="@drawable/ic_close_white_24dp"/>

<ImageView
android:id="@+id/drag_button"
android:layout_width="?attr/actionBarSize"
android:layout_height="?attr/actionBarSize"
android:layout_gravity="center_vertical"
android:background="@drawable/image_button_background"
android:scaleType="center"
android:src="@drawable/ic_drag_white_24dp"/>

</LinearLayout>
</merge>

Механізм перетягування реалізований з допомогою
OnTouchListener
, який запам'ятовує початкову точку дотику і при русі встановлює
translationX
та
translationY
відповідно торкання. Коли користувач відпускає кнопку перетягування (
drag_button
),
FAM
повертається у вихідне положення по горизонталі і, якщо користувач потягнув
FAM
досить далеко по горизонталі, то викликається метод
this@FloatingActionMode.close()
.
OnTouchListener
drag_button.setOnTouchListener(object : OnTouchListener {
var prevTransitionY = 0f
var startRawX = 0f
var startRawY = 0f

override fun onTouch(v: View, event: MotionEvent): Boolean {
if (!this@FloatingActionMode.canDrag) {
return false
}

val fractionX = Math.abs(event.rawX - startRawX) / this@FloatingActionMode.width

when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
this@FloatingActionMode.drag_button.isPressed = true
startRawX = event.rawX
startRawY = event.rawY
prevTransitionY = this@FloatingActionMode.translationY
}
MotionEvent.ACTION_MOVE -> {
this@FloatingActionMode.maximizeTranslationY =
prevTransitionY + event.rawY - startRawY
translationX = event.rawX - startRawX
if (canDismiss) {
val alpha =
if (fractionX < dismissThreshold)
1.0 f
else
Math.pow(1.0 - (fractionX - dismissThreshold)
/ (1 - dismissThreshold), 4.0).toFloat()
this@FloatingActionMode.alpha = alpha
}
}
MotionEvent.ACTION_UP -> {
drag_button.isPressed = false
this@FloatingActionMode.animate().translationX(0f)
.duration = animationDuration
if (canDismiss && fractionX > dismissThreshold) {
this@FloatingActionMode.close()
}
}
}
return true
}
})

Використання
CoordinatorLayout

Раніше говорилося, що методи
calculateArrangeTranslationY()
та
calculateMinimizeTranslationY()
враховують відступи зверху і знизу для визначення правильного положення
FAM
. Ці відступи обчислюються з допомогою
FloatingActionModeBehavior
розширення
CoordinatorLayout.Behavior
, який задає верхній відступ як висоту
AppBarLayout
, а нижній відступ як висоту видимої частини
Snackbar.SnackbarLayout
.
Також
FloatingActionModeBehavior
дозволяє
FAM
реагувати на скрол, згортаючись при скролінгу вниз і розвертаючись при скролінгу вгору (quick return pattern).
За замовчуванням
FAM
має
background
, тому ви можете використовувати будь-який потрібно. Також для створення тіні на пристроях з API>=21 може використовуватися атрибут
android:translationZ="8dp"

FloatingActionModeBehavior
open class FloatingActionModeBehavior
@JvmOverloads constructor(context: Context? = null, attrs: AttributeSet? = null)
: CoordinatorLayout.Behavior<FloatingActionMode>(context, attrs) {

override fun layoutDependsOn(parent: CoordinatorLayout?,
child: FloatingActionMode?, dependency: View?): Boolean {
return dependency is AppBarLayout || dependency is Snackbar.SnackbarLayout
}

override fun onDependentViewChanged(parent: CoordinatorLayout,
child: FloatingActionMode, dependency: View): Boolean {
when (dependency) {
is AppBarLayout -> child.topOffset = dependency.bottom
is Snackbar.SnackbarLayout ->
child.bottomOffset = dependency.height - dependency.translationY.toInt()
}
return false
}

override fun onStartNestedScroll(coordinatorLayout: CoordinatorLayout?,
child: FloatingActionMode?, directTargetChild: View?,
target: View?, nestedScrollAxes: Int): Boolean {
return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL
}

override fun onNestedScroll(coordinatorLayout: CoordinatorLayout,
child: FloatingActionMode, target: View, dxConsumed: Int,
dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int) {
super.onNestedScroll(coordinatorLayout, child, target,
dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed)

// FAM не повинен реагувати на скролінг своїх дочірніх View.
var parent = target.parent
while (parent != coordinatorLayout) {
if (parent == child) {
return
}
parent = parent.parent
}

if (dyConsumed > 0) {
child.minimize(true)
} else if (dyConsumed < 0) {
child.maximize(true)
}
}
}

Ось так
FAM
може виглядати у файлі розмітки:
<android.support.design.widget.CoordinatorLayout>

<android.support.design.widget.AppBarLayout>
...
</android.support.design.widget.AppBarLayout>

<android.support.v7.widget.RecyclerView
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

<com.qwert2603.floating_action_mode.FloatingActionMode
android:id="@+id/floating_action_mode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/action_mode_margin"
android:animateLayoutChanges="true"
android:background="@drawable/action_mode_background"
android:translationZ="8dp"
app:animation_duration="@integer/action_mode_animation_duration"
app:can_dismiss="true"
app:can_drag="true"
app:content_res="@layout/user_list_action_mode_2"
app:dismiss_threshold="0.35"
app:drag_icon="@drawable/ic_drag_white_24dp"
app:minimize_direction="nearest"/>

</android.support.design.widget.CoordinatorLayout>

Вихідний код
Вихідний код
FloatingActionMode
доступний на GitHub (директорія library). Там же знаходиться demo додаток, що використовує
FAM
(директорія app).
Сам
FloatingActionMode
, а також
FloatingActionModeBehavior
визначені як
open
класи, тому Ви можете модернізувати їх так, як Вам потрібно. Ключові методи
FloatingActionMode
також визначені як
open
.
Дякую за увагу. Happy coding!
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.