[Перевод] 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
. Позже, в рамках этой серии статей, мы расширим рамки и рассмотрим остальные возможности.
Примечание. Хотите сразу увидеть результат? Загляните на 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
public int getMovementFlags(RecyclerView recyclerView,
RecyclerView.ViewHolder viewHolder) {
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
return makeMovementFlags(dragFlags, swipeFlags);
}
ItemTouchHelper
позволяет легко определить направление события. Вам нужно переопределить метод getMovementFlags()
, чтобы указать, какие направления для перетаскивания будут поддерживаться. Для создания возвращаемых флагов используйте вспомогательный метод ItemTouchHelper.makeMovementFlags(int, int)
. В этом примере мы разрешаем перетаскивание и смахивание в обоих направлениях.
@Override
public boolean isLongPressDragEnabled() {
return true;
}
ItemTouchHelper
можно использовать только для перетаскивания без функционала смахивания (или наоборот), поэтому вы должны точно указать, какие функции должны поддерживаться. Метод isLongPressDragEnabled()
должен возвращать значение true
, чтобы поддерживалось перетаскивание после длительного нажатия на элемент RecyclerView
. В качестве альтернативы можно вызвать метод ItemTouchHelper.startDrag(RecyclerView.ViewHolder)
, чтобы начать перетаскивание вручную. Рассмотрим этот вариант позже.
@Override
public boolean isItemViewSwipeEnabled() {
return true;
}
Чтобы разрешить смахивание после касания где угодно в рамках view
-компонента, просто верните значение true
из метода isItemViewSwipeEnabled()
. В качестве альтернативы можно вызвать метод ItemTouchHelper.startSwipe(RecyclerView.ViewHolder)
, чтобы начать смахивание вручную.
Следующие два метода, onMove()
и onSwiped()
, необходимы для того, чтобы уведомить об обновлении данных. Итак, сначала мы создадим интерфейс, который позволит передать эти события по цепочке вызовов.
ItemTouchHelperAdapter.java
public interface ItemTouchHelperAdapter {
void onItemMove(int fromPosition, int toPosition);
void onItemDismiss(int position);
}
Самый простой способ сделать это — сделать так, чтобы RecyclerListAdapter имплементировал слушателя.
public class RecyclerListAdapter extends
RecyclerView.Adapter
implements ItemTouchHelperAdapter {
// ... код из [примера](https://gist.github.com/iPaulPro/2216ea5e14818056cfcc#file-recyclerlistadapter-java)
@Override
public void onItemDismiss(int position) {
mItems.remove(position);
notifyItemRemoved(position);
}
@Override
public boolean onItemMove(int fromPosition, int toPosition) {
if (fromPosition < toPosition) {
for (int i = fromPosition; i < toPosition; i++) {
Collections.swap(mItems, i, i + 1);
}
} else {
for (int i = fromPosition; i > toPosition; i--) {
Collections.swap(mItems, i, i - 1);
}
}
notifyItemMoved(fromPosition, toPosition);
return true;
}
Очень важно вызвать методы notifyItemRemoved()
и notifyItemMoved()
, чтобы адаптер увидел изменения. Также нужно отметить, что мы меняем позицию элемента каждый раз, когда view
-компонент смещается на новый индекс, а не в самом конце перемещения (событие «drop»).
Теперь мы можем вернуться к созданию SimpleItemTouchHelperCallback
, поскольку нам всё ещё необходимо переопределить методы onMove()
и onSwiped()
. Сначала добавьте конструктор и поле для адаптера:
private final ItemTouchHelperAdapter mAdapter;
public SimpleItemTouchHelperCallback(
ItemTouchHelperAdapter adapter) {
mAdapter = adapter;
}
Затем переопределите оставшиеся события и сообщите об этом адаптеру:
@Override
public boolean onMove(RecyclerView recyclerView,
RecyclerView.ViewHolder viewHolder,
RecyclerView.ViewHolder target) {
mAdapter.onItemMove(viewHolder.getAdapterPosition(),
target.getAdapterPosition());
return true;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder,
int direction) {
mAdapter.onItemDismiss(viewHolder.getAdapterPosition());
}
В результате класс Callback
должен выглядеть примерно так:
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
private final ItemTouchHelperAdapter mAdapter;
public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter) {
mAdapter = adapter;
}
@Override
public boolean isLongPressDragEnabled() {
return true;
}
@Override
public boolean isItemViewSwipeEnabled() {
return true;
}
@Override
public int getMovementFlags(RecyclerView recyclerView, ViewHolder viewHolder) {
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
return makeMovementFlags(dragFlags, swipeFlags);
}
@Override
public boolean onMove(RecyclerView recyclerView, ViewHolder viewHolder,
ViewHolder target) {
mAdapter.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
return true;
}
@Override
public void onSwiped(ViewHolder viewHolder, int direction) {
mAdapter.onItemDismiss(viewHolder.getAdapterPosition());
}
}
Когда Callback готов, мы можем создать ItemTouchHelper
и вызвать метод attachToRecyclerView(RecyclerView)
(например, в MainFragment.java):
ItemTouchHelper.Callback callback =
new SimpleItemTouchHelperCallback(adapter);
ItemTouchHelper touchHelper = new ItemTouchHelper(callback);
touchHelper.attachToRecyclerView(recyclerView);
После запуска должно получиться приблизительно следующее:
Заключение
Это максимально упрощённая реализация ItemTouchHelper
. Тем не менее, вы можете заметить, что вам не обязательно использовать стороннюю библиотеку для реализации стандартных действий drag & drop и swipe-to-dismiss в RecyclerView
. В следующей части мы уделим больше внимания внешнему виду элементов в момент перетаскивания или смахивания.
Исходный код
Я создал проект на GitHub для демонстрации того, о чём рассказывается в этой серии статей: Android-ItemTouchHelper-Demo. Первый коммит в основном относится к этой части и немного ко второй.