An overview of the

RecyclerView can not only display large data sets in a limited window, but also Swipe and Drag the Item view, which can be easily achieved by using the ItemTouchHelper helper class.

The basic use

Key code:

// 1. Create itemTouchHelper. Callback to implement the Callback method
ItemTouchHelper.Callback callback = new ItemTouchHelper.Callback() {
    // Return the allowed sliding direction
    @Override
    public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
        // Return the slippable direction by using an int to mark each bit.
        Swipe supports up and down, and swipe supports left and right.
        int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
        int swipeFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
        // Returns a compound int with the identity bit set
        return makeMovementFlags(dragFlags, swipeFlags);
    }

    // Called when ItemTouchHelper wants to move the dragged item from its old location to a new location, with drag allowed
    @Override
    public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
        // Get the adapter index of the dragged item and the target item (the adapter index is the index of the data set corresponding to the item, and getLayoutPosition is the position of the current layout)
        int from = viewHolder.getAdapterPosition();
        int to = target.getAdapterPosition();
        // Exchange the data of the dataset
        Collections.swap(data, from, to);
        // Notify Adapter to update
        adapter.notifyItemMoved(from, to);
        // Return true to indicate that item has been moved to the target location
        return true;
    }

    // Allow swipe to be called when the user slides ViewHolder to trigger criticals
    @Override
    public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
        // Get the adaptor index of the sliding item
        int pos = viewHolder.getAdapterPosition();
        // Remove data from the dataset
        data.remove(pos);
        // Notify Adapter to updateadapter.notifyItemRemoved(pos); }};// 2. Pass itemTouchHelper.callback
ItemTouchHelper touchHelper = new ItemTouchHelper(callback);
// 3. Bind touchHelper to recyclerView
touchHelper.attachToRecyclerView(recyclerView);
Copy the code

Swipe and Drag can be achieved in the following three steps:

Critical thinking

We know that RecyclerView, as a ViewGroup, has its own sliding events, so how can ItemTouchHelper swipe and drag without causing a conflict?

How ItemTouchHelper can host touch events to itself by attaching RecyclerView to it via attachToRecyclerView.

Itemtouchhelper. Callback interface onMove and onSwiped are called when.

Note in the diagram above, the Drag operation will scroll up the RecyclerView when the item reaches the boundary.

With these questions in mind, dive into the source code to see how ItemTouchHelper works in general.

The source code to explore

In this paper, the source code is based on ‘androidx. Recyclerview: recyclerview: 1.1.0’

ItemTouchHelper binding

AttachToRecyclerView [ItemTouchHelper#attachToRecyclerView]

public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
    if (mRecyclerView == recyclerView) {
        return; // nothing to do
    }
    // If bind other RecyclerView, unbind and clean the old data
    if(mRecyclerView ! =null) {
        destroyCallbacks();
    }
    mRecyclerView = recyclerView;
    if(recyclerView ! =null) {
        final Resources resources = recyclerView.getResources();
        mSwipeEscapeVelocity = resources
                .getDimension(R.dimen.item_touch_helper_swipe_escape_velocity);
        mMaxSwipeVelocity = resources
                .getDimension(R.dimen.item_touch_helper_swipe_escape_max_velocity);
        // Set the callbacksetupCallbacks(); }}Copy the code

The key is in the setupCallbacks method: [itemTouchPer #setupCallbacks]

private void setupCallbacks(a) {
    ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());
    mSlop = vc.getScaledTouchSlop();
    // Add the mItemDecorations collection to mRecyclerView
    mRecyclerView.addItemDecoration(this);
    // Add to mRecyclerView's mOnItemTouchListeners collection
    mRecyclerView.addOnItemTouchListener(mOnItemTouchListener);
    / / added to mRecyclerView mOnChildAttachStateListeners collection
    mRecyclerView.addOnChildAttachStateChangeListener(this);
    / / create the GestureDetector
    startGestureDetection();
}
Copy the code

This method registers the various callback listeners that are key to swipe and drag.

ItemTouchHelper inherits ItemDecoration, which decorates the item and is usually used to draw a dividing line. ItemTouchHelper uses it to move items with your finger.

MOnItemTouchListener registers with RecyclerView, which calls back events to it. ItemTouchHelper intercepts events and processes them itself.

ItemTouchHelper OnChildAttachStateChangeListener interface, In the interface onChildViewDetachedFromWindow method when dealing with detached view to release action or animation, clear view references.

The startGestureDetection method creates a GestureDetector that listens for touch events. When onLongPress is triggered, determine whether to start drag.

RecyclerView Touch event hosting

Now look at how RecyclerView can host touch events to ItemTouchHelper.

onInterceptTouchEvent

[RecyclerView#onInterceptTouchEvent]

public boolean onInterceptTouchEvent(MotionEvent e) {
    / /...
    mInterceptingOnItemTouchListener = null;
    // Check whether OnItemTouchListener intercepts the event
    if (findInterceptingOnItemTouchListener(e)) {
        // Cancel scrolling if there are intercepts
        cancelScroll();
        return true;
    }
    // RecyclerView onInterceptTouchEvent logic
}

private boolean findInterceptingOnItemTouchListener(MotionEvent e) {
    int action = e.getAction();
    final int listenerCount = mOnItemTouchListeners.size();
    // Send events to listeners saved by the mOnItemTouchListeners
    for (int i = 0; i < listenerCount; i++) {
        final OnItemTouchListener listener = mOnItemTouchListeners.get(i);
        if (listener.onInterceptTouchEvent(this, e) && action ! = MotionEvent.ACTION_CANCEL) {/ / if the listener to intercept and current events not CANCEL, use mInterceptingOnItemTouchListener save the listener, the end of the traverse
            mInterceptingOnItemTouchListener = listener;
            return true; }}return false;
}
Copy the code

RecyclerView will be sent to OnItemTouchListener set before processing RecyclerView’s own event interception logic in onInterceptTouchEvent. If OnItemTouchListener is processed, RecyclerView itself is no longer processed.

onTouchEvent

[RecyclerView#onTouchEvent]

public boolean onTouchEvent(MotionEvent e) {
    / /...
    // Check if there is an OnItemTouchListener consumption event
    if (dispatchToOnItemTouchListeners(e)) {
        // Unscroll if there is a consumption event
        cancelScroll();
        return true;
    }
    // RecyclerView's onTouchEvent logic
}

private boolean dispatchToOnItemTouchListeners(MotionEvent e) {
    if (mInterceptingOnItemTouchListener == null) {
        // If OnItemTouchListener is not intercepting the event when onInterceptTouchEvent is intercepted, the OnItemTouchListener is intercepting
        // Events are also sent to OnItemTouchListener, but DOWN events are filtered out to avoid repeated dispatches.
        if (e.getAction() == MotionEvent.ACTION_DOWN) {
            return false;
        }
        return findInterceptingOnItemTouchListener(e);
    } else {
        // If there is an OnItemTouchListener that intercepts the event, pass it directly to its onTouchEvent method
        mInterceptingOnItemTouchListener.onTouchEvent(this, e);
        final int action = e.getAction();
        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
            / / if the end of the sequence of events, the empty mInterceptingOnItemTouchListener
            mInterceptingOnItemTouchListener = null;
        }
        return true; }}Copy the code

Before RecyclerView processes its logic in onTouchEvent, it will send events to OnItemTouchListener first. If there is a consumption event, RecyclerView itself will not process any more.

requestDisallowInterceptTouchEvent

RecyclerView rewrite the requestDisallowInterceptTouchEvent method, in which also callback OnItemTouchListener: [RecyclerView#requestDisallowInterceptTouchEvent]

public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    final int listenerCount = mOnItemTouchListeners.size();
    // Call OnItemTouchListeners in sequence
    for (int i = 0; i < listenerCount; i++) {
        final OnItemTouchListener listener = mOnItemTouchListeners.get(i);
        // In the ItemTouchHelper listener, if the event passed in does not want to be intercepted, the ItemTouchHelper will release the move
        listener.onRequestDisallowInterceptTouchEvent(disallowIntercept);
    }
    super.requestDisallowInterceptTouchEvent(disallowIntercept);
}
Copy the code

ItemTouchHelper intercepts event handling

When RecyclerView receives the touch event, it will give the event to OnItemTouchListener first. If any event is consumed, RecyclerView itself will no longer consume. The ItemTouchHelper receives an event via OnItemTouchListener that triggers SWIPE or DRAG.

onInterceptTouchEvent

Take a look at the corresponding event interception method implemented in ItemTouchHelper’s mOnItemTouchListener.

[OnItemTouchListener#onInterceptTouchEvent]

public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent event) {
    // GestureDetector listens for input events
    mGestureDetector.onTouchEvent(event);
    if (DEBUG) {
        Log.d(TAG, "intercept: x:" + event.getX() + ",y:" + event.getY() + "," + event);
    }
    final int action = event.getActionMasked();
    if (action == MotionEvent.ACTION_DOWN) {
        // If the event is the start of a sequence of events, record the touch point ID and the initial coordinate position
        mActivePointerId = event.getPointerId(0);
        mInitialTouchX = event.getX();
        mInitialTouchY = event.getY();
        obtainVelocityTracker();
        // The mSelected member records the ViewHolder currently selected. The default value is null
        if (mSelected == null) {
            Find the response animations for the corresponding item based on the event position from the mRecoverAnimations collection. (The response animations are animations that automatically move the view to the specified position when a finger is released.)
            final RecoverAnimation animation = findAnimation(event);
            if(animation ! =null) {
                // Animation's mX and mY record the current view offset position
                mInitialTouchX -= animation.mX;
                mInitialTouchY -= animation.mY;
                // End the animation
                endRecoverAnimation(animation.mViewHolder, true);
                // mPendingCleanup Cache detached view to be cleaned
                if (mPendingCleanup.remove(animation.mViewHolder.itemView)) {
                    // Restore the TranslationX and TranslationY Elevation attributes of the view to 0
                    mCallback.clearView(mRecyclerView, animation.mViewHolder);
                }
                // Re-use this item as the selected ViewHolder
                select(animation.mViewHolder, animation.mActionState);
                // Update mDx, mDy (mDx, mDy)
                updateDxDy(event, mSelectedFlags, 0); }}}else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
        // If the event is the end of a sequence of events, empty the touchpoint ID and release the ViewHolder selected
        mActivePointerId = ACTIVE_POINTER_ID_NONE;
        select(null, ACTION_STATE_IDLE);
    } else if(mActivePointerId ! = ACTIVE_POINTER_ID_NONE) {// in a non scroll orientation, if distance change is above threshold, we
        // can select the item
        // The event in this case is an intermediate event in the sequence of events, if the touchpoint ID exists (the previous ACTION_DOWN will be saved)
        // Get the touchpoint index
        final int index = event.findPointerIndex(mActivePointerId);
        if (DEBUG) {
            Log.d(TAG, "pointer index " + index);
        }
        if (index >= 0) {
            // Check to see if the swipe trigger condition is met. If so, select is calledcheckSelectForSwipe(action, event, index); }}if(mVelocityTracker ! =null) {
        // Listen for event for acceleration calculation
        mVelocityTracker.addMovement(event);
    }
    // If a ViewHolder is selected, true is returned to indicate interception
    returnmSelected ! =null;
}
Copy the code

The main logic of this method is to record the initial touch position in ACTION_DOWN and judge whether the swipe trigger condition is met in ACTION_MOVE, ACTION_POINTER_DOWN and ACTION_POINTER_UP. Released when ACTION_UP or ACTION_CANCEL.

Calling checkSelectForSwipe method to check swipe condition is the key method to trigger SWIPE. Also notice that there are multiple places where the Select method is called, which is also the key method that handles item selection and release operations.

onTouchEvent

[OnItemTouchListener#onTouchEvent]

public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent event) {
    // gesture listener
    mGestureDetector.onTouchEvent(event);
    if (DEBUG) {
        Log.d(TAG,
                "on touch: x:" + mInitialTouchX + ",y:" + mInitialTouchY + ",," + event);
    }
    // Acceleration monitor
    if(mVelocityTracker ! =null) {
        mVelocityTracker.addMovement(event);
    }
    if (mActivePointerId == ACTIVE_POINTER_ID_NONE) {
        return;
    }
    final int action = event.getActionMasked();
    final int activePointerIndex = event.findPointerIndex(mActivePointerId);
    if (activePointerIndex >= 0) {
        // Check whether swipe is triggered
        checkSelectForSwipe(action, event, activePointerIndex);
    }
    ViewHolder viewHolder = mSelected;
    if (viewHolder == null) {
        Swipe condition is not met to return
        return;
    }
    Swipe or drag is swipe or drag
    switch (action) {
        case MotionEvent.ACTION_MOVE: {
            // Find the index of the active pointer and fetch its position
            if (activePointerIndex >= 0) {
                // Update the sliding offset
                updateDxDy(event, mSelectedFlags, activePointerIndex);
                // If the item is in drag, the onMove callback is triggered
                moveIfNecessary(viewHolder);
                // mScrollRunnable is used to handle LayoutManager scrolling when the user drags an item beyond the edge
                mRecyclerView.removeCallbacks(mScrollRunnable);
                mScrollRunnable.run();
                // Trigger RecyclerView redraw
                mRecyclerView.invalidate();
            }
            break;
        }
        case MotionEvent.ACTION_CANCEL:
            // If the CANCEL event (finger marks the view range) clears the acceleration calculation
            if(mVelocityTracker ! =null) {
                mVelocityTracker.clear();
            }
            // fall through
        case MotionEvent.ACTION_UP:
            ACTION_CANCEL and ACTION_UP are both released selected
            select(null, ACTION_STATE_IDLE);
            mActivePointerId = ACTIVE_POINTER_ID_NONE;
            break;
        case MotionEvent.ACTION_POINTER_UP: {
            // Update the touch point ID and slide offset when one finger is lifted in multi-finger touch
            final int pointerIndex = event.getActionIndex();
            final int pointerId = event.getPointerId(pointerIndex);
            if (pointerId == mActivePointerId) {
                // This was our active pointer going up. Choose a new
                // active pointer and adjust accordingly.
                final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
                mActivePointerId = event.getPointerId(newPointerIndex);
                updateDxDy(event, mSelectedFlags, pointerIndex);
            }
            break; }}}Copy the code

The checkSelectForSwipe method is also used to determine whether swipe is triggered. Notice that in the ACTION_MOVE case is the key code for dragging and sliding.

As you can see, ItemTouchHelper also passes the events it receives to the GestureDetector and VelocityTracker. The GestureDetector is used to monitor long press events, and the VelocityTracker is used to calculate acceleration and determine whether to swipe out item when all fingers are lifted.

SWIPE and DRAG trigger criteria

Motion state

[ItemTouchHelper]

Swipe or Drag has not yet been triggered by any user event
public static final int ACTION_STATE_IDLE = 0;
// Swipe view is currently underway
public static final int ACTION_STATE_SWIPE = 1;
// Currently in drag view
public static final int ACTION_STATE_DRAG = 2;

private int mActionState = ACTION_STATE_IDLE;
Copy the code

The mActionState member of ItemTouchHelper is used to record the current state.

SWIPE and DRAG cannot be triggered at the same time. Let’s look at the trigger conditions for both operations.

SWIPE the trigger

ItemTouchHelper: [ItemTouchHelper#checkSelectForSwipe]

void checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) {
    if(mSelected ! =null|| action ! = MotionEvent.ACTION_MOVE || mActionState == ACTION_STATE_DRAG || ! mCallback.isItemViewSwipeEnabled()) {Swipe can be disabled by overriding the isItemViewSwipeEnabled method.
        // Return if the selected ViewHolder or current event is not ACTION_MOVE or is currently in DRAG or Swipe is disabled.
        return;
    }
    if (mRecyclerView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) {
        // RecyclerView itself is already in the process of scrolling, then return
        return;
    }
    // Find the sliderable ViewHolder
    final ViewHolder vh = findSwipedView(motionEvent);
    if (vh == null) {
        // If no ViewHolder matches, return
        return;
    }
    Itemtouchhelper. Callback's getMovementFlags Callback method is called to return the orientation we set
    final int movementFlags = mCallback.getAbsoluteMovementFlags(mRecyclerView, vh);

    // Retrieve the swipe identifier
    final int swipeFlags = (movementFlags & ACTION_MODE_SWIPE_MASK)
            >> (DIRECTION_FLAG_COUNT * ACTION_STATE_SWIPE);

    // Determine if there is a direction to support Swipe
    if (swipeFlags == 0) {
        return;
    }

    // mDx and mDy are only set in allowed directions. We use custom x/y here instead of
    // updateDxDy to avoid swiping if user moves more in the other direction
    final float x = motionEvent.getX(pointerIndex);
    final float y = motionEvent.getY(pointerIndex);

    // Calculate the distance moved
    // Calculate the sliding distance
    final float dx = x - mInitialTouchX;
    final float dy = y - mInitialTouchY;
    // swipe target is chose w/o applying flags so it does not really check if swiping in that
    // direction is allowed. This why here, we use mDx mDy to check slope value again.
    // take the absolute value
    final float absDx = Math.abs(dx);
    final float absDy = Math.abs(dy);

    // Check whether the distance reaches the minimum sliding distance
    if (absDx < mSlop && absDy < mSlop) {
        return;
    }
    // Determine the sliding direction, horizontal and vertical sliding offset, which direction is larger, belongs to which direction
    if (absDx > absDy) {
        // Swipe your finger to the left to determine whether the left slide is supported
        if (dx < 0 && (swipeFlags & LEFT) == 0) {
            return;
        }
        // Determine whether a right swipe is supported
        if (dx > 0 && (swipeFlags & RIGHT) == 0) {
            return; }}else {
        // Swipe your finger up to see if you can slide up
        if (dy < 0 && (swipeFlags & UP) == 0) {
            return;
        }
        // Determine whether to support the slide
        if (dy > 0 && (swipeFlags & DOWN) == 0) {
            return; }}// The ViewHolder is located and the SWIPE condition is met.
    // New start SWIPE, reset variable
    mDx = mDy = 0f;
    mActivePointerId = motionEvent.getPointerId(0);
    // Pass the corresponding states of ViewHolder and SWIPE
    select(vh, ACTION_STATE_SWIPE);
}
Copy the code

SWIPE ViewHolder [ItemTouchHelper#findSwipedView]

private ViewHolder findSwipedView(MotionEvent motionEvent) {
    / / get LayoutManager
    final RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager();
    if (mActivePointerId == ACTIVE_POINTER_ID_NONE) {
        return null;
    }
    final int pointerIndex = motionEvent.findPointerIndex(mActivePointerId);
    // Calculate the sliding offset
    final float dx = motionEvent.getX(pointerIndex) - mInitialTouchX;
    final float dy = motionEvent.getY(pointerIndex) - mInitialTouchY;
    final float absDx = Math.abs(dx);
    final float absDy = Math.abs(dy);

    // Determine whether the slip offset reaches the minimum slip distance
    if (absDx < mSlop && absDy < mSlop) {
        return null;
    }
    if (absDx > absDy && lm.canScrollHorizontally()) {
        // If you SWIPE horizontally and the current LayoutManager can SWIPE horizontally, do not SWIPE to avoid collisions
        // (for example, LinearLayoutManager cannot SWIPE horizontally)
        return null;
    } else if (absDy > absDx && lm.canScrollVertically()) {
        // Same as above, if you SWIPE vertically and LayoutManager can also SWIPE vertically, not SWIPE
        return null;
    }
    // Find the view based on the event location
    View child = findChildView(motionEvent);
    if (child == null) {
        return null;
    }
    // Return the ViewHolder of the view
    return mRecyclerView.getChildViewHolder(child);
}
Copy the code

FindChildView [ItemTouchHelper#findChildView]

View findChildView(MotionEvent event) {
    // first check elevated views, if none, then call RV
    final float x = event.getX();
    final float y = event.getY();
    if(mSelected ! =null) {
        final View selectedView = mSelected.itemView;
        // If there is a ViewHolder selected, determine whether the touchpoint position falls within the view scope
        if (hitTest(selectedView, x, y, mSelectedStartX + mDx, mSelectedStartY + mDy)) {
            returnselectedView; }}for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) {
        // If there is a reply animation, check whether the position of the touch point falls in the view range of the animation execution
        final RecoverAnimation anim = mRecoverAnimations.get(i);
        final View view = anim.mViewHolder.itemView;
        if (hitTest(view, x, y, anim.mX, anim.mY)) {
            returnview; }}// Go through the child view of RecyclerView from top to bottom to get the view where the touch point falls
    return mRecyclerView.findChildViewUnder(x, y);
}
Copy the code

The conditions for triggering SWIPE are summarized as follows: first, calculate the sliding distance and sliding direction, which must meet the minimum sliding distance and not conflict with the sliding direction of LayoutManager. Obtain the ViewHolder of the corresponding view according to the position of the touch point. Swipe is allowed and swipe is not allowed in the current direction, as set by itemTouchHelper. Callback. If yes, the select method is called, passing ViewHolder and ACTION_STATE_SWIPE to determine the selection.

DRAG the trigger

The onLongPress callback is triggered only when the DRAG is long. ItemTouchHelper listens for the MotionEvent through the GestureDetector and triggers the onLongPress callback when it is long: [ItemTouchHelperGestureListener#onLongPress]

public void onLongPress(MotionEvent e) {
    if(! mShouldReactToLongPress) {return;
    }
    // Get the corresponding view according to the position of the touch point
    View child = findChildView(e);
    if(child ! =null) {
        // Get the ViewHolder corresponding to the view
        ViewHolder vh = mRecyclerView.getChildViewHolder(child);
        if(vh ! =null) {
            // If drag is supported or not, itemTouchHelper. Callback will trigger getMovementFlags,
            // Return true if the drag direction is set
            if(! mCallback.hasDragFlag(mRecyclerView, vh)) {return;
            }
            int pointerId = e.getPointerId(0);
            // Long press is deferred.
            // Check w/ active pointer id to avoid selecting after motion
            // event is canceled.
            if (pointerId == mActivePointerId) {
                final int index = e.findPointerIndex(mActivePointerId);
                final float x = e.getX(index);
                final float y = e.getY(index);
                // Save the initial touch position coordinates
                mInitialTouchX = x;
                mInitialTouchY = y;
                mDx = mDy = 0f;
                if (DEBUG) {
                    Log.d(TAG,
                            "onlong press: x:" + mInitialTouchX + ",y:" + mInitialTouchY);
                }
                // Check whether drag is allowed. Default returns true
                if (mCallback.isLongPressDragEnabled()) {
                    // Select
                    select(vh, ACTION_STATE_DRAG);
                }
            }
        }
    }
}
Copy the code

As you can see in the long-press callback, the select method is also called to determine if drag is supported, passing in the long-press ViewHolder and ACTION_STATE_DRAG.

Select Select and release

As you saw earlier, SELECT is called when SWIPE or DRAG is triggered, as well as when ACTION_CANCEL or ACTION_UP is triggered.

The timing Parameters selected Parameter actionState
Trigger a SWIPE Press and hold the ViewHolder ACTION_STATE_SWIPE
Trigger a DRAG Press and hold the ViewHolder ACTION_STATE_DRAG
Lift the release null ACTION_STATE_IDLE

[itemTouchHelper_select] [itemTouchHelper_select] [itemTouchHelper_select]

void select(@Nullable ViewHolder selected, int actionState) {
    if (selected == mSelected && actionState == mActionState) {
        // Avoid repeated calls
        return;
    }
    mDragScrollStartTimeInMs = Long.MIN_VALUE;
    final int prevActionState = mActionState;
    // prevent duplicate animations
    endRecoverAnimation(selected, true);
    mActionState = actionState;
    if (actionState == ACTION_STATE_DRAG) {
        // If DRAG is triggered
        if (selected == null) {
            throw new IllegalArgumentException("Must pass a ViewHolder when dragging");
        }

        // we remove after animation is complete. this means we only elevate the last drag
        // child but that should perform good enough as it is very hard to start dragging a
        // new child before the previous one settles.
        // Save a reference to the selected view
        mOverdrawChild = selected.itemView;
        // If the ViewGroup is smaller than 21, the custom traversal child rule is set
        addChildDrawingOrderCallback();
    }
    int actionStateMask = (1 << (DIRECTION_FLAG_COUNT + DIRECTION_FLAG_COUNT * actionState))
            - 1;
    boolean preventLayout = false;
    
    // If a ViewHolder was previously selected, it will be released
    if(mSelected ! =null) {
        // omit the release logic
    }
    // The ViewHolder to be selected is null if the ViewHolder is currently released
    if(selected ! =null) {
        mSelectedFlags =
                (mCallback.getAbsoluteMovementFlags(mRecyclerView, selected) & actionStateMask)
                        >> (mActionState * DIRECTION_FLAG_COUNT);
        // Record will be selected in the upper left corner of the view
        mSelectedStartX = selected.itemView.getLeft();
        mSelectedStartY = selected.itemView.getTop();
        // mSelected assigns the ViewHolder to be selected
        mSelected = selected;

        if (actionState == ACTION_STATE_DRAG) {
            // If DRAG is triggered, a haptic feedback is given to the usermSelected.itemView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); }}final ViewParent rvParent = mRecyclerView.getParent();
    if(rvParent ! =null) {
        // Request the parent layout not to intercept events when selected, and vice versa when releasedrvParent.requestDisallowInterceptTouchEvent(mSelected ! =null);
    }
    if(! preventLayout) {// Make RecyclerView run SimpleAnimation on the next layout
        mRecyclerView.getLayoutManager().requestSimpleAnimationsInNextLayout();
    }
    // Call onSelectedChanged to ItemTouchHelper.Callback
    mCallback.onSelectedChanged(mSelected, mActionState);
    // Trigger RecyclerView redraw
    mRecyclerView.invalidate();
}
Copy the code

You can see that when swipe or drag is triggered, the main logic is to save the top-left coordinate of the selected view and the ViewHolder reference.

In the case of drag, it also saves references to the selected view and sets the custom order in which the ViewGroup traverses the child (API<21). The purpose of this is to keep the view above other views while dragging it. We know that the ViewGroup draws children in the order of the mChildren array by default. In API<21, we can set a custom traversal rule to draw the specified view last, so that the specified child is at the top. In API>=21 you can set elevation to the upper level.

[ItemTouchHelper#select]

void select(@Nullable ViewHolder selected, int actionState) {
    if (selected == mSelected && actionState == mActionState) {
        return;
    }
    mDragScrollStartTimeInMs = Long.MIN_VALUE;
    final int prevActionState = mActionState;
    // prevent duplicate animations
    endRecoverAnimation(selected, true);
    mActionState = actionState;
    // omit the ACTION_STATE_DRAG part
    int actionStateMask = (1 << (DIRECTION_FLAG_COUNT + DIRECTION_FLAG_COUNT * actionState))
            - 1;
    boolean preventLayout = false;
    
    // If a ViewHolder was previously selected, it will be released
    if(mSelected ! =null) {
        final ViewHolder prevSelected = mSelected;
        // Determine if the previously selected view is still attached to the parent layout
        if(prevSelected.itemView.getParent() ! =null) {
            // If there is no drag operation before, then get the sliding direction.
            . / / call the Callback getMovementFlags we set direction, and then make sliding acceleration
            / / is more than a Callback. GetSwipeEscapeVelocity set threshold or sliding distance is beyond
            / / Callback. GetSwipeThreshold setting the threshold value. If you do, the view will slide away, swipeDir is the direction of the slide.
            // Otherwise swipeDir is 0.
            final int swipeDir = prevActionState == ACTION_STATE_DRAG ? 0
                    : swipeIfNecessary(prevSelected);
            releaseVelocityTracker();
            // find where we should animate to
            final float targetTranslateX, targetTranslateY;
            int animationType;
            // Calculate offset distance according to sliding direction
            switch (swipeDir) {
                case LEFT:
                case RIGHT:
                case START:
                case END:
                    // In the horizontal direction, the Y axis remains unchanged, and the X axis needs to move outside the RecyclerView boundary
                    targetTranslateY = 0;
                    targetTranslateX = Math.signum(mDx) * mRecyclerView.getWidth();
                    break;
                case UP:
                case DOWN:
                    targetTranslateX = 0;
                    targetTranslateY = Math.signum(mDy) * mRecyclerView.getHeight();
                    break;
                default:
                    targetTranslateX = 0;
                    targetTranslateY = 0;
            }
            // Record the animation type
            if (prevActionState == ACTION_STATE_DRAG) {
                animationType = ANIMATION_TYPE_DRAG;
            } else if (swipeDir > 0) {
                // ItemTouchHelper treats a complete SWIPE as a SWIPE success
                animationType = ANIMATION_TYPE_SWIPE_SUCCESS;
            } else {
                // Return to original position as SWIPE cancelled
                animationType = ANIMATION_TYPE_SWIPE_CANCEL;
            }
            // Calculate the current X and Y offsets of the selected view in the mTmpPosition array
            getSelectedDxDy(mTmpPosition);
            final float currentTranslateX = mTmpPosition[0];
            final float currentTranslateY = mTmpPosition[1];
            // Create RecoverAnimation to encapsulate property animation operations internally. Move the view from currentTranslate to targetTranslate.
            final RecoverAnimation rv = new RecoverAnimation(prevSelected, animationType,
                    prevActionState, currentTranslateX, currentTranslateY,
                    targetTranslateX, targetTranslateY) {
                @Override
                public void onAnimationEnd(Animator animation) {
                    // The animation is complete
                    super.onAnimationEnd(animation);
                    // If the user touches the view again during the animation, mOverridden will be marked as true
                    if (this.mOverridden) {
                        return;
                    }
                    if (swipeDir <= 0) {
                        // this is a drag or failed swipe. recover immediately
                        mCallback.clearView(mRecyclerView, prevSelected);
                        // full cleanup will happen on onDrawOver
                    } else {
                        // Slide out animation ends
                        // wait until remove animation is complete.
                        // Save the view into the mPendingCleanup collection for later cleanup
                        mPendingCleanup.add(prevSelected.itemView);
                        mIsPendingCleanup = true;
                        if (swipeDir > 0) {
                            // Swipe away view
                            // Animation might be ended by other animators during a layout.
                            // We defer callback to avoid editing adapter during a layout.
                            // Send the main thread and call callback. onSwiped when no animation is executed.
                            // In this callback, we remove the corresponding item from the adapter dataset.
                            postDispatchSwipe(this, swipeDir); }}// removed from the list after it is drawn for the last time
                    if (mOverdrawChild == prevSelected.itemView) {
                        // For drag, mOverdrawChild is set to view,
                        // We need to clean up references and cancel the ViewGroup custom traversal child rule.removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView); }}};final long duration = mCallback.getAnimationDuration(mRecyclerView, animationType,
                    targetTranslateX - currentTranslateX, targetTranslateY - currentTranslateY);
            rv.setDuration(duration);
            // Save the animation to the mRecoverAnimations collection
            mRecoverAnimations.add(rv);
            // Start animation
            rv.start();
            // The preventLayout flag is true and RecyclerView will not execute SimpleAnimation
            preventLayout = true;
        } else {
            removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);
            mCallback.clearView(mRecyclerView, prevSelected);
        }
        // mSelected is null
        mSelected = null;
    }
    if(selected ! =null) {
        // omit the selected logic
    }
    final ViewParent rvParent = mRecyclerView.getParent();
    if(rvParent ! =null) {
        // Request the parent layout not to intercept events when selected, and vice versa when releasedrvParent.requestDisallowInterceptTouchEvent(mSelected ! =null);
    }
    if(! preventLayout) {// Make RecyclerView run SimpleAnimation on the next layout
        mRecyclerView.getLayoutManager().requestSimpleAnimationsInNextLayout();
    }
    // Call onSelectedChanged to ItemTouchHelper.Callback
    mCallback.onSelectedChanged(mSelected, mActionState);
    // Trigger RecyclerView redraw
    mRecyclerView.invalidate();
}
Copy the code

Swipe or Drag is judged to swipe or drag and whether the view is slid away. Calculate Translate and create RecoverAnimation to execute the property animation. Swipe slides away after animation is completed and is sent to the main thread to be called callback. onSwiped if there is no animation. If drag, remove the ViewGroup custom traversal Child rule.

SWIPE and DRAG

When select ViewHolder and onTouchEvent process ACTION_MOVE both trigger recyclerView. invalidate redraw.

RecyclerView rewrites draw and onDraw, RecyclerView#onDraw, RecyclerView#onDraw

public void onDraw(Canvas c) {
    super.onDraw(c);

    final int count = mItemDecorations.size();
    for (int i = 0; i < count; i++) {
        mItemDecorations.get(i).onDraw(c, this, mState); }}public void draw(Canvas c) {
    super.draw(c);

    final int count = mItemDecorations.size();
    for (int i = 0; i < count; i++) {
        mItemDecorations.get(i).onDrawOver(c, this, mState);
    }
    
    / /...
}
Copy the code

ItemTouchHelper inherits ItemDecoration and adds mItemDecorations to RecyclerView. Therefore, when redrawing, the onDraw and onDrawOver methods of ItemTouchHelper will be called successively.

[ItemTouchHelper#onDraw]

public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
    // we don't know if RV changed something so we should invalidate this index.
    mOverdrawChildPosition = -1;
    float dx = 0, dy = 0;
    if(mSelected ! =null) {
        // Calculate the offset
        getSelectedDxDy(mTmpPosition);
        dx = mTmpPosition[0];
        dy = mTmpPosition[1];
    }
    // Call the onDraw method of Callback
    mCallback.onDraw(c, parent, mSelected,
            mRecoverAnimations, mActionState, dx, dy);
}
Copy the code

[Callback#onDraw]

void onDraw(Canvas c, RecyclerView parent, ViewHolder selected,
        List<ItemTouchHelper.RecoverAnimation> recoverAnimationList,
        int actionState, float dX, float dY) {
    final int recoverAnimSize = recoverAnimationList.size();
    for (int i = 0; i < recoverAnimSize; i++) {
        // Update the view offset in the animation if there is a reply animation
        final ItemTouchHelper.RecoverAnimation anim = recoverAnimationList.get(i);
        anim.update();
        final int count = c.save();
        onChildDraw(c, parent, anim.mViewHolder, anim.mX, anim.mY, anim.mActionState,
                false);
        c.restoreToCount(count);
    }
    if(selected ! =null) {
        Save the current state of the canvas
        final int count = c.save();
        // The onDraw method of ItemTouchUIUtilImpl will be called
        onChildDraw(c, parent, selected, dX, dY, actionState, true);
        // Restore the canvas to its original statec.restoreToCount(count); }}Copy the code

The key logic is in ItemTouchUIUtilImpl’s onDraw method: [ItemTouchUIUtilImpl#onDraw]

public void onDraw(Canvas c, RecyclerView recyclerView, View view, float dX, float dY,
        int actionState, boolean isCurrentlyActive) {
    // When API>=21, calculate the maximum elevation value set to the view so that it is at the top level
    if (Build.VERSION.SDK_INT >= 21) {
        if (isCurrentlyActive) {
            Object originalElevation = view.getTag(R.id.item_touch_helper_previous_elevation);
            if (originalElevation == null) {
                originalElevation = ViewCompat.getElevation(view);
                float newElevation = 1f+ findMaxElevation(recyclerView, view); ViewCompat.setElevation(view, newElevation); view.setTag(R.id.item_touch_helper_previous_elevation, originalElevation); }}}// Update the view offset
    view.setTranslationX(dX);
    view.setTranslationY(dY);
}
Copy the code

It can be seen that swipe and drag are achieved by using ItemTouchHelper to listen to RecyclerView redraw and constantly update the view’s displacement coordinates.

The logic in the onDrawOver method is similar to that in onDraw, except that with the cleanup judgment on mRecoverAnimations, the onDrawOver method of ItemTouchUIUtilImpl is called back, but the method is an empty implementation.

DRAG triggers swapping and scrolling

In the above analysis an OnItemTouchListener onTouchEvent ACTION_MOVE will determine whether triggering RecyclerView scroll: [OnItemTouchListener# onTouchEvent]

public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent event) {
    mGestureDetector.onTouchEvent(event);
    / /...
    switch (action) {
        case MotionEvent.ACTION_MOVE: {
            // Find the index of the active pointer and fetch its position
            if (activePointerIndex >= 0) {
                // Update the sliding offset
                updateDxDy(event, mSelectedFlags, activePointerIndex);
                // If the item is in drag, the onMove callback is triggered
                moveIfNecessary(viewHolder);
                // mScrollRunnable is used to handle LayoutManager scrolling when the user drags an item beyond the edge
                mRecyclerView.removeCallbacks(mScrollRunnable);
                mScrollRunnable.run();
                // Trigger RecyclerView redraw
                mRecyclerView.invalidate();
            }
            break;
        }
        / /...
    }
    / /...
}
Copy the code

[ItemTouchHelper#moveIfNecessary]

void moveIfNecessary(ViewHolder viewHolder) {
    if (mRecyclerView.isLayoutRequested()) {
        return;
    }
    if(mActionState ! = ACTION_STATE_DRAG) {return;
    }

    final float threshold = mCallback.getMoveThreshold(viewHolder);
    final int x = (int) (mSelectedStartX + mDx);
    final int y = (int) (mSelectedStartY + mDy);
    // Determine whether the drag distance threshold reaches half the width or height of the view
    if (Math.abs(y - viewHolder.itemView.getTop()) < viewHolder.itemView.getHeight() * threshold
            && Math.abs(x - viewHolder.itemView.getLeft())
            < viewHolder.itemView.getWidth() * threshold) {
        return;
    }
    // Find all other children that overlap with the selected view and calculate the distance between them
    List<ViewHolder> swapTargets = findSwapTargets(viewHolder);
    if (swapTargets.size() == 0) {
        return;
    }
    // may swap.
    // Find a ViewHolder that can be swapped. For example, drag vertically. If you drag up, select the upper edge of the view
    // If it is smaller than the upper boundary of the target view, drag it down to select the lower boundary of the view and the target view as the threshold value.
    // If there are multiple views, use the one with the largest difference as the target view.
    ViewHolder target = mCallback.chooseDropTarget(viewHolder, swapTargets, x, y);
    if (target == null) {
        // If no ViewHolder is found, the collection is cleared and returned
        mSwapTargets.clear();
        mDistances.clear();
        return;
    }
    final int toPosition = target.getAdapterPosition();
    final int fromPosition = viewHolder.getAdapterPosition();
    // Callback. OnMove, where we exchange items in the adapter dataset
    if (mCallback.onMove(mRecyclerView, viewHolder, target)) {
        // If data is exchanged, onMove must return true.
        // The default implementation of onMoved determines whether the object view is beyond the RecyclerView and LayoutManager boundaries.
        / / and then call RecyclerView scrollToPosition method, scroll to the specified index position.
        // keep target visiblemCallback.onMoved(mRecyclerView, viewHolder, fromPosition, target, toPosition, x, y); }}Copy the code

The moveIfNecessary method is the key to drag. In the drag process, determine the boundary between the selected view and other views as the threshold value, as the condition to trigger onMove. And after item exchange is successful, it will judge whether the target View exceeds RecyclerView, and then trigger scrolling.

Back in the ACTION_MOVE case of the onTouchEvent method, after executing moveIfNecessary, execute mscrollRunnable.run () and look at this method:

final Runnable mScrollRunnable = new Runnable() {
    @Override
    public void run(a) {
        // scrollIfNecessary returns true if there is a scroll
        if(mSelected ! =null && scrollIfNecessary()) {
            if(mSelected ! =null) { //it might be lost during scrolling
                moveIfNecessary(mSelected);
            }
            mRecyclerView.removeCallbacks(mScrollRunnable);
            ViewCompat.postOnAnimation(mRecyclerView, this); }}};Copy the code

In scrollIfNecessary, determine whether the selected view boundary exceeds whether RecyclerView and LayoutManager support scrolling in the corresponding direction. If yes, calculate the scrolling offset. And through the Callback interpolateOutOfBoundsScroll calculation difference migration, the last call RecyclerView. ScrollBy trigger a scroll.

Scrolling is possible in both mScrollRunnable and moveIfNecessary. MScrollRunnable recyclerView. scrollBy recyclerView. scrollBy MoveIfNecessary after the exchange of the item, determine target view beyond borders, through RecyclerView. ScrollToPosition rolling method to the target view specified index position.

conclusion

By analyzing the source code of swipe and Drag, ItemTouchHelper is broken down into initial registration binding, event hosting, event blocking handling, Swipe and Drag trigger detection, drag swap and out-of-bounds scrolling. You have a general idea of how ItemTouchHelper works.