Drag và swipe với RecyclerView

Đã có rất nhiều tutorial, thư viện hay ví dụ về cách implement "drag & drop" hay "swipe-to-dismiss" trong Android, sử dụng RecyclerView. Hầu hết các thư viện hay ví dụ này vẫn đang dùng View.OnDragListener, và cách tiếp cận của Romain Nurik, tác giả của thư viện SwipeToDismiss, mặc dù đã có những hàm mới hơn và tốt hơn trong framework. Một vài thư viện thì sử dụng API mới hơn nhưng lại thường phải dựa vào GestureDetectorsonInterceptTouchEvent, hoặc cách implement rất phức tạp. Thật ra thì có 1 cách rất đơn giản để thêm những tính năng này vào RecyclerView. Cách này chỉ yêu cầu 1 class, và hơn thế nữa là class này lại là 1 phần của Android Support Library, đó là ItemTouchHelper

ItemTouchHelper là 1 class utility cung cấp những yếu tố cần thiết để bạn có thể tích hợp cả drag & drop lẫn swipe-to-dismiss vào RecyclerView của mình. Nó là 1 subclass của RecyclerView.ItemDecoration, có nghĩa là rất dễ để thêm nó vào bất cứ 1 LayoutManager hoặc Adapter đã có sẵn. Trong bài này, tôi sẽ trình bày 1 ví dụ đơn giản về cách implement của ItemTouchHelper.

Setup

Việc đầu tiên chúng ta cần làm là thêm RecyclerView vào dependency:

compile 'com.android.support:recyclerview-v7:25.0.0'

ItemTouchHelper có tác dụng với hầu như tất cả RecyclerView.AdapterLayoutManager, nhưng bài viết này sẽ được viết dựa trên những setup cơ bản từ file này:

https://gist.github.com/iPaulPro/2216ea5e14818056cfcc

Sử dụng ItemTouchHelperItemTouchHelper.Callback

Để có thể sử dụng được ItemTouchHelper, bạn sẽ phải tạo 1 ItemTouchHelper.Callback. Đây là 1 interface cho phép chúng ta lắng nghe sự kiện "di chuyển" và "vuốt". Nó cũng là nơi cho phép bạn điều khiển state của view đang được select, và override những animation mặc định. Đã có 1 helper class mà bạn có thể dùng nếu bạn muốn những chức năng cơ bản đó, nó tên là SimpleCallback, nhưng vì mục đích học cách nó hoạt động nên chúng ta sẽ tự làm một phiên bản riêng.

Những callback chính mà chúng ta phải override để kích hoạt drag & drop và swipe-to-dismiss cơ bản là:


getMovementFlags(RecyclerView, ViewHolder)

onMove(RecyclerView, ViewHolder, ViewHolder)

onSwiped(ViewHolder, int)

Chúng ta cũng sẽ sử dụng 1 vài hàm helper:


isLongPressDragEnabled()

isItemViewSwipeEnabled()

Chúng ta sẽ lần lượt xem từng hàm một:


@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 cho phép chúng ta dễ dàng xác định hướng của 1 sự kiện. Bạn cần phải override getMovementFlags() để định nghĩa là hướng nào của "kéo" và "vuốt" được hỗ trợ. Sử dụng hàm ItemTouchHelper.makeMovementFlags(int, int) để tạo ra và trả về các flag. Chúng ta sẽ kích hoạt cả 2 hướng ở đây.

@Override
public boolean isLongPressDragEnabled() {
    return true;
}

ItemTouchHelper có thể được dùng để "drag" mà không "swipe" (hoặc ngược lại), nên bạn cần phải chắc chắn về việc bạn muốn support những event nào. Hàm trên cần phải return true để có thể support sự kiện drag từ sự kiện long press vào 1 item trong RecyclerView. Ngoài ra, ItemTouchHelper.startDrag(RecyclerView.ViewHolder) có thể được gọi để bắt đầu sự kiện "drag" từ 1 điểm nắm (handle). Vấn đề này sẽ được giải quyết ở những bài sau.

@Override
public boolean isItemViewSwipeEnabled() {
    return true;
}

Để kích hoạt swipe từ sự kiện touch được bắt đầu ở bất cứ chỗ nào nằm trong view, đơn giản return true từ hàm trên. Ngoài ra ItemTouchHelper.startSwipe(RecyclerView.ViewHolder) còn có thể được gọi để bắt đầu sự kiện drag một cách thủ công.

2 hàm tiếp theo, onMove()onSwiped() dùng để thông báo về việc update lớp dữ liệu của Adapter. Nên đầu tiên chúng ta sẽ tạo 1 interface để xử lý những sự kiện này.


public interface ItemTouchHelperAdapter {

    void onItemMove(int fromPosition, int toPosition);

    void onItemDismiss(int position);
}

Cách đơn giản nhất để làm việc này (chỉ để minh họa) là cho RecyclerListAdapter của chúng ta trực tiếp implement interface trên. Lưu ý là nếu bạn làm thật thì cần để activity/fragment chứa Recyclerview implement chứ đừng cho vào Adapter.

public class RecyclerListAdapter extends
        RecyclerView.Adapter<ItemViewHolder>
        implements ItemTouchHelperAdapter {
// ... code from gist
@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;
}

Lưu ý là bạn cần phải gọi notifyItemRemoved()notifyItemMoved() để Adapter có thể biết được những thay đổi trong tầng dữ liệu. Một điểm cần lưu ý nữa là chúng ta sẽ thay đổi vị trí của item bất cứ khi nào view được chuyển đến 1 index mới chứ không phải là chỉ thay đổi vị trí của item vào cuối sự kiện "drop" (sau khi bạn đã thả tay ra).

Giờ chúng ta có thể tiếp tục hoàn thiện SimpleItemTouchHelperCallback bởi vì chúng ta vẫn cần phải override onMove()onSwiped(). Đầu tiên hãy thêm 1 constructor và 1 field cho Adapter:

private final ItemTouchHelperAdapter mAdapter;

public SimpleItemTouchHelperCallback(
        ItemTouchHelperAdapter adapter) {
    mAdapter = adapter;
}

Sau đó override những sự kiện còn lại và thông báo cho 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 class sẽ trông như thế này:

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());
    }

}

Với class callback đã hoàn thiện, chúng ta có thể tạo ItemTouchHelper và gọi hàm attachToRecyclerView:

ItemTouchHelper.Callback callback =
    new SimpleItemTouchHelperCallback(adapter);
ItemTouchHelper touchHelper = new ItemTouchHelper(callback);
touchHelper.attachToRecyclerView(recyclerView);

Kết quả khi chạy:

1-FdJbZnF5I-iOw0wgiuVJGQ.gif

Kết luận

Tuy ví dụ trên chỉ là 1 implementation cơ bản của ItemTouchHelper, mục đích chính của bài viết này là để chỉ ra rằng chúng ta không cần thiết phải sử dụng thư viện của bên thứ 3 để có được những tính năng như "drag & drop" hay "swipe-to-dismiss" cho RecyclerView. Các bạn có thể tham khảo project demo tại https://github.com/iPaulPro/Android-ItemTouchHelper-Demo

Nguồn: https://medium.com/@ipaulpro/drag-and-swipe-with-recyclerview-b9456d2b1aaf#.u6vbazip9