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<ItemViewHolder> 
        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. Первый коммит в основном относится к этой части и немного ко второй.


→ Drag и Swipe в RecyclerView. Часть 2: контроллеры перетаскивания, сетки и пользовательские анимации