3

So, I'm trying to figure this out. I see many posts online about having RecyclerView items with both swipe and drag-- I've got that down. But what I can't get to work is both drag and long press actions.

I've got working code for both drag and longclick, the problem is, when I long click a recycler item, it's a toss up whether it will run the longclick function or the drag function.

Here is the code for the long click listener:

        holder.itemView.setOnLongClickListener {
             currentNote.toggleSelection(it)
             // change the MainActivity menu to the selection menu
             MainActivity.currentMenu = R.menu.menu_select
             (it.context as Activity).invalidateOptionsMenu()

             // set a flag to change the onClickListener to select notes rather than edit/view
             SELECTING = true
             notifyDataSetChanged()

             true  

Simple enough. And here is how I implemented the drag action:

        val itemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() {
        override fun isLongPressDragEnabled() = true
        override fun isItemViewSwipeEnabled() = true

        override fun getMovementFlags(
            recyclerView: RecyclerView,
            viewHolder: RecyclerView.ViewHolder
        ): Int {
            val dragFlags = UP or DOWN or START or END
            val swipeFlags = LEFT or RIGHT
            return makeMovementFlags(dragFlags, swipeFlags)
        }

        override fun onMove(
            recyclerView: RecyclerView,
            viewHolder: RecyclerView.ViewHolder,
            target: RecyclerView.ViewHolder
        ): Boolean {
            val fromPosition = viewHolder.adapterPosition
            val toPosition = target.adapterPosition
            val item = NOTES_ARRAY.removeAt(fromPosition)
            NOTES_ARRAY.add(toPosition, item)
            recyclerView.adapter!!.notifyItemMoved(fromPosition, toPosition)
            return true
        }
    })

This is a note taking app, so I use the long press action for doing multi select. So the question is, how can I differentiate between a long press action and a drag action, as they are both bound the the "long press."

I know that this is the problem, because if I comment out the onLongClickListener, I can easily drag and drop without issue.

claxtastic
  • 125
  • 2
  • 8

2 Answers2

0

I solved the problem by implementing a custom RecyclerView.OnItemTouchListener and by redefining what a long press is.

A long press in Android is triggered by a MotionEvent#ACTION_DOWN and then a continuous MotionEvent#ACTION_MOVE for a specific duration of time without moving much, and without needing the Pointer (finger or mouse) to triggere MotionEvent#ACTION_UP.

I had to changed the long press to be triggered after the continuous MotionEvent#ACTION_MOVE for a specific duration of time on MotionEvent#ACTION_UP, for the ItemTouchHelper to not misfire and trigger a Drag Event.

Custom on Item Touch Logic Implementation

import android.graphics.PointF;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;

import java.util.function.Supplier;

import de.blueworldgmbh.tracemate.util.TimberWrapper;

public class SelectionAndDragNDropOnTouchHelper
{
    private static final long LONG_PRESS_TIME_MS = ViewConfiguration.getLongPressTimeout();
    private static final float MOVE_MARGIN_PX = 15;

    private long initialTouchTime;
    private boolean isDragging = false;
    private boolean isScrolling = false;

    @NonNull
    private PointF initialPoint = new PointF();

    @NonNull
    private final ItemTouchHelper itemTouchHelper;

    @NonNull
    private final ItemTouchCallback touchCallback;

    @NonNull
    private final Supplier<Boolean> isMultiSelectActive;

    public SelectionAndDragNDropOnTouchHelper(
            @NonNull ItemTouchHelper itemTouchHelper,
            @NonNull ItemTouchCallback touchCallback,
            @NonNull Supplier<Boolean> isMultiSelectActive
    )
    {
        this.itemTouchHelper = itemTouchHelper;
        this.touchCallback = touchCallback;
        this.isMultiSelectActive = isMultiSelectActive;
        touchCallback.enabledDragNDrop(false);
    }

    public boolean onTouch(OnSelectionAndDragNDropOnTouchHelper v, MotionEvent event)
    {
        int action = event.getAction();
        PointF currentPoint = new PointF(event.getX(), event.getY());

        if (action == MotionEvent.ACTION_DOWN)
        {
            reset();
            initialTouchTime = System.currentTimeMillis();
            initialPoint = new PointF(event.getX(), event.getY());
        }
        else if (action == MotionEvent.ACTION_MOVE)
        {
            long pressTime = System.currentTimeMillis() - initialTouchTime;

            float distanceX = Math.abs(currentPoint.x - initialPoint.x);
            float distanceY = Math.abs(currentPoint.y - initialPoint.y);

            if (!isMultiSelectActive.get() && !isDragging && !isScrolling && pressTime > LONG_PRESS_TIME_MS)
            {
                if (distanceX > MOVE_MARGIN_PX || distanceY > MOVE_MARGIN_PX)
                {
                    isDragging = true;
                    touchCallback.enabledDragNDrop(true);
                    itemTouchHelper.startDrag(v.getViewHolder());
                }
            }
            else
            {
                if (distanceX > MOVE_MARGIN_PX || distanceY > MOVE_MARGIN_PX)
                {
                    isScrolling = true;
                }
            }
        }
        else if (action == MotionEvent.ACTION_UP)
        {
            if (isDragging)
            {
                touchCallback.enabledDragNDrop(false);
                return false;
            }

            if (isScrolling)
            {
                return false;
            }

            long pressTime = System.currentTimeMillis() - initialTouchTime;

            if (pressTime <= LONG_PRESS_TIME_MS)
            {
                v.notifyOnClick();
            }
            else
            {
                v.notifyOnLongClick();
            }

            return true;
        }

        return false;
    }

    private void reset()
    {
        touchCallback.enabledDragNDrop(false);
        isDragging = false;
        isScrolling = false;
    }

    public interface OnTouchHelper
    {
        void notifyOnClick();

        void notifyOnLongClick();

        RecyclerView.ViewHolder getViewHolder();
    }
}

To initialize this class you need the ItemTouchHelper of your list and it's callback ItemTouchHelper.Callback implementation, and a supplier for when the list is in multi-selection mode.

Check out the LONG_PRESS_TIME_MS constant, for how long the user has to press and hold for a Drag and Drop or a long press event to be triggered, I used ViewConfiguration.getLongPressTimeout() which for the phones I'm using is 400ms, I think it is a bit much, but I'm still fine tuning it.

Every time we return false, we indicate that we didn't "handle" the touch event. That is how the drag and drop event can be used after starting it manually here, or how scrolling will continue to function as intendet.

OnTouchHelper Interface

This interface is for the View Holders of the list. See below for the implementation.

ItemTouchHelper.Callback Implementation

This implementation need a few additions. First a method to enable or disable the drag and drop feature of the list, see enabledDragNDrop(boolean) and in the method getMovementFlags(RecyclerView, RecyclerView.ViewHolder) it should check if the drag and drop feature is active to pass on the drag flags or not. ❕ Do not overlook the swipe flags, I only had to deal with Drag and Drop.

@Override
public int getMovementFlags(
        @NotNull RecyclerView recyclerView,
        @NotNull RecyclerView.ViewHolder viewHolder
) 
{
    if (enabled) 
    {
        return makeFlag(ACTION_STATE_DRAG, ItemTouchHelper.DOWN | ItemTouchHelper.UP);
    }
    else 
    {
        return 0;
    }
}

public void enabledDragNDrop(boolean enabled) {
    this.enabled = enabled;
}

RecyclerView.ViewHolder implements OnTouchHelper

In the View Holder, do not register the click or long click listeners with the view, but "triggere" it manually, otherwise it will trigger twice. If you find a way to register these listener and not triggere it twice, a comment is appreciated

@Override
public void notifyOnClick()
{
    if (clickListener != null)
    {
        // custom click listener interface
        clickListener.onItemClick(this, getAdapterPosition());
    }
}

@Override
public void notifyOnLongClick()
{
    if (longClickListener != null)
    {
        // custom long click listener interface
        longClickListener.onItemLongClick(this, getAdapterPosition());
    }
}

@Override
public RecyclerView.ViewHolder getViewHolder()
{
    return this;
}

RecyclerView.OnItemTouchListener

And finally the RecyclerView.OnItemTouchListener, only implement onInterceptTouchEvent and ignore the other two methods

@Override
public boolean onInterceptTouchEvent(
        @NonNull RecyclerView rv,
        @NonNull MotionEvent motionEvent
)
{
    View view = rv.findChildViewUnder(motionEvent.getX(), motionEvent.getY());
    if (view != null)
    {
        RecyclerView.ViewHolder viewHolder = rv.findContainingViewHolder(view);
        if (viewHolder instanceof SelectionAndDragNDropOnTouchHelper.OnTouchHelper)
        {
            return selectionAndDragNDropOnTouchHelper.onTouch(
                    (SelectionAndDragNDropOnTouchHelper.OnTouchHelper) viewHolder,
                    motionEvent
            );
        }
    }
    return false;
}

Here we get the view via the touch coordinates and with the view we get the View Holder.

Just pass the item touch event along to the custom on item touch logic implementation in SelectionAndDragNDropOnTouchHelper, and return it's return value, otherwise return false.


I know the code can be optimize a bit more and written better, but it is fulfilling it's function.

I hope it helps

iboalali
  • 379
  • 1
  • 5
  • 13
0

I think the recommended way to resolve this problem is to override ItemTouchHelper.Callback.isLongPressDragEnabled to return false and then call ItemTouchHelper.startDrag() in your long-click handler

tipa
  • 369
  • 1
  • 5
  • 17