Drag и Swipe в RecyclerView. Часть 1: ItemTouchHelper
Существует множество обучающих материалов, библиотек и примеров реализации drag & drop и swipe-to-dismiss в Android c использованием RecyclerView. В большинстве из них по-прежнему используются устаревший View.OnDragListener и подход SwipeToDismiss, разработанный Романом Нуриком. Хотя уже доступны новые и более эффективные методы. Совсем немногие используют новейшие API, зачастую полагаясь на GestureDetectors и onInterceptTouchEvent или же на другие более сложные имплементации. На самом деле существует очень простой способ добавить эти функции в RecyclerView. Для этого требуется всего лишь один класс, который к тому же является частью Android Support Library.
ItemTouchHelper
ItemTouchHelper — это мощная утилита, которая позаботится обо всём, что необходимо сделать, чтобы добавить функции drag & drop и swipe-to-dismiss в RecyclerView. Эта утилита является подклассом RecyclerView.ItemDecoration, благодаря чему её легко добавить практически к любому существующему LayoutManager'у и адаптеру. Она также работает с анимацией элементов и предоставляет возможность перетаскивать элементы одного типа на другое место в списке и многое другое. В этой статье я продемонстрирую простую реализацию ItemTouchHelper. В следующей статье мы расширим рамки и рассмотрим остальные возможности RecyclerView.
Примечание. Хотите сразу увидеть результат? Загляните на Github: Android-ItemTouchHelper-Demo. Первый коммит относится к этой статье. Демо .apk-файл можно скачать здесь.
Настройка
Сперва нам нужно настроить RecyclerView. Если вы ещё этого не сделали, добавьте зависимость RecyclerView в свой файл build.gradle. Примечание: здесь указана старая версия библиотеки поддержки.
compile 'com.android.support:recyclerview-v7:22.2.0'
ItemTouchHelper будет работать практически с любыми RecyclerView.Adapter и LayoutManager, но эта статья базируется на примерах, использующих эти файлы.
Использование ItemTouchHelper и ItemTouchHelper.Callback
Чтобы использовать ItemTouchHelper, вам необходимо создать ItemTouchHelper.Callback. Это интерфейс, который позволяет отслеживать действия перемещения (англ. move) и смахивания (англ. swipe). Кроме того, здесь вы можете контролировать состояние выделенного view-компонента и переопределять анимацию по умолчанию. Существует вспомогательный класс, который вы можете использовать, если хотите использовать базовую имплементацию, — SimpleCallback. Но для того чтобы понять, как это работает на практике, сделаем всё самостоятельно.
Основные функции интерфейса, которые мы должны переопределить, чтобы включить базовый функционал drag & drop и swipe-to-dismiss:
getMovementFlags(RecyclerView, ViewHolder) onMove(RecyclerView, ViewHolder, ViewHolder) onSwiped(ViewHolder, Int)
Мы также будем использовать несколько вспомогательных методов:
isLongPressDragEnabled() isItemViewSwipeEnabled()
Рассмотрим их поочередно.
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) }
ItemTouchHelper позволяет легко определить направление события. Вам нужно переопределить метод getMovementFlags(), чтобы указать, какие направления для перетаскивания будут поддерживаться. Для создания возвращаемых флагов используйте вспомогательный метод ItemTouchHelper.makeMovementFlags(Int, Int). В этом примере мы разрешаем перетаскивание и смахивание в обоих направлениях.
override fun isLongPressDragEnabled(): Boolean { return true }
ItemTouchHelper можно использовать только для перетаскивания без функционала смахивания (или наоборот), поэтому вы должны точно указать, какие функции должны поддерживаться. Метод isLongPressDragEnabled() должен возвращать значение true, чтобы поддерживалось перетаскивание после длительного нажатия на элемент RecyclerView. В качестве альтернативы можно вызвать метод ItemTouchHelper.startDrag(RecyclerView.ViewHolder), чтобы начать перетаскивание вручную. Рассмотрим этот вариант позже.
override fun isItemViewSwipeEnabled(): Boolean { return true }
Чтобы разрешить смахивание после касания где угодно в рамках view-компонента, просто верните значение true из метода isItemViewSwipeEnabled(). В качестве альтернативы можно вызвать метод ItemTouchHelper.startSwipe(RecyclerView.ViewHolder), чтобы начать смахивание вручную.
Следующие два метода, onMove() и onSwiped(), необходимы для того, чтобы уведомить об обновлении данных. Итак, сначала мы создадим интерфейс, который позволит передать эти события по цепочке вызовов.
interface ItemTouchHelperAdapter { fun onItemMove(fromPosition: Int, toPosition: Int) fun onItemDismiss(position: Int) }
Самый простой способ сделать это — сделать так, чтобы RecyclerListAdapter имплементировал слушателя.
class RecyclerListAdapter: RecyclerView.Adapter<ItemViewHolder>, ItemTouchHelperAdapter { // ... код из примера override fun onItemDismiss(position: Int) { mItems.remove(position) notifyItemRemoved(position) } override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean { val range = if (fromPosition < toPosition) { fromPosition..(toPosition - 1) } else { toPosition..(fromPosition - 1) } for (i in range) { val c = mItems[i] mItems[i] = mItems[i + 1] mItems[i + 1] = c } notifyItemMoved(fromPosition, toPosition) return true } }
Очень важно вызвать методы notifyItemRemoved() и notifyItemMoved(), чтобы адаптер увидел изменения. Также нужно отметить, что мы меняем позицию элемента каждый раз, когда view-компонент смещается на новый индекс, а не в самом конце перемещения (событие «drop»).
Теперь мы можем вернуться к созданию SimpleItemTouchHelperCallback, поскольку нам всё ещё необходимо переопределить методы onMove() и onSwiped(). Сначала добавьте конструктор с полем для адаптера:
class SimpleItemTouchHelperCallback( private val adapter: ItemTouchHelperAdapter ): ItemTouchHelper.Callback { //... }
Затем переопределите оставшиеся события и сообщите об этом адаптеру:
override fun onMove( recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder ): Boolean { adapter.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition()) return true } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { adapter.onItemDismiss(viewHolder.getAdapterPosition()) }
В результате класс Callback должен выглядеть примерно так:
class SimpleItemTouchHelperCallback( private val adapter: ItemTouchHelperAdapter ): ItemTouchHelper.Callback { override fun isLongPressDragEnabled() = true override fun isItemViewSwipeEnabled() = true override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: ViewHolder): Int { val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN val swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END return makeMovementFlags(dragFlags, swipeFlags) } override fun onMove( recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder ): Boolean { adapter.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition()) return true } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { adapter.onItemDismiss(viewHolder.getAdapterPosition()) } }
Когда Callback готов, мы можем создать ItemTouchHelper и вызвать метод attachToRecyclerView(RecyclerView) (например, в MainFragment):
val callback = SimpleItemTouchHelperCallback(adapter) val touchHelper = ItemTouchHelper(callback) touchHelper.attachToRecyclerView(recyclerView)
После запуска должно получиться приблизительно следующее:
Заключение
Это максимально упрощённая реализация ItemTouchHelper. Тем не менее, вы можете заметить, что вам не обязательно использовать стороннюю библиотеку для реализации стандартных действий drag & drop и swipe-to-dismiss в RecyclerView. В следующей части мы уделим больше внимания внешнему виду элементов в момент перетаскивания или смахивания.
Исходный код
Я создал проект на GitHub для демонстрации того, о чём рассказывается в этой серии статей: Android-ItemTouchHelper-Demo. Первый коммит в основном относится к этой части и немного ко второй.
Источник: Drag и Swipe в RecyclerView. Часть 1: ItemTouchHelper