android - how to catch Drop action of ItemTouchHelper which is used with RecyclerView

23,277

Solution 1

While dragging and dropping an item, the onMove() can be called more than once, but the clearView() will be called once. So you can use this to indicate the drag was over(drop was happened). And use two variables dragFrom and dragTo to trace the really position in a completed "drag & drop".

private ItemTouchHelper.Callback dragCallback = new ItemTouchHelper.Callback() {

    int dragFrom = -1;
    int dragTo = -1;

    @Override
    public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        return makeMovementFlags(ItemTouchHelper.UP|ItemTouchHelper.DOWN|ItemTouchHelper.LEFT|ItemTouchHelper.RIGHT,
                0);
    }

    @Override
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {

        int fromPosition = viewHolder.getAdapterPosition();
        int toPosition = target.getAdapterPosition();


        if(dragFrom == -1) {
            dragFrom =  fromPosition;
        }
        dragTo = toPosition;

        adapter.onItemMove(fromPosition, toPosition);

        return true;
    }

    private void reallyMoved(int from, int to) {
        // I guessed this was what you want...
    }

    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {

    }

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

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

    @Override
    public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        super.clearView(recyclerView, viewHolder);

        if(dragFrom != -1 && dragTo != -1 && dragFrom != dragTo) {
            reallyMoved(dragFrom, dragTo);
        }

        dragFrom = dragTo = -1;
    }

};

adapter.onItemMove(fromPosition, toPosition) was like below:

public void onItemMove(int fromPosition, int toPosition) {
    list.add(toPosition, list.remove(fromPosition));
    notifyItemMoved(fromPosition, toPosition);
}

Solution 2

The onSelectedChanged(RecyclerView.ViewHolder, int) callback provides information about the current actionState:
- ACTION_STATE_IDLE:
- ACTION_STATE_DRAG
- ACTION_STATE_SWIPE

So you could keep track whether the order changed, and when the state changes to ACTION_STATE_IDLE, you can do what you need to do!

Example:

private final class MyCallback extends ItemTouchHelper.Callback {
    private boolean mOrderChanged;

    @Override
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
        // Check if positions of viewHolders correspond to underlying model, and if not, flip the items in the model and set the mOrderChanged flag
        mOrderChanged = true;
    }

    @Override
    public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
    super.onSelectedChanged(viewHolder, actionState);

    if (actionState == ItemTouchHelper.ACTION_STATE_IDLE && mOrderChanged) {
        doSomething();
        mOrderChanged = false;
    }
}

Solution 3

I did some tests and onSelectedChanged(RecyclerView.ViewHolder?, Int) seemed most reliable for me to detect end of the gesture (drop). The method is called whenever an item is being dragged and passed action state of ACTION_STATE_DRAG. When the drag is over, it is called with action state of ACTION_STATE_IDLE.

See my solution below. The onItemDrag(Int, Int) callback is used for reordering items in an adapter as the item is being dragged. On the other hand the onItemDragged(Int, Int) callback is intended for updating positions in a database at the end of the gesture.

class ItemGestureHelper(private val listener: OnItemGestureListener) : ItemTouchHelper.Callback() {

    interface OnItemGestureListener {

        fun onItemDrag(fromPosition: Int, toPosition: Int): Boolean

        fun onItemDragged(fromPosition: Int, toPosition: Int)

        fun onItemSwiped(position: Int)
    }

    private var dragFromPosition = -1
    private var dragToPosition = -1

    // Other methods omitted...

    override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
        // Item is being dragged, keep the current target position
        dragToPosition = target.adapterPosition
        return listener.onItemDrag(viewHolder.adapterPosition, target.adapterPosition)
    }

    override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
        listener.onItemSwiped(viewHolder.adapterPosition)
    }

    override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
        super.onSelectedChanged(viewHolder, actionState)

        when (actionState) {
            ItemTouchHelper.ACTION_STATE_DRAG -> {
                viewHolder?.also { dragFromPosition = it.adapterPosition }
            }
            ItemTouchHelper.ACTION_STATE_IDLE -> {
                if (dragFromPosition != -1 && dragToPosition != -1 && dragFromPosition != dragToPosition) {
                    // Item successfully dragged
                    listener.onItemDragged(dragFromPosition, dragToPosition)
                    // Reset drag positions
                    dragFromPosition = -1
                    dragToPosition = -1
                }
            }
        }
    }

}

Solution 4

You must implement OnMove listener in you adapter:

Collections.swap(youCoolList, fromPosition, toPosition); notifyItemMoved(fromPosition, toPosition);

like this man doing https://medium.com/@ipaulpro/drag-and-swipe-with-recyclerview-b9456d2b1aaf#.blviq6jxp

special grid example https://medium.com/@ipaulpro/drag-and-swipe-with-recyclerview-6a6f0c422efd#.xb74uu7ke

Share:
23,277

Related videos on Youtube

okarakose
Author by

okarakose

I'm an Android Developer.

Updated on September 17, 2020

Comments

  • okarakose
    okarakose almost 4 years

    I have a problem with ItemTouchHelper of RecyclerView.

    I am making a game. The game board is actually a RecyclerView. RecyclerView has GridLayoutManager with some span count. I want to implement drag & drop recyclerview's items. Any item can dragging over all directions (up, down, left, right).

    private void initializeLayout() {
        recyclerView.setHasFixedSize(true);
        recyclerView.setLayoutFrozen(true);
        recyclerView.setNestedScrollingEnabled(false);
    
        // set layout manager
        GridLayoutManager layoutManager = new GridLayoutManager(getContext(), BOARD_SIZE,
            LinearLayoutManager.VERTICAL, true);
        recyclerView.setLayoutManager(layoutManager);
    
        // Extend the Callback class
        ItemTouchHelper.Callback itemTouchCallback = new ItemTouchHelper.Callback() {
    
        public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
            Log.w(TAG, "onMove");
            return false;
        }
    
        @Override
        public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
            // Application does not include swipe feature.
        }
    
        @Override
        public void onMoved(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
                            int fromPos, RecyclerView.ViewHolder target, int toPos, int x, int y) {
            Log.d(TAG, "onMoved");
            // this is calling every time, but I need only when user dropped item, not after every onMove function.
        }
    
        @Override
        public boolean isItemViewSwipeEnabled() {
            return false;
        }
    
        @Override
        public boolean isLongPressDragEnabled() {
            return true;
        }
    
        @Override
        public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
            int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.START | ItemTouchHelper.END;
            int swipeFlags = 0;
            return makeMovementFlags(dragFlags, swipeFlags);
        }
        };
    
        ItemTouchHelper touchHelper = new ItemTouchHelper(itemTouchCallback);
        touchHelper.attachToRecyclerView(recyclerView);
    }
    

    SO, why ItemTouchHelper's onMoved function works when I still dragging item on the RecyclerView ? How can I achieve this ?

  • Ucdemir
    Ucdemir about 8 years
    I was looking this :=)
  • andre719mv
    andre719mv over 7 years
    Works as expected
  • seekingStillness
    seekingStillness over 7 years
    I've been trying to figure out how to know the drop has completed. Your explanation of the clearView() is perfect. Thanks for showing the implementation as well.
  • Matan Dahan
    Matan Dahan about 7 years
    This was also my intuitive answer, but apparently it does not work as expected...
  • andygeers
    andygeers over 6 years
    I used a method like this successfully by tracking the previous state each time onSelectedChanged was called - if it USED to be ACTION_STATE_DRAG and is now ACTION_STATE_IDLE then doSomething()
  • Yusuf Çağlar
    Yusuf Çağlar over 6 years
    This is way to go!
  • Riddhi Shankar
    Riddhi Shankar almost 6 years
    Worked as Expected, Thank you so much :)
  • Carnal
    Carnal over 5 years
    Thank you, very nice solution!
  • brucemax
    brucemax about 5 years
    I use recycle view in motion layout and clearView calls every time when I cross next item :( It start from removeAndRecycleScrapInt method in recycleview because getScrapCount() != 0 Upd: This behavior starts when I move from constraint-layout-alpha3 to beta1
  • Adrian Grygutis
    Adrian Grygutis almost 5 years
    Works perfect for me!
  • AlexS
    AlexS over 4 years
    This called when view is drag, not dropped. This is not work
  • Vivek
    Vivek about 4 years
    This will be perfect solution for those who just wanted to implement drag and drop action . As clearView() will be called in both in Drag and Swipe action and then differentiating between the action won't be possible
  • Dan
    Dan almost 4 years
    @andre719mv in which class i have write this above code, i mean in activity class, adapter or in class class DragItemTouchHelper extends ItemTouchHelper.Callback class ??
  • Dan
    Dan almost 4 years
    In which class i have write this above code, i mean in activity class, adapter or in class class DragItemTouchHelper extends ItemTouchHelper.Callback class ??
  • Dan
    Dan almost 4 years
    Please help me on same question stackoverflow.com/questions/62653282
  • Egist Li
    Egist Li over 3 years
    This is NOT the right way to do it since clearView can be called when a viewholder is somehow scrapped by recyclerview! This leads to incorrect timeing for calling your subsequence logics which may lead to crashes. Use onSelectedChanged is the right way to go. In onSelectedChanged you can check viewHolder == null and IDLE state which means a drop is finished (hence selected is set to null)
  • Dr.jacky
    Dr.jacky over 3 years
    For me, viewHolder is always null!