Drag и Swipe в RecyclerView. Часть 2: контроллеры перетаскивания, сетки и пользовательские анимации
В первой части мы рассмотрели ItemTouchHelper и реализацию ItemTouchHelper.Callback, которая добавляет базовые функции drag & drop и swipe-to-dismiss в RecyclerView. В этой статье мы продолжим то, что было сделано в предыдущей, добавив поддержку расположения элементов в виде сетки, контроллеры перетаскивания, выделение элемента списка и пользовательские анимации смахивания (англ. swipe).
Контроллеры перетаскивания
При создании списка, поддерживающего drag & drop, обычно реализуют возможность перетаскивания элементов по касанию. Это способствует понятности и удобству использования списка в «режиме редактирования», а также рекомендуется material-гайдлайнами. Добавить контроллеры перетаскивания в наш пример сказочно легко.
Сперва обновим layout элемента (item_main.xml).
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"; android:id="@+id/item" android:layout_width="match_parent" android:layout_height="?listPreferredItemHeight" android:clickable="true" android:focusable="true" android:foreground="?selectableItemBackground"> <TextView android:id="@+id/text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginLeft="16dp" android:textAppearance="?android:attr/textAppearanceMedium" /> <ImageView android:id="@+id/handle" android:layout_width="?listPreferredItemHeight" android:layout_height="match_parent" android:layout_gravity="center_vertical|right" android:scaleType="center" android:src="@drawable/ic_reorder_grey_500_24dp" /> </FrameLayout>
Изображение, используемое для контроллера перетаскивания, можно найти в Material Design иконках и добавить в проект с помощью удобного плагина генератора иконок в Android Studio.
Как кратко упоминалось в прошлой статье, вы можете использовать ItemTouchHelper.startDrag(ViewHolder), чтобы программно запустить перетаскивание. Итак, всё, что нам нужно сделать, это обновить ViewHolder, добавив контроллер перетаскивания и настроить простой обработчик касаний, который будет вызывать startDrag().
Нам понадобится интерфейс для передачи события по цепочке:
interface OnStartDragListener { /** * Called when a view is requesting a start of a drag. * * @param viewHolder The holder of the view to drag. */ fun onStartDrag(RecyclerView.ViewHolder viewHolder) }
Затем определите ImageView для контроллера перетаскивания в ItemViewHolder:
class ItemViewHolder(private val itemView: View): RecyclerView.ViewHolder(itemView) { val handleView = itemView.findViewById<ImageView>(R.id.handle) }
и обновите RecyclerListAdapter:
class RecyclerListAdapter( private val dragStartListener: OnStartDragListener ): RecyclerView.Adapter<ItemViewHolder>() { override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { // ... holder.handleView.setOnTouchListener(object : OnTouchListener() { override onTouch(v: View, event: MotionEvent): Boolean { if (MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_DOWN) { dragStartListener.onStartDrag(holder) } return false } }) } // ... }
RecyclerListAdapter теперь должен выглядеть примерно так.
Всё, что осталось сделать, это добавить OnStartDragListener во фрагмент:
class RecyclerListFragment: Fragment(), OnStartDragListener { // ... override fun onViewCreated(view: View, bundle: Bundle) { super.onViewCreated(view, bundle) val adapter = RecyclerListAdapter(this) // ... } override fun onStartDrag(viewHolder: RecyclerView.ViewHolder) { mItemTouchHelper.startDrag(viewHolder) } }
RecyclerListFragment теперь должен выглядеть следующим образом. Теперь, когда вы запустите приложение, то сможете начать перетаскивание, коснувшись контроллера.
Выделение элемента списка
Сейчас в нашем примере нет никакой визуальной индикации элемента, который перетаскивается. Очевидно, так быть не должно, но это легко исправить. С помощью ItemTouchHelper можно использовать стандартные эффекты подсветки элемента. На Lollipop и более поздних версиях Android подсветка «расплывается» по элементу в процессе взаимодействия с ним; на более ранних версиях элемент просто меняет свой цвет на затемнённый.
Чтобы реализовать это в нашем примере, просто добавьте фон (свойство background) в корневой FrameLayout элемента item_main.xml или установите его в конструкторе RecyclerListAdapter.ItemViewHolder. Это будет выглядеть примерно так:
Выглядит круто, но, возможно, вы захотите контролировать ещё больше. Один из способов сделать это — позволить ViewHolder обрабатывать изменения состояния элемента. Для этого ItemTouchHelper.Callback предоставляет ещё два метода:
- onSelectedChanged(ViewHolder, int) вызывается каждый раз, когда состояние элемента меняется на drag (ACTION_STATE_DRAG) или swipe (ACTION_STATE_SWIPE). Это идеальное место, чтобы изменить состояние view-компонента на активное.
- clearView(RecyclerView, ViewHolder) вызывается при окончании перетаскивания view-компонента, а также при завершении смахивания (ACTION_STATE_IDLE). Здесь обычно восстанавливается изначальное состояние вашего view-компонента.
А теперь давайте просто соберём всё это вместе.
Сперва создайте интерфейс, который будут реализовывать ViewHolder'ы:
/** * Notifies a View Holder of relevant callbacks from * {@link ItemTouchHelper.Callback}. */ interface ItemTouchHelperViewHolder { /** * Called when the {@link ItemTouchHelper} first registers an * item as being moved or swiped. * Implementations should update the item view to indicate * it's active state. */ fun onItemSelected() /** * Called when the {@link ItemTouchHelper} has completed the * move or swipe, and the active item state should be cleared. */ fun onItemClear() }
Затем в SimpleItemTouchHelperCallback реализуйте соответствующие методы:
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder, actionState: Int) { // We only want the active item if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) { if (viewHolder is ItemTouchHelperViewHolder) { viewHolder.onItemSelected() } } super.onSelectedChanged(viewHolder, actionState) } override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { super.clearView(recyclerView, viewHolder) if (viewHolder is ItemTouchHelperViewHolder) { viewHolder.onItemClear() } }
Теперь осталось только, чтобы RecyclerListAdapter.ItemViewHolder реализовал ItemTouchHelperViewHolder:
class ItemViewHolder( private val itemView: View ): RecyclerView.ViewHolder(itemView), ItemTouchHelperViewHolder { // ... override fun onItemSelected() { itemView.setBackgroundColor(Color.LTGRAY) } override fun onItemClear() { itemView.setBackgroundColor(0) } }
В этом примере мы просто добавляем серый фон во время активности элемента, а затем его удаляем. Если ваш ItemTouchHelper и адаптер тесно связаны, вы можете легко отказаться от этой настройки и переключать состояние view-компонента прямо в ItemTouchHelper.Callback.
Сетки
Если теперь вы попытаетесь использовать GridLayoutManager, вы увидите, что он работает неправильно. Причина и решение проблемы просты: мы должны сообщить нашему ItemTouchHelper, что мы хотим поддерживать перетаскивание элементов влево и вправо. Ранее в SimpleItemTouchHelperCallback мы уже указывали:
override fun getMovementFlags( recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder ): Int { val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN val swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END return makeMovementFlags(dragFlags, swipeFlags) }
Единственное изменение, необходимое для поддержки сеток, заключается в добавлении соответствующих флагов:
val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
Тем не менее, swipe-to-dismiss – не очень естественное поведение для элементов в виде сетки, поэтому swipeFlags разумнее всего обнулить:
override fun getMovementFlags( recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder ): Int { val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT val swipeFlags = 0 return makeMovementFlags(dragFlags, swipeFlags) }
Чтобы увидеть рабочий пример GridLayoutManager, смотрите RecyclerGridFragment. Вот как это выглядит при запуске:
Пользовательские анимации смахивания
ItemTouchHelper.Callback предоставляет действительно удобный способ для полного контроля анимации во время перетаскивания или смахивания. Поскольку ItemTouchHelper — это RecyclerView.ItemDecoration, мы можем вмешаться в процесс отрисовки view-компонента похожим образом. В следующей части мы разберём этот вопрос подробнее, а пока посмотрим на простой пример переопределения анимации смахивания по умолчанию, чтобы показать линейное исчезновение.
override fun onChildDraw( c: Canvas, recyclerView: RecyclerView, viewHolder: ViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean ) { if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) { val width = viewHolder.itemView.getWidth().toFloat() val alpha = 1.0f - abs(dX) / width viewHolder.itemView.setAlpha(alpha) viewHolder.itemView.setTranslationX(dX) } else { super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) } }
Параметры dX и dY — это текущий сдвиг относительно выделенного view-компонента, где:
- -1.0f — это полное смахивание справа налево (от ItemTouchHelper.END к ItemTouchHelper.START);
- 1.0f — это полное смахивание слева направо (от ItemTouchHelper.START к ItemTouchHelper.END).
Важно вызывать super для любого actionState, который вы не обрабатываете, для того чтобы запускалась анимация по умолчанию.
Заключение
На самом деле, настройка ItemTouchHelper — это довольно весело. Чтобы не увеличивать объём этой статьи, я разделил её на несколько.
Исходный код
Весь код этой серии статей смотрите на GitHub-репозитории Android-ItemTouchHelper-Demo. Эта статья охватывает коммиты от ef8f149 до d164fba.
Источник: Drag и Swipe в RecyclerView. Часть 2: контроллеры перетаскивания, сетки и пользовательские анимации