ItemTouchHelper的基本使用上次已经介绍了,今天来分析下ItemTouchHelper的源码,我们从attachToRecyclerView方法入手
/*** Attaches the ItemTouchHelper to the provided RecyclerView. If TouchHelper is already* attached to a RecyclerView, it will first detach from the previous one. You can call this* method with {@code null} to detach it from the current RecyclerView.** @param recyclerView The RecyclerView instance to which you want to add this helper or* {@code null} if you want to remove ItemTouchHelper from the current* RecyclerView.*/public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {if (mRecyclerView == recyclerView) {return; // nothing to do}if (mRecyclerView != null) {destroyCallbacks();}mRecyclerView = recyclerView;if (mRecyclerView != 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();}}
可以看到就是将recyclerView赋值给mRecyclerView ,并调用了setupCallbacks方法,来到setupCallbacks方法:
private void setupCallbacks() {ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());mSlop = vc.getScaledTouchSlop();//说明ItemTouchHelper本身继承了ItemDecoration//ItemDecoration我们一般用于画分割线mRecyclerView.addItemDecoration(this);mRecyclerView.addOnItemTouchListener(mOnItemTouchListener);mRecyclerView.addOnChildAttachStateChangeListener(this);initGestureDetector();}
其中初始化了判断移动的阀值mSlop ,然后调用mRecyclerView.addItemDecoration(this),我们看ItemTouchHelper继承ItemDecoration干了什么,发现ItemTouchHelper改写了onDraw和onDrawOver方法
/*** 该方法在recyclerView的draw方法中调用,draw方法会调用onDraw方法** @param c* @param parent* @param state*/@Overridepublic void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {float dx = 0, dy = 0;if (mSelected != null) {getSelectedDxDy(mTmpPosition);dx = mTmpPosition[0];dy = mTmpPosition[1];}mCallback.onDrawOver(c, parent, mSelected,mRecoverAnimations, mActionState, dx, dy);}/*** 该方法在recyclerView的onDraw方法中调用** @param c* @param parent* @param state*/@Overridepublic 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];}mCallback.onDraw(c, parent, mSelected,mRecoverAnimations, mActionState, dx, dy);}
两个方法都差不多,先判断mSelected 是不是null,获取dx,dy,即横向的偏移量和纵向的偏移量;接下来调用mCallback的方法,mCallback就是我们使用的ItemTouchHelper.Callback,下面是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++) {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();onChildDraw(c, parent, selected, dX, dY, actionState, true);c.restoreToCount(count);}}
我们关注onChildDraw方法,我们追踪代码发现,最后调用的是ItemTouchUIUtilImpl中的onDraw和onDrawOver方法,ItemTouchUIUtilImpl是ItemTouchUIUtil的实现类。mSelected 其实就是我们选中的ViewHodler,在接下来我们的分析中会知道,这边先提一下。下面贴一下ItemTouchUIUtilImpl的代码
/*** Package private class to keep implementations. Putting them inside ItemTouchUIUtil makes them* public API, which is not desired in this case.*/
class ItemTouchUIUtilImpl {static class Lollipop extends Honeycomb {@Overridepublic void onDraw(Canvas c, RecyclerView recyclerView, View view,float dX, float dY, int actionState, boolean isCurrentlyActive) {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);}}super.onDraw(c, recyclerView, view, dX, dY, actionState, isCurrentlyActive);}private float findMaxElevation(RecyclerView recyclerView, View itemView) {final int childCount = recyclerView.getChildCount();float max = 0;for (int i = 0; i < childCount; i++) {final View child = recyclerView.getChildAt(i);if (child == itemView) {continue;}final float elevation = ViewCompat.getElevation(child);if (elevation > max) {max = elevation;}}return max;}@Overridepublic void clearView(View view) {final Object tag = view.getTag(R.id.item_touch_helper_previous_elevation);if (tag != null && tag instanceof Float) {ViewCompat.setElevation(view, (Float) tag);}view.setTag(R.id.item_touch_helper_previous_elevation, null);super.clearView(view);}}static class Honeycomb implements ItemTouchUIUtil {@Overridepublic void clearView(View view) {ViewCompat.setTranslationX(view, 0f);ViewCompat.setTranslationY(view, 0f);}@Overridepublic void onSelected(View view) {}@Overridepublic void onDraw(Canvas c, RecyclerView recyclerView, View view,float dX, float dY, int actionState, boolean isCurrentlyActive) {ViewCompat.setTranslationX(view, dX);ViewCompat.setTranslationY(view, dY);}@Overridepublic void onDrawOver(Canvas c, RecyclerView recyclerView,View view, float dX, float dY, int actionState, boolean isCurrentlyActive) {}}static class Gingerbread implements ItemTouchUIUtil {private void draw(Canvas c, RecyclerView parent, View view,float dX, float dY) {c.save();c.translate(dX, dY);parent.drawChild(c, view, 0);c.restore();}@Overridepublic void clearView(View view) {view.setVisibility(View.VISIBLE);}@Overridepublic void onSelected(View view) {view.setVisibility(View.INVISIBLE);}@Overridepublic void onDraw(Canvas c, RecyclerView recyclerView, View view,float dX, float dY, int actionState, boolean isCurrentlyActive) {if (actionState != ItemTouchHelper.ACTION_STATE_DRAG) {draw(c, recyclerView, view, dX, dY);}}@Overridepublic void onDrawOver(Canvas c, RecyclerView recyclerView,View view, float dX, float dY,int actionState, boolean isCurrentlyActive) {if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {draw(c, recyclerView, view, dX, dY);}}}
}
到了这里,我们发现,都是canvas的矩阵变换效果(api28中是设置View的属性),也就是我们拖拽和侧滑,最终的动画效果就是利用的canvas,那么具体我们要执行到哪个ViewHodler上,是在哪里判断的呢?我们继续来到setupCallbacks方法
private void setupCallbacks() {ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());mSlop = vc.getScaledTouchSlop();//说明ItemTouchHelper本事实现了ItemDecoration//ItemDecoration我们一般用于画分割线mRecyclerView.addItemDecoration(this);//addOnItemTouchListener方法做了什么?mRecyclerView.addOnItemTouchListener(mOnItemTouchListener);mRecyclerView.addOnChildAttachStateChangeListener(this);initGestureDetector();}
之前分析了mRecyclerView.addItemDecoration方法,知道了RecyclerView每次onDraw的时候,都会调用ItemTouchHelper.Callback的onDraw方法,我们再分析mRecyclerView.addOnItemTouchListener做了什么?来到RecyclerView的addOnItemTouchListener方法
/*** Add an {@link OnItemTouchListener} to intercept touch events before they are dispatched* to child views or this view's standard scrolling behavior.** <p>Client code may use listeners to implement item manipulation behavior. Once a listener* returns true from* {@link OnItemTouchListener#onInterceptTouchEvent(RecyclerView, MotionEvent)} its* {@link OnItemTouchListener#onTouchEvent(RecyclerView, MotionEvent)} method will be called* for each incoming MotionEvent until the end of the gesture.</p>** @param listener Listener to add* @see SimpleOnItemTouchListener*/public void addOnItemTouchListener(OnItemTouchListener listener) {mOnItemTouchListeners.add(listener);}
添加到了mOnItemTouchListeners集合中,那么mOnItemTouchListeners用来做什么呢?我们发现在RecyclerView的dispatchOnItemTouchIntercept方法中,用到了这个mOnItemTouchListeners集合
而dispatchOnItemTouchIntercept方法是在RecyclerView的onInterceptTouchEvent事件中调用的
@Overridepublic boolean onInterceptTouchEvent(MotionEvent e) {if (mLayoutFrozen) {// When layout is frozen, RV does not intercept the motion event.// A child view e.g. a button may still get the click.return false;}if (dispatchOnItemTouchIntercept(e)) {cancelTouch();//消费了事件,直接返回return true;}
...}
private boolean dispatchOnItemTouchIntercept(MotionEvent e) {final int action = e.getAction();if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_DOWN) {mActiveOnItemTouchListener = null;}final int listenerCount = mOnItemTouchListeners.size();for (int i = 0; i < listenerCount; i++) {final OnItemTouchListener listener = mOnItemTouchListeners.get(i);if (listener.onInterceptTouchEvent(this, e) && action != MotionEvent.ACTION_CANCEL) {//赋值mActiveOnItemTouchListener = listener;return true;}}return false;}
也就是说,事件分发时,在onInterceptTouchEvent方法中又传递给了ItemTouchHelper的OnItemTouchListener的onInterceptTouchEvent方法,并且如果该方法返回了true,则消费事件,并且又把这个OnItemTouchListener 赋值给了mActiveOnItemTouchListener,我们再看跟踪这个mActiveOnItemTouchListener,最终发现了调用的地方
@Overridepublic boolean onTouchEvent(MotionEvent e) {if (mLayoutFrozen || mIgnoreMotionEventTillDown) {return false;}if (dispatchOnItemTouch(e)) {cancelTouch();return true;}
...}private boolean dispatchOnItemTouch(MotionEvent e) {final int action = e.getAction();if (mActiveOnItemTouchListener != null) {if (action == MotionEvent.ACTION_DOWN) {// Stale state from a previous gesture, we're starting a new one. Clear it.mActiveOnItemTouchListener = null;} else {mActiveOnItemTouchListener.onTouchEvent(this, e);if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {// Clean up for the next gesture.mActiveOnItemTouchListener = null;}return true;}}// Listeners will have already received the ACTION_DOWN via dispatchOnItemTouchIntercept// as called from onInterceptTouchEvent; skip it.if (action != MotionEvent.ACTION_DOWN) {final int listenerCount = mOnItemTouchListeners.size();for (int i = 0; i < listenerCount; i++) {final OnItemTouchListener listener = mOnItemTouchListeners.get(i);if (listener.onInterceptTouchEvent(this, e)) {mActiveOnItemTouchListener = listener;return true;}}}return false;}
如果事件分发到onTouchEvent(onInterceptTouchEvent为true或者子控件没有消费事件),那么dispatchOnItemTouch方法会被调用,在不是ACTION_DOWN事件的情况下,如果mActiveOnItemTouchListener不为null,mActiveOnItemTouchListener的onTouchEvent方法会被调用,并且返回true,如果mActiveOnItemTouchListener为null,则又会调用OnItemTouchListener的onInterceptTouchEvent方法
值得注意的是onInterceptTouchEvent方法中,OnItemTouchListener是能接受到ACTION_DOWN事件的,但是onTouchEvent事件中,OnItemTouchListener不能接受到ACTION_DOWN事件
上述事件分发的方法,调用有点乱,没什么头绪,那么我们先来看OnItemTouchListener这个对象,该对象在ItemTouchHelper中
private final OnItemTouchListener mOnItemTouchListener= new OnItemTouchListener() {@Overridepublic boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent event) {mGestureDetector.onTouchEvent(event);if (DEBUG) {Log.d(TAG, "intercept: x:" + event.getX() + ",y:" + event.getY() + ", " + event);}final int action = MotionEventCompat.getActionMasked(event);if (action == MotionEvent.ACTION_DOWN) {mActivePointerId = event.getPointerId(0);mInitialTouchX = event.getX();mInitialTouchY = event.getY();obtainVelocityTracker();if (mSelected == null) {final RecoverAnimation animation = findAnimation(event);if (animation != null) {mInitialTouchX -= animation.mX;mInitialTouchY -= animation.mY;endRecoverAnimation(animation.mViewHolder, true);if (mPendingCleanup.remove(animation.mViewHolder.itemView)) {mCallback.clearView(mRecyclerView, animation.mViewHolder);}select(animation.mViewHolder, animation.mActionState);updateDxDy(event, mSelectedFlags, 0);}}} else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {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 itemfinal int index = event.findPointerIndex(mActivePointerId);if (DEBUG) {Log.d(TAG, "pointer index " + index);}if (index >= 0) {checkSelectForSwipe(action, event, index);}}if (mVelocityTracker != null) {mVelocityTracker.addMovement(event);}return mSelected != null;}@Overridepublic void onTouchEvent(RecyclerView recyclerView, 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 = MotionEventCompat.getActionMasked(event);final int activePointerIndex = event.findPointerIndex(mActivePointerId);if (activePointerIndex >= 0) {checkSelectForSwipe(action, event, activePointerIndex);}ViewHolder viewHolder = mSelected;if (viewHolder == null) {return;}switch (action) {case MotionEvent.ACTION_MOVE: {// Find the index of the active pointer and fetch its positionif (activePointerIndex >= 0) {updateDxDy(event, mSelectedFlags, activePointerIndex);moveIfNecessary(viewHolder);mRecyclerView.removeCallbacks(mScrollRunnable);mScrollRunnable.run();mRecyclerView.invalidate();}break;}case MotionEvent.ACTION_CANCEL:if (mVelocityTracker != null) {mVelocityTracker.clear();}// fall throughcase MotionEvent.ACTION_UP:select(null, ACTION_STATE_IDLE);mActivePointerId = ACTIVE_POINTER_ID_NONE;break;case MotionEvent.ACTION_POINTER_UP: {final int pointerIndex = MotionEventCompat.getActionIndex(event);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;}}}@Overridepublic void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {if (!disallowIntercept) {return;}select(null, ACTION_STATE_IDLE);}};
首先,我们先看onInterceptTouchEvent的ACTION_DOWN,其中findAnimation方法(源码有点多,就不贴出来了)会根据手指按下的位置,找到选中的子控件,然后通过select方法赋值给mSelected,最后return mSelected != null;而ACTION_UP,则会通过select方法将mSelected置空,并且判断是否需要执行侧滑动画,并最终根据判断是否要调用Callback的onSwiped方法,所以ItemTouchHelper的OnItemTouchListener的onInterceptTouchEvent方法只是找到mSelected和释放mSelected
之前了解到onInterceptTouchEvent的onTouchEvent方法中是不会有ACTION_DOWN的,我们先来看ACTION_MOVE,首先调用了updateDxDy方法
void updateDxDy(MotionEvent ev, int directionFlags, int pointerIndex) {final float x = ev.getX(pointerIndex);final float y = ev.getY(pointerIndex);// Calculate the distance movedmDx = x - mInitialTouchX;mDy = y - mInitialTouchY;if ((directionFlags & LEFT) == 0) {mDx = Math.max(0, mDx);}if ((directionFlags & RIGHT) == 0) {mDx = Math.min(0, mDx);}if ((directionFlags & UP) == 0) {mDy = Math.max(0, mDy);}if ((directionFlags & DOWN) == 0) {mDy = Math.min(0, mDy);}}
就是根据标志位得出相应的偏移,接下来调用moveIfNecessary方法
/*** Checks if we should swap w/ another view holder.*/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);if (Math.abs(y - viewHolder.itemView.getTop()) < viewHolder.itemView.getHeight() * threshold&& Math.abs(x - viewHolder.itemView.getLeft())< viewHolder.itemView.getWidth() * threshold) {return;}List<ViewHolder> swapTargets = findSwapTargets(viewHolder);if (swapTargets.size() == 0) {return;}// may swap.ViewHolder target = mCallback.chooseDropTarget(viewHolder, swapTargets, x, y);if (target == null) {mSwapTargets.clear();mDistances.clear();return;}final int toPosition = target.getAdapterPosition();final int fromPosition = viewHolder.getAdapterPosition();if (mCallback.onMove(mRecyclerView, viewHolder, target)) {// keep target visiblemCallback.onMoved(mRecyclerView, viewHolder, fromPosition,target, toPosition, x, y);}}
发现最后调用了Callback的onMove方法,这也是我们需要改写的拖拽方法,而ACTION_UP和onInterceptTouchEvent中的差不多
最后总结一下,ItemTouchHelper是通过OnItemTouchListener获取到选中的ViewHolder,并通过它的内部类Callback,调用ItemTouchUIUtilImpl中的方法进行绘制工作