Android开发

AndroidX RecyclerView总结-ItemTouchHelper

本文主要是介绍AndroidX RecyclerView总结-ItemTouchHelper,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

概述

RecyclerView不仅实现在有限窗口显示大数据集,还支持对其中的item视图进行Swipe(轻扫)Drag(拖拽)操作,这可以借助ItemTouchHelper辅助类轻松实现。

基本使用

关键代码:

// 1.创建ItemTouchHelper.Callback,实现回调方法
ItemTouchHelper.Callback callback = new ItemTouchHelper.Callback() {
    // 返回允许滑动的方向
    @Override
    public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
        // 返回可滑动方向,通过使用一个int,在各个bit位标记来记录。
        // 这里drag支持上下方向,swipe支持左右方向。
        int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
        int swipeFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
        // 返回设置了标识位的复合int
        return makeMovementFlags(dragFlags, swipeFlags);
    }

    // 允许drag的前提下,当ItemTouchHelper想要将拖动的项目从其旧位置移动到新位置时调用
    @Override
    public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
        // 获取被拖拽item和目标item的适配器索引(适配器索引是该item对应数据集的索引,getLayoutPosition是当前布局的位置)
        int from = viewHolder.getAdapterPosition();
        int to = target.getAdapterPosition();
        // 交换数据集的数据
        Collections.swap(data, from, to);
        // 通知Adapter更新
        adapter.notifyItemMoved(from, to);
        // 返回true表示item移到了目标位置
        return true;
    }

    // 允许swipe的前提下,当用户滑动ViewHolder触发临界时调用
    @Override
    public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
        // 获取滑动的item对应的适配器索引
        int pos = viewHolder.getAdapterPosition();
        // 从数据集移除数据
        data.remove(pos);
        // 通知Adapter更新
        adapter.notifyItemRemoved(pos);
    }
};
// 2.传入ItemTouchHelper.Callback
ItemTouchHelper touchHelper = new ItemTouchHelper(callback);
// 3.将touchHelper和recyclerView绑定
touchHelper.attachToRecyclerView(recyclerView);
复制代码

以上代码三个步骤就可以实现swipe和drag,效果如图:

swipe示例

drag示例

关键思考

我们知道RecyclerView作为ViewGroup,有自己的滑动事件处理,那么ItemTouchHelper是如何进行swipe和drag,而不产生冲突。

ItemTouchHelper如何通过attachToRecyclerView方法附加RecyclerView,就能将触摸事件托管到自己身上执行。

ItemTouchHelper.Callback接口中的onMove和onSwiped是在什么时机回调。

我们注意到上面图示中,Drag操作拖拽item到达边界时,RecyclerView会跟着滚动起来,这是如何调度的。

带着这些问题进入源码,看看ItemTouchHelper大致实现机制,就能知道答案。

源码探究

文中源码基于 'androidx.recyclerview:recyclerview:1.1.0'

ItemTouchHelper绑定

首先看attachToRecyclerView方法: [ItemTouchHelper#attachToRecyclerView]

public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
    if (mRecyclerView == recyclerView) {
        return; // nothing to do
    }
    // 若绑定过其他RecyclerView,则与旧的解除绑定和清理数据
    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);
        // 设置回调
        setupCallbacks();
    }
}
复制代码

关键在setupCallbacks方法中: [ItemTouchHelper#setupCallbacks]

private void setupCallbacks() {
    ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());
    mSlop = vc.getScaledTouchSlop();
    // 添加到mRecyclerView的mItemDecorations集合中
    mRecyclerView.addItemDecoration(this);
    // 添加到mRecyclerView的mOnItemTouchListeners集合中
    mRecyclerView.addOnItemTouchListener(mOnItemTouchListener);
    // 添加到mRecyclerView的mOnChildAttachStateListeners集合中
    mRecyclerView.addOnChildAttachStateChangeListener(this);
    // 创建GestureDetector
    startGestureDetection();
}
复制代码

该方法中进行各类回调监听注册,这些回调监听是实现swipe和drag的关键。

ItemTouchHelper继承ItemDecoration,ItemDecoration作用是装饰item,通常用来绘制分割线。ItemTouchHelper借助其实现item跟随手指移动。

mOnItemTouchListener注册给RecyclerView后,RecyclerView会将事件回调给它,ItemTouchHelper从而能够拦截事件派发自行处理。

ItemTouchHelper实现OnChildAttachStateChangeListener接口,在该接口的onChildViewDetachedFromWindow方法中处理视图detached时进行释放动作或结束动画、清理视图引用。

startGestureDetection方法中会创建GestureDetector,用于监听触摸事件。当触发onLongPress长按时,判断是否开始drag。

RecyclerView触摸事件托管

接下来看看RecyclerView如何把触摸事件托管给ItemTouchHelper。

onInterceptTouchEvent

[RecyclerView#onInterceptTouchEvent]

public boolean onInterceptTouchEvent(MotionEvent e) {
    // ···
    mInterceptingOnItemTouchListener = null;
    // 判断是否有OnItemTouchListener拦截事件
    if (findInterceptingOnItemTouchListener(e)) {
        // 若有拦截则取消滚动
        cancelScroll();
        return true;
    }
    // RecyclerView的onInterceptTouchEvent逻辑 ···
}

private boolean findInterceptingOnItemTouchListener(MotionEvent e) {
    int action = e.getAction();
    final int listenerCount = mOnItemTouchListeners.size();
    // 依次将事件派发给mOnItemTouchListeners保存的listener
    for (int i = 0; i < listenerCount; i++) {
        final OnItemTouchListener listener = mOnItemTouchListeners.get(i);
        if (listener.onInterceptTouchEvent(this, e) && action != MotionEvent.ACTION_CANCEL) {
            // 若有listener拦截事件并且当前事件不是CANCEL,则用mInterceptingOnItemTouchListener保存该listener,结束遍历
            mInterceptingOnItemTouchListener = listener;
            return true;
        }
    }
    return false;
}
复制代码

RecyclerView在onInterceptTouchEvent方法中处理RecyclerView自身事件拦截逻辑前,会先派发给OnItemTouchListener集合,若有OnItemTouchListener处理则RecyclerView自身不再处理。

onTouchEvent

[RecyclerView#onTouchEvent]

public boolean onTouchEvent(MotionEvent e) {
    // ···
    // 判断是否有OnItemTouchListener消费事件
    if (dispatchToOnItemTouchListeners(e)) {
        // 若有消费事件则取消滚动
        cancelScroll();
        return true;
    }
    // RecyclerView的onTouchEvent逻辑 ···
}

private boolean dispatchToOnItemTouchListeners(MotionEvent e) {
    if (mInterceptingOnItemTouchListener == null) {
        // 若在onInterceptTouchEvent时没有OnItemTouchListener拦截事件,那么这里
        // 还会将事件派发给OnItemTouchListener,但是会过滤掉DOWN事件,避免重复派发。
        if (e.getAction() == MotionEvent.ACTION_DOWN) {
            return false;
        }
        return findInterceptingOnItemTouchListener(e);
    } else {
        // 若有拦截事件的OnItemTouchListener,则直接交给它的onTouchEvent方法
        mInterceptingOnItemTouchListener.onTouchEvent(this, e);
        final int action = e.getAction();
        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
            // 若是事件序列结束,则清空mInterceptingOnItemTouchListener
            mInterceptingOnItemTouchListener = null;
        }
        return true;
    }
}
复制代码

RecyclerView在onTouchEvent中处理自身的逻辑前,会先将事件派发给OnItemTouchListener,若有消费事件,则RecyclerView自身不再处理。

requestDisallowInterceptTouchEvent

RecyclerView重写了requestDisallowInterceptTouchEvent方法,在其中也会回调OnItemTouchListener: [RecyclerView#requestDisallowInterceptTouchEvent]

public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    final int listenerCount = mOnItemTouchListeners.size();
    // 依次调用OnItemTouchListeners集合中listener
    for (int i = 0; i < listenerCount; i++) {
        final OnItemTouchListener listener = mOnItemTouchListeners.get(i);
        // 在ItemTouchHelper的listener中,若传入不希望拦截事件,那么ItemTouchHelper会释放移动
        listener.onRequestDisallowInterceptTouchEvent(disallowIntercept);
    }
    super.requestDisallowInterceptTouchEvent(disallowIntercept);
}
复制代码

ItemTouchHelper拦截事件处理

RecyclerView在收到触摸事件时,会优先将事件交给OnItemTouchListener,若有事件被消费,则RecyclerView自身不再消费。ItemTouchHelper便是通过OnItemTouchListener来接收事件,触发SWIPE或DRAG。

onInterceptTouchEvent

看看ItemTouchHelper的mOnItemTouchListener实现的对应事件拦截方法。

[OnItemTouchListener#onInterceptTouchEvent]

public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,
        @NonNull MotionEvent event) {
    // GestureDetector监听输入的事件
    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) {
        // 若该event是一个事件序列的开始,则记录触摸点ID和初始坐标位置
        mActivePointerId = event.getPointerId(0);
        mInitialTouchX = event.getX();
        mInitialTouchY = event.getY();
        obtainVelocityTracker();
        // mSelected成员记录当前选中的ViewHolder,默认为null
        if (mSelected == null) {
            // 从mRecoverAnimations集合中根据event位置查找对应item的回复动画(回复动画是手指释放时,view自动移动到指定位置的动画)
            final RecoverAnimation animation = findAnimation(event);
            if (animation != null) {
                // animation的mX、mY记录当前view的偏移位置
                mInitialTouchX -= animation.mX;
                mInitialTouchY -= animation.mY;
                // 结束动画
                endRecoverAnimation(animation.mViewHolder, true);
                // mPendingCleanup缓存detached后待清除状态的view
                if (mPendingCleanup.remove(animation.mViewHolder.itemView)) {
                    // 恢复view的Elevation属性,将TranslationX、TranslationY重置为0
                    mCallback.clearView(mRecyclerView, animation.mViewHolder);
                }
                // 将该item重新作为选中的ViewHolder
                select(animation.mViewHolder, animation.mActionState);
                // 更新mDx、mDy(mDx、mDy记录已经滑动的偏移量)
                updateDxDy(event, mSelectedFlags, 0);
            }
        }
    } else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
        // 若该event是一个事件序列的结束,则清空触摸点ID和释放选中ViewHolder
        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
        // 这个case中的event就是事件序列的中间事件,若触摸点ID存在(前面ACTION_DOWN时会保存)
        // 获取触摸点索引
        final int index = event.findPointerIndex(mActivePointerId);
        if (DEBUG) {
            Log.d(TAG, "pointer index " + index);
        }
        if (index >= 0) {
            // 检查是否符合swipe触发条件,若符合则会调用select方法进行选中处理
            checkSelectForSwipe(action, event, index);
        }
    }
    if (mVelocityTracker != null) {
        // 监听event,用于加速度计算
        mVelocityTracker.addMovement(event);
    }
    // 若有选中的ViewHolder,则返回true表示拦截
    return mSelected != null;
}
复制代码

该方法主要逻辑就是在ACTION_DOWN时记录初始触摸位置,ACTION_MOVE、ACTION_POINTER_DOWN、ACTION_POINTER_UP时判断是否符合swipe触发条件,ACTION_UP、ACTION_CANCEL时释放。

其中调用checkSelectForSwipe方法检查swipe条件,是触发swipe的关键方法。还有注意到有多个地方会调用select方法,该方法也是关键方法,会处理item选中和释放的操作。

onTouchEvent

[OnItemTouchListener#onTouchEvent]

public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent event) {
    // 手势监听
    mGestureDetector.onTouchEvent(event);
    if (DEBUG) {
        Log.d(TAG,
                "on touch: x:" + mInitialTouchX + ",y:" + mInitialTouchY + ", :" + event);
    }
    // 加速度监听
    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) {
        // 检查是否触发swipe
        checkSelectForSwipe(action, event, activePointerIndex);
    }
    ViewHolder viewHolder = mSelected;
    if (viewHolder == null) {
        // 若不满足swipe条件则返回
        return;
    }
    // 执行到这里,说明有在swipe或drag的item
    switch (action) {
        case MotionEvent.ACTION_MOVE: {
            // Find the index of the active pointer and fetch its position
            if (activePointerIndex >= 0) {
                // 更新滑动偏移量
                updateDxDy(event, mSelectedFlags, activePointerIndex);
                // 如果当前处于drag状态,则会判断是否达到和某个item交换的条件,触发onMove回调
                moveIfNecessary(viewHolder);
                // mScrollRunnable用于处理当用户拖动item超出边缘时触发LayoutManager滚动
                mRecyclerView.removeCallbacks(mScrollRunnable);
                mScrollRunnable.run();
                // 触发RecyclerView重绘
                mRecyclerView.invalidate();
            }
            break;
        }
        case MotionEvent.ACTION_CANCEL:
            // 若是CANCEL事件(手指划出view范围),则清除加速度计算
            if (mVelocityTracker != null) {
                mVelocityTracker.clear();
            }
            // fall through
        case MotionEvent.ACTION_UP:
            // ACTION_CANCEL和ACTION_UP都释放选中
            select(null, ACTION_STATE_IDLE);
            mActivePointerId = ACTIVE_POINTER_ID_NONE;
            break;
        case MotionEvent.ACTION_POINTER_UP: {
            // 多指触摸情况下一个手指抬起,更新触摸点ID和滑动偏移量
            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;
        }
    }
}
复制代码

该方法中也会通过checkSelectForSwipe方法判断是否触发swipe。注意ACTION_MOVE case中是手指拖动滑动的关键代码。

可以看到ItemTouchHelper也会将收到的事件传给GestureDetector和VelocityTracker。其中GestureDetector用于监听长按事件,VelocityTracker用于计算加速度,当手指全部抬起时判断是否swipe移出item。

SWIPE和DRAG触发判定

动作状态

[ItemTouchHelper]

// 空闲状态,当前没有用户事件或事件尚未触发swipe或drag
public static final int ACTION_STATE_IDLE = 0;
// 目前正在swipe视图
public static final int ACTION_STATE_SWIPE = 1;
// 目前正在drag视图
public static final int ACTION_STATE_DRAG = 2;

private int mActionState = ACTION_STATE_IDLE;
复制代码

ItemTouchHelper的mActionState成员用于记录当前状态。

SWIPE和DRAG不能同时触发,接下来分别看下两种操作的触发条件。

SWIPE触发

ItemTouchHelper接收到开始滑动事件时调用checkSelectForSwipe检查SWIPE: [ItemTouchHelper#checkSelectForSwipe]

void checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) {
    if (mSelected != null || action != MotionEvent.ACTION_MOVE
            || mActionState == ACTION_STATE_DRAG || !mCallback.isItemViewSwipeEnabled()) {
        // mCallback即我们创建的ItemTouchHelper.Callback,可重写isItemViewSwipeEnabled方法禁用Swipe。
        // 若已有选中的ViewHolder或当前事件非ACTION_MOVE或当前已处于DRAG中或禁用了Swipe,则返回。
        return;
    }
    if (mRecyclerView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) {
        // RecyclerView自身已在滚动中,则返回
        return;
    }
    // 查找可滑动的ViewHolder
    final ViewHolder vh = findSwipedView(motionEvent);
    if (vh == null) {
        // 没有符合的ViewHolder则返回
        return;
    }
    // 获取支持滑动的方向,将调用ItemTouchHelper.Callback的getMovementFlags回调方法返回我们设置的方向
    final int movementFlags = mCallback.getAbsoluteMovementFlags(mRecyclerView, vh);

    // 取出swipe对应标识位
    final int swipeFlags = (movementFlags & ACTION_MODE_SWIPE_MASK)
            >> (DIRECTION_FLAG_COUNT * ACTION_STATE_SWIPE);

    // 判断是否有支持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
    // 计算滑动距离
    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.
    // 取绝对值
    final float absDx = Math.abs(dx);
    final float absDy = Math.abs(dy);

    // 判断距离是否达到最小滑动距离
    if (absDx < mSlop && absDy < mSlop) {
        return;
    }
    // 判断滑动方向,水平和垂直滑动偏移,哪个方向大,就属于哪个方向
    if (absDx > absDy) {
        // 手指向左划,判断是否支持左滑
        if (dx < 0 && (swipeFlags & LEFT) == 0) {
            return;
        }
        // 判断是否支持右划
        if (dx > 0 && (swipeFlags & RIGHT) == 0) {
            return;
        }
    } else {
        // 手指向上划,判断是否支持上滑
        if (dy < 0 && (swipeFlags & UP) == 0) {
            return;
        }
        // 判断是否支持下滑
        if (dy > 0 && (swipeFlags & DOWN) == 0) {
            return;
        }
    }
    // 执行到这里说明找到ViewHolder且满足SWIPE条件。
    // 新开始SWIPE,重置变量
    mDx = mDy = 0f;
    mActivePointerId = motionEvent.getPointerId(0);
    // 传入ViewHolder和SWIPE对应状态
    select(vh, ACTION_STATE_SWIPE);
}
复制代码

该方法中提供findSwipedView方法查找一个ViewHolder进行SWIPE,看看这个方法: [ItemTouchHelper#findSwipedView]

private ViewHolder findSwipedView(MotionEvent motionEvent) {
    // 获取LayoutManager
    final RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager();
    if (mActivePointerId == ACTIVE_POINTER_ID_NONE) {
        return null;
    }
    final int pointerIndex = motionEvent.findPointerIndex(mActivePointerId);
    // 计算滑动偏移量
    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);

    // 判断滑动偏移量是否达到最小滑动距离
    if (absDx < mSlop && absDy < mSlop) {
        return null;
    }
    if (absDx > absDy && lm.canScrollHorizontally()) {
        // 若偏向水平滑动且当前LayoutManager也可水平滑动,为避免冲突,则不能SWIPE
        // (例如设置了水平排布的LinearLayoutManager,则不能进行水平方向的SWIPE操作)
        return null;
    } else if (absDy > absDx && lm.canScrollVertically()) {
        // 同上,若垂直滑动且LayoutManager也可垂直滑动,不能SWIPE
        return null;
    }
    // 根据event位置查找view
    View child = findChildView(motionEvent);
    if (child == null) {
        return null;
    }
    // 返回view对应的ViewHolder
    return mRecyclerView.getChildViewHolder(child);
}
复制代码

接着看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;
        // 若存在选中的ViewHolder,则判断触摸点位置是否落于该view范围中
        if (hitTest(selectedView, x, y, mSelectedStartX + mDx, mSelectedStartY + mDy)) {
            return selectedView;
        }
    }
    for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) {
        // 若存在回复动画,依次判断触摸点位置是否落于动画执行的view范围中
        final RecoverAnimation anim = mRecoverAnimations.get(i);
        final View view = anim.mViewHolder.itemView;
        if (hitTest(view, x, y, anim.mX, anim.mY)) {
            return view;
        }
    }
    // 从上往下遍历RecyclerView的子view,获取触摸点位置落于的view
    return mRecyclerView.findChildViewUnder(x, y);
}
复制代码

简单总结触发SWIPE的条件:首先计算滑动距离和滑动方向,需要满足最小滑动距离且不能和LayoutManager的滑动方向冲突,根据触摸点位置获取对应的view的ViewHolder。接着判断我们通过ItemTouchHelper.Callback设置的标识位,是否允许swipe和当前方向swipe。若都满足,则调用select方法,传入ViewHolder和ACTION_STATE_SWIPE,进行选中判断操作。

DRAG触发

DRAG是在长按时才会触发,ItemTouchHelper通过GestureDetector监听MotionEvent,长按时触发onLongPress回调: [ItemTouchHelperGestureListener#onLongPress]

public void onLongPress(MotionEvent e) {
    if (!mShouldReactToLongPress) {
        return;
    }
    // 根据触摸点位置获取对应的view
    View child = findChildView(e);
    if (child != null) {
        // 获取view对应的ViewHolder
        ViewHolder vh = mRecyclerView.getChildViewHolder(child);
        if (vh != null) {
            // 判断是否支持drag,将触发ItemTouchHelper.Callback的getMovementFlags,
            // 若有设置drag方向则会返回true
            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);
                // 保存初始触摸位置坐标
                mInitialTouchX = x;
                mInitialTouchY = y;
                mDx = mDy = 0f;
                if (DEBUG) {
                    Log.d(TAG,
                            "onlong press: x:" + mInitialTouchX + ",y:" + mInitialTouchY);
                }
                // 判断是否允许drag,默认返回true
                if (mCallback.isLongPressDragEnabled()) {
                    // 进行选中操作
                    select(vh, ACTION_STATE_DRAG);
                }
            }
        }
    }
}
复制代码

可以看到在长按回调中,判断若支持drag,则也调用select方法,传入长按的ViewHolder和ACTION_STATE_DRAG。

select选中和释放

在前文中看到,当触发SWIPE或DRAG时,和ACTION_CANCEL、ACTION_UP时,均会调用select。

时机 参数selected 参数actionState
触发SWIPE 按住的ViewHolder ACTION_STATE_SWIPE
触发DRAG 按住的ViewHolder ACTION_STATE_DRAG
抬起释放 null ACTION_STATE_IDLE

select方法中包含选中和释放的逻辑,先看选中部分: [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;
    if (actionState == ACTION_STATE_DRAG) {
        // 若是触发DRAG
        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.
        // 保存选中的view的引用
        mOverdrawChild = selected.itemView;
        // 如果是小于21的版本,则会设置ViewGroup采用自定义遍历child规则
        addChildDrawingOrderCallback();
    }
    int actionStateMask = (1 << (DIRECTION_FLAG_COUNT + DIRECTION_FLAG_COUNT * actionState))
            - 1;
    boolean preventLayout = false;
    
    // 如果之前有选中的ViewHolder,则要对其释放
    if (mSelected != null) {
        // 省略释放的逻辑 ···
    }
    // 如果当前是释放,则selected为null,否则为将选中的ViewHolder
    if (selected != null) {
        mSelectedFlags =
                (mCallback.getAbsoluteMovementFlags(mRecyclerView, selected) & actionStateMask)
                        >> (mActionState * DIRECTION_FLAG_COUNT);
        // 记录将选中view的左上角
        mSelectedStartX = selected.itemView.getLeft();
        mSelectedStartY = selected.itemView.getTop();
        // mSelected赋值为将选中的ViewHolder
        mSelected = selected;

        if (actionState == ACTION_STATE_DRAG) {
            // 若是触发DRAG,则给用户一个触觉反馈
            mSelected.itemView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
        }
    }
    final ViewParent rvParent = mRecyclerView.getParent();
    if (rvParent != null) {
        // 选中时请求父布局不拦截事件,释放时反之
        rvParent.requestDisallowInterceptTouchEvent(mSelected != null);
    }
    if (!preventLayout) {
        // 使RecyclerView在下一次布局时运行SimpleAnimation
        mRecyclerView.getLayoutManager().requestSimpleAnimationsInNextLayout();
    }
    // 回调给ItemTouchHelper.Callback的onSelectedChanged
    mCallback.onSelectedChanged(mSelected, mActionState);
    // 触发RecyclerView重绘
    mRecyclerView.invalidate();
}
复制代码

可以看出当触发swipe或drag时,主要逻辑是保存选中view的左上角坐标和ViewHolder引用

如果是drag的话,则还会额外保存选中的view的引用和设置ViewGroup遍历child的自定义顺序(API<21) ,这样做的目的是为了在拖动view时,使这个view保持在其他view上面。我们知道ViewGroup在绘制child时,默认是按照mChildren数组的顺序遍历,为了使指定的child位于上层,在API<21可以通过设置自定义遍历规则,让指定view在最后绘制。在API>=21可以通过设置elevation使之位于上层。

接着看释放时的逻辑: [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;
    // 省略ACTION_STATE_DRAG部分 ···
    int actionStateMask = (1 << (DIRECTION_FLAG_COUNT + DIRECTION_FLAG_COUNT * actionState))
            - 1;
    boolean preventLayout = false;
    
    // 如果之前有选中的ViewHolder,则要对其释放
    if (mSelected != null) {
        final ViewHolder prevSelected = mSelected;
        // 判断之前选中view是否还依附于父布局
        if (prevSelected.itemView.getParent() != null) {
            // 若之前不是drag操作,则获取滑动方向。
            // 调用Callback.getMovementFlags获取我们设置的方向,然后会判断滑动加速度
            // 是否超过Callback.getSwipeEscapeVelocity设置的阈值或滑动距离是否超出
            // Callback.getSwipeThreshold设置的阈值。若超过,则view将被滑走,swipeDir是滑走的那个方向。
            // 否则swipeDir是0。
            final int swipeDir = prevActionState == ACTION_STATE_DRAG ? 0
                    : swipeIfNecessary(prevSelected);
            releaseVelocityTracker();
            // find where we should animate to
            final float targetTranslateX, targetTranslateY;
            int animationType;
            // 根据滑动方向计算偏移距离
            switch (swipeDir) {
                case LEFT:
                case RIGHT:
                case START:
                case END:
                    // 水平方向,Y轴不变,X轴上view要移动到RecyclerView边界外
                    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;
            }
            // 记录动画类型
            if (prevActionState == ACTION_STATE_DRAG) {
                animationType = ANIMATION_TYPE_DRAG;
            } else if (swipeDir > 0) {
                // ItemTouchHelper将完全滑走视为SWIPE成功
                animationType = ANIMATION_TYPE_SWIPE_SUCCESS;
            } else {
                // 回到原始位置视为SWIPE取消
                animationType = ANIMATION_TYPE_SWIPE_CANCEL;
            }
            // 计算选中view当前的X、Y轴偏移量,保存在mTmpPosition数组中
            getSelectedDxDy(mTmpPosition);
            final float currentTranslateX = mTmpPosition[0];
            final float currentTranslateY = mTmpPosition[1];
            // 创建RecoverAnimation,内部封装属性动画操作。将view从currentTranslate移动到targetTranslate。
            final RecoverAnimation rv = new RecoverAnimation(prevSelected, animationType,
                    prevActionState, currentTranslateX, currentTranslateY,
                    targetTranslateX, targetTranslateY) {
                @Override
                public void onAnimationEnd(Animator animation) {
                    // 动画完成
                    super.onAnimationEnd(animation);
                    // 若动画期间,用户又触摸该view,mOverridden会标记为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 {
                        // 滑出动画结束
                        // wait until remove animation is complete.
                        // 将view保存进mPendingCleanup集合待后续清除
                        mPendingCleanup.add(prevSelected.itemView);
                        mIsPendingCleanup = true;
                        if (swipeDir > 0) {
                            // 针对swipe滑走view情况
                            // Animation might be ended by other animators during a layout.
                            // We defer callback to avoid editing adapter during a layout.
                            // 发送主线程,待没有动画执行时,回调Callback.onSwiped。
                            // 在该回调中,我们将对应的item从适配器数据集中移除。
                            postDispatchSwipe(this, swipeDir);
                        }
                    }
                    // removed from the list after it is drawn for the last time
                    if (mOverdrawChild == prevSelected.itemView) {
                        // 针对drag情况,触发drag时mOverdrawChild赋值为选中view,
                        // 这里需要清理引用和取消ViewGroup自定义遍历child规则。
                        removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);
                    }
                }
            };
            final long duration = mCallback.getAnimationDuration(mRecyclerView, animationType,
                    targetTranslateX - currentTranslateX, targetTranslateY - currentTranslateY);
            rv.setDuration(duration);
            // 将动画保存进mRecoverAnimations集合
            mRecoverAnimations.add(rv);
            // 启动动画
            rv.start();
            // preventLayout标记为true,RecyclerView将不执行SimpleAnimation
            preventLayout = true;
        } else {
            removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);
            mCallback.clearView(mRecyclerView, prevSelected);
        }
        // mSelected置为null
        mSelected = null;
    }
    if (selected != null) {
        // 省略选中时的逻辑 ···
    }
    final ViewParent rvParent = mRecyclerView.getParent();
    if (rvParent != null) {
        // 选中时请求父布局不拦截事件,释放时反之
        rvParent.requestDisallowInterceptTouchEvent(mSelected != null);
    }
    if (!preventLayout) {
        // 使RecyclerView在下一次布局时运行SimpleAnimation
        mRecyclerView.getLayoutManager().requestSimpleAnimationsInNextLayout();
    }
    // 回调给ItemTouchHelper.Callback的onSelectedChanged
    mCallback.onSelectedChanged(mSelected, mActionState);
    // 触发RecyclerView重绘
    mRecyclerView.invalidate();
}
复制代码

可以看出释放时,会判断swipe还是drag和是否将view滑走,计算translate创建RecoverAnimation执行属性动画。动画完成后,若是swipe滑走,则发送到主线程待没有任何动画时回调Callback.onSwiped。若是drag,则将之前设置的ViewGroup自定义遍历child规则取消。

SWIPE滑动和DRAG拖动

当select选中ViewHolder和onTouchEvent处理ACTION_MOVE时都会触发RecyclerView.invalidate重绘。

RecyclerView重写了draw和onDraw方法,看看这两个方法: [RecyclerView#onDraw、RecyclerView#draw]

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);
    }
    
    // ···
}
复制代码

ItemTouchHelper继承ItemDecoration,在和RecyclerView绑定时添加进mItemDecorations集合,因此当重绘时,会先后回调ItemTouchHelper的onDraw和onDrawOver方法。

先进入onDraw方法: [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) {
        // 计算偏移量
        getSelectedDxDy(mTmpPosition);
        dx = mTmpPosition[0];
        dy = mTmpPosition[1];
    }
    // 回调Callback的onDraw方法
    mCallback.onDraw(c, parent, mSelected,
            mRecoverAnimations, mActionState, dx, dy);
}
复制代码

[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++) {
        // 若存在回复动画,则更新动画中的view的偏移量
        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) {
        // 保存画布当前状态
        final int count = c.save();
        // 将调用ItemTouchUIUtilImpl的onDraw方法
        onChildDraw(c, parent, selected, dX, dY, actionState, true);
        // 恢复画布到原来状态
        c.restoreToCount(count);
    }
}
复制代码

关键逻辑在ItemTouchUIUtilImpl的onDraw方法中: [ItemTouchUIUtilImpl#onDraw]

public void onDraw(Canvas c, RecyclerView recyclerView, View view, float dX, float dY,
        int actionState, boolean isCurrentlyActive) {
    // 当API>=21时,计算最大的elevation值设置给view,使它位于最上层
    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);
            }
        }
    }

    // 更新view的偏移量
    view.setTranslationX(dX);
    view.setTranslationY(dY);
}
复制代码

可以看出swipe和drag的滑动和拖动,是通过ItemTouchHelper监听RecyclerView重绘,不断更新view的位移坐标来实现的。

onDrawOver方法中的逻辑和onDraw类似,区别是多了对mRecoverAnimations的清理判断工作,会回调ItemTouchUIUtilImpl的onDrawOver方法,但是该方法是空实现。

DRAG触发交换和滚动

前文分析过OnItemTouchListener.onTouchEvent中在ACTION_MOVE会判断是否触发RecyclerView滚动: [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) {
                // 更新滑动偏移量
                updateDxDy(event, mSelectedFlags, activePointerIndex);
                // 如果当前处于drag状态,则会判断是否达到和某个item交换的条件,触发onMove回调
                moveIfNecessary(viewHolder);
                // mScrollRunnable用于处理当用户拖动item超出边缘时触发LayoutManager滚动
                mRecyclerView.removeCallbacks(mScrollRunnable);
                mScrollRunnable.run();
                // 触发RecyclerView重绘
                mRecyclerView.invalidate();
            }
            break;
        }
        // ···
    }
    // ···
}
复制代码

进入moveIfNecessary方法: [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);
    // 判断拖动距离的阈值是否达到view宽或高的一半
    if (Math.abs(y - viewHolder.itemView.getTop()) < viewHolder.itemView.getHeight() * threshold
            && Math.abs(x - viewHolder.itemView.getLeft())
            < viewHolder.itemView.getWidth() * threshold) {
        return;
    }
    // 查找所有和选中view有交叉重叠的其他child,并预计算之间的距离
    List<ViewHolder> swapTargets = findSwapTargets(viewHolder);
    if (swapTargets.size() == 0) {
        return;
    }
    // may swap.
    // 找到一个可交换的ViewHolder。(以垂直拖动为例,若往上拖拽,则比较选中view的上边界
    // 是否小于目标view的上边界,往下拖拽则以下选中view和目标view的下边界作为临界值。
    // 如果有多个view满足,以差值最大的作为目标view)
    ViewHolder target = mCallback.chooseDropTarget(viewHolder, swapTargets, x, y);
    if (target == null) {
        // 若没有找到可交换的ViewHolder,则清空集合返回
        mSwapTargets.clear();
        mDistances.clear();
        return;
    }
    final int toPosition = target.getAdapterPosition();
    final int fromPosition = viewHolder.getAdapterPosition();
    // 回调Callback.onMove,我们在此方法中进行适配器数据集中的item交换
    if (mCallback.onMove(mRecyclerView, viewHolder, target)) {
        // 若进行了数据交换,onMove需要返回true。
        // onMoved的默认实现中会判断目标view的边界是否超出RecyclerView和LayoutManager是否支持对应方向滚动,
        // 进而调用RecyclerView.scrollToPosition方法,滚动到指定索引位置。
        // keep target visible
        mCallback.onMoved(mRecyclerView, viewHolder, fromPosition,
                target, toPosition, x, y);
    }
}
复制代码

可见moveIfNecessary方法是drag拖动的关键,在拖动过程中判断选中view和其他view的边界作为临界值,作为触发onMove的条件。并且在item交换成功后,还会判断目标view是否超出RecyclerView,继而触发滚动。

回到onTouchEvent方法的ACTION_MOVE case中,在执行完moveIfNecessary后,接着执行mScrollRunnable.run(),看看这个方法:

final Runnable mScrollRunnable = new Runnable() {
    @Override
    public void run() {
        // scrollIfNecessary若有滚动则返回true
        if (mSelected != null && scrollIfNecessary()) {
            if (mSelected != null) { //it might be lost during scrolling
                moveIfNecessary(mSelected);
            }
            mRecyclerView.removeCallbacks(mScrollRunnable);
            ViewCompat.postOnAnimation(mRecyclerView, this);
        }
    }
};
复制代码

scrollIfNecessary方法中判断选中view的边界是否超出RecyclerView和LayoutManager是否支持对应方向滚动,若满足则计算滚动偏移量,并通过Callback.interpolateOutOfBoundsScroll计算差值偏移,最后调用RecyclerView.scrollBy触发滚动。

mScrollRunnable和moveIfNecessary中都有可能触发滚动。区别是mScrollRunnable中是当选中view拖拽超出边界时,通过RecyclerView.scrollBy方法滚动一定偏移距离。moveIfNecessary中时当交换item后,判断目标view超出边界,通过RecyclerView.scrollToPosition方法滚动到目标view指定索引位置。

总结

通过对swipe和drag的过程的源码分析,将ItemTouchHelper拆解为初始注册绑定、事件托管、事件拦截处理、SWIPE和DRAG触发判定、选中View的拖动和释放处理、DRAG交换和超出边界滚动等部分,对ItemTouchHelper的实现机制有了大概的了解。

这篇关于AndroidX RecyclerView总结-ItemTouchHelper的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!