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

Комментарии (2)


  1. demonit
    03.11.2018 09:51

    а вот интересно… почему комментов 0?! ни кому не интересна тема? мне была очень


  1. Zhenika
    03.11.2018 17:45

    Простая для понимания статья. Хотелось бы увидеть реализацию вложенных объектов именно с данным стандартным способом drag & drop. Когда есть карточка (родительская), а в ней ещё карточки (дочерние). И можно перетаскивать как родительскую карточку, так и дочерние. Причём дочерние можно было б переносить от одной родительской карточки к другой.