RecyclerView源码详解(第一篇ItemTouchHelper源码详解)

来源:互联网 发布:ug10.0编程视频教程 编辑:程序博客网 时间:2024/06/05 06:00

在写上一篇vlayout源码解析第一篇的时候,我发现随着源码的深入,这个框架对RecycleView的运用已达到如火纯青的地步,也就是说写这个框架的哥们对RecyclerView源码已经研究的相当透彻,那么为了更好的理解这个框架,就要先来研究一下RecyclerView源码。今天的主题是ItemTouchHelper源码详解,怎么最快的实现侧滑删除的效果,先看效果图:


这样的效果要是用以前的自定义控件的方式的话起码得上千行代码,而用ItemTouchHelper的话,少量的代码就可以搞定,先看一下怎么ItemTouchHelper怎么和RecycleView绑定的。

ItemTouchHelper.Callback callback = new MyItemTouchHelperCallback((ItemTouchMoveListener) adapter);ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);itemTouchHelper.attachToRecyclerView(messagemain_lsitview);
三行代码实现RecyclerView绑定ItemTouchHelper,如果想定义自己的效果只需要重写ItemTouchHelper.Callback这个类即可,但是前提你得知道需要覆写那些方法,这些方法到底是干什么的。虽然有文档可看,但是文档的意思模棱两可,很难体会它想表达的意思,只能说有时候文档确实很坑。那么唯一的办法就是看源码了,好继续进入itemTouchHelper.attachToRecyclerView(messagemain_lsitview)这个方法

public void attachToRecyclerView(RecyclerView recyclerView) {//已经绑定了同一个RecyclerView就返回if (mRecyclerView == recyclerView) {return; // nothing to do}//先释放解绑以前的if (mRecyclerView != null) {destroyCallbacks();}mRecyclerView = recyclerView;if (mRecyclerView != null) {//RecycleView开始绑定ItemTouchHelpersetupCallbacks();}}

/* * 绑定ItemTouchHelp */private void setupCallbacks() {ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());//得到认为滑动的最小的距离mSlop = vc.getScaledTouchSlop();//添加分割线画法,显然这里不是用来画分割线的mRecyclerView.addItemDecoration(this);//添加触发事件的监听函数,RecycleView通过mOnItemTouchListener实现和itemtouch的交互mRecyclerView.addOnItemTouchListener(mOnItemTouchListener);//子View被attach或dettach时通知回调mRecyclerView.addOnChildAttachStateChangeListener(this);//初始化手势initGestureDetector();}
这两个方法就是ItemTouchHelper内部绑定RecyclerView的方法,这里有一个奇怪的方法mRecyclerView.addItemDecoration(this)添加分割线,这里为啥要添加分割线的监听回调
public class ItemTouchHelper extends RecyclerView.ItemDecoration
奥,原来是ItemTouchHelper 继承自ItemDecoration,我们知道RecyclerView不像ListView那样直接设置俩个属性就会出现分割线,想要在RecyclerView上实现分割线的话,那么必须实现ItemDecoration这个类让RecycleView去根据这个类去画这个分割线,当然不止是分割线,你也可以画一些其他的东西,只要你有创意。
那么接下来看一下
mOnItemTouchListener是干嘛的

public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e);/** * Process a touch event as part of a gesture that was claimed by * returning true from a previous call to {@link #onInterceptTouchEvent} * . *  * @param e *            MotionEvent describing the touch event. All coordinates *            are in the RecyclerView's coordinate system. *//** * 触摸处理事件分发 *  * @param rv * @param e */public void onTouchEvent(RecyclerView rv, MotionEvent e);/** * Called when a child of RecyclerView does not want RecyclerView and * its ancestors to intercept touch events with * {@link ViewGroup#onInterceptTouchEvent(MotionEvent)}. *  * @param disallowIntercept *            True if the child does not want the parent to intercept *            touch events. * @see ViewParent#requestDisallowInterceptTouchEvent(boolean) *//** * 不希望RecycleView拦截事件 *  * @param disallowIntercept */public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept);}
这是一个接口,在里面看到了熟悉的方法onInterceptTouchEvent(这不是ViewGroup的拦截事件吗),onTouchEvent(这不是View的处理事件的方法吗),so那么现在可以猜测,RecycleView是通过这个接口将触摸事件传给ItemTouchHelper 类来处理的,好接下来验证推论


@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;}if (mLayout == null) {return false;}下面省略n行}

看RecycleView的拦截事件,这个方法如果分发给ItemTouchHelper返回true,则RecycleView将不向下执行直接拦截
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);// 哪一个返回true,哪一个就等于mActiveOnItemTouchListenerif (listener.onInterceptTouchEvent(this, e)&& action != MotionEvent.ACTION_CANCEL) {mActiveOnItemTouchListener = listener;return true;}}return false;}

这个方法就是将拦截事件交个ItemTouchHelper了,也就是在ItemTouchHelper被RecycleView绑定的时候,将onInterceptTouchEvent实现类绑定到了RecycleViewmOnItemTouchListeners集合中,从而实现RecycleView的触摸事件回调回ItemTouchHelper的类进行处理

public boolean onTouchEvent(MotionEvent e) {if (mLayoutFrozen || mIgnoreMotionEventTillDown) {return false;}// 先看看itemView要不要处理itemtouchif (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事件采用同样的方式将事件传给ItemTouchHelper!那么ItemView的滑动肯定和ItemTouchHelper的OnItemTouchListener接口实现类肯定密不可分,好绕了一圈那么再回到ItemTouchHelper这个类,看一下事件分发到实现接口后都做了什么

public 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 = MotionEventCompat.getPointerId(event, 0);// action=Down的时候按下mInitialTouchX = event.getX();// action=Down的时候按下mInitialTouchY = event.getY();obtainVelocityTracker();if (mSelected == null) {final RecoverAnimation animation = findAnimation(event);if (animation != null) {Log.i("huoying", "inter:animation!=null");mInitialTouchX -= animation.mX;mInitialTouchY -= animation.mY;endRecoverAnimation(animation.mViewHolder, true);// 结束动画时清除mPendingCleanup保存的itemView的集合if (mPendingCleanup.remove(animation.mViewHolder.itemView)) {mCallback.clearView(mRecyclerView,animation.mViewHolder);}select(animation.mViewHolder, animation.mActionState);// 更新选中的itemView的距离updateDxDy(event, mSelectedFlags, 0);}}}// 抬起,取消设置无状态else if (action == MotionEvent.ACTION_CANCEL|| action == MotionEvent.ACTION_UP) {mActivePointerId = ACTIVE_POINTER_ID_NONE;Log.i("huoying", "inter:up");select(null, ACTION_STATE_IDLE);}// 如果mActivePointerId有效就走判断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 = MotionEventCompat.findPointerIndex(event,mActivePointerId);if (DEBUG) {Log.d(TAG, "pointer index " + index);}if (index >= 0) {Log.i("huoying", "inter:ACTIVE_POINTER_ID_NONE");// 检查是否可滑动checkSelectForSwipe(action, event, index);}}if (mVelocityTracker != null) {mVelocityTracker.addMovement(event);}return mSelected != null;}

这个方法的代码有点多,主题意思就是点击的时候的手指点击的时候记录初始触摸坐标,并初始化手指速度测试类(VelocityTracker用来判断滑动有没有变成fling),查找集合中有没有和当前子ItemView(触摸事件的x,y在itemView位置的内部)绑定的动画类RecoverAnimation ,如果有结束动画,并且重新确定手指选择的是哪个itemView,手指mActivePointerId != ACTIVE_POINTER_ID_NONE,也就是触摸的手指事件有效的情况下调用checkSelectForSwipe(action, event, index),检查是否是滑动状态,最后手指抬起或发生意外取消的时候调用select(null, ACTION_STATE_IDLE),此类中最重要的就是select和checkSelectForSwipe这两个方法。第1次走拦截事件的时候,由于动画是null的,那么最后会走到checkSelectForSwipe(action, event, index),好看一看这个方法


private boolean checkSelectForSwipe(int action, MotionEvent motionEvent,int pointerIndex) {// 当已经有选中的View时,或事件不等于滑动事件,或者mActionState=正在被拖动的状态,或者mCallback不支持滑动直接返回falseif (mSelected != null || action != MotionEvent.ACTION_MOVE|| mActionState == ACTION_STATE_DRAG|| !mCallback.isItemViewSwipeEnabled()) {return false;}// 如果当前mRecyclerView的状态是正在拖动的状态返回falseif (mRecyclerView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) {return false;}// 根据触摸事件找到手指放在哪个子View的位置final ViewHolder vh = findSwipedView(motionEvent);if (vh == null) {return false;}// 得到移动状态final int movementFlags = mCallback.getAbsoluteMovementFlags(mRecyclerView, vh);// 通过计算得到滑动多的状态参数值final int swipeFlags = (movementFlags & ACTION_MODE_SWIPE_MASK) >> (DIRECTION_FLAG_COUNT * ACTION_STATE_SWIPE);if (swipeFlags == 0) {return false;}// 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 directionfinal float x = MotionEventCompat.getX(motionEvent, pointerIndex);final float y = MotionEventCompat.getY(motionEvent, pointerIndex);// 得到手指水平竖直移动距离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 false;}// 水平方向移动的距离大的时候if (absDx > absDy) {// dx小于0表示手指向左滑动,如果设置的swipeFlags不包括item的话,不做操作if (dx < 0 && (swipeFlags & LEFT) == 0) {return false;}// 和上面同理if (dx > 0 && (swipeFlags & RIGHT) == 0) {return false;}} else {// 和上面同理if (dy < 0 && (swipeFlags & UP) == 0) {return false;}// 和上面同理if (dy > 0 && (swipeFlags & DOWN) == 0) {return false;}}// 当前选中itemView的偏移量归零mDx = mDy = 0f;mActivePointerId = MotionEventCompat.getPointerId(motionEvent, 0);// 满足滑动,设置滑动状态ACTION_STATE_SWIPEselect(vh, ACTION_STATE_SWIPE);return true;}
额,代码量也不少,如果当前被选中的itemView还为空,此事件不是move事件,不是拖撤事件,Callback是可以滑动的(isItemViewSwipeEnabled默认返回true),那么
不满足这些条件程序继续走,根据点击的x,y坐标判断触点在哪个子itemView之中,然后根据子View得到ViewHolder ,接下来获取客户端定义的状态movementFlags 。
也就是下面这个方法,我们将左右事件定义为滑动类型,上下滑动定义为拖撤类型,这个方法是抽象的,也就是说自定义ItemTouchHelper.Callback时必须重写这个方法

public int getMovementFlags(RecyclerView recyclerView, ViewHolder holder) {int dragFlags = ItemTouchHelper.UP|ItemTouchHelper.DOWN;int swipeFlags = ItemTouchHelper.LEFT|ItemTouchHelper.RIGHT;//定义上下为拖动,左右为滑动int flags = makeMovementFlags(dragFlags, swipeFlags);return flags;}
那么现在知道这个方法到底是干啥用的了,告诉ItemTouchHelper什么情况下是拖撤事件,什么情况下是滑动事件,接下来计算滑动是那一个方向的,如果滑动不包括left,right,down,up的话直接返回false,以下判断都满足后清空偏移距离,并调用select(vh, ACTION_STATE_SWIPE),此时传入的状态是滑动状态。也就是说ItemTouchHelper
为ItemView设置了拖动状态、滑动状态、无状态三种

ACTION_STATE_SWIPE:滑动状态,手指没有长按时滑动

ACTION_STATE_IDLE:无状态,手指抬起时声明为此状态

ACTION_STATE_DRAG:长按滑动时的拖动状态

好接着看select(vh, ACTION_STATE_SWIPE)方法

if (selected == mSelected && actionState == mActionState) {return;}mDragScrollStartTimeInMs = Long.MIN_VALUE;final int prevActionState = mActionState;// prevent duplicate animationsendRecoverAnimation(selected, true);mActionState = actionState;// 假如状态是拖动,例如久安的时候if (actionState == ACTION_STATE_DRAG) {// 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.mOverdrawChild = selected.itemView;addChildDrawingOrderCallback();}//int actionStateMask = (1 << (DIRECTION_FLAG_COUNT + DIRECTION_FLAG_COUNT* actionState)) - 1;boolean preventLayout = false;if (mSelected != null) {// 上一个选中的mSelectedfinal ViewHolder prevSelected = mSelected;if (prevSelected.itemView.getParent() != null) {final int swipeDir = prevActionState == ACTION_STATE_DRAG ? 0: swipeIfNecessary(prevSelected);releaseVelocityTracker();// find where we should animate tofinal float targetTranslateX, targetTranslateY;int animationType;switch (swipeDir) {// 向左滑动,向右滑动case LEFT:case RIGHT:case START:case END:// java.lang.Math.signum(double d)// 如果参数大于零返回1.0,如果参数小于零返回-1,如果参数为0,则返回signum函数的参数为零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) {// 标记动画状态为可滑动animationType = ANIMATION_TYPE_SWIPE_SUCCESS;} else {// 标记动画状态为滑动取消状态animationType = ANIMATION_TYPE_SWIPE_CANCEL;}// 将选中view的移动距离赋值到数组中getSelectedDxDy(mTmpPosition);final float currentTranslateX = mTmpPosition[0];final float currentTranslateY = mTmpPosition[1];// 定义回复动画final RecoverAnimation rv = new RecoverAnimation(prevSelected,animationType, prevActionState, currentTranslateX,currentTranslateY, targetTranslateX, targetTranslateY) {@Overridepublic void onAnimationEnd(ValueAnimatorCompat animation) {super.onAnimationEnd(animation);if (this.mOverridden) {return;}// 上面计算的swipeDir《=0的时候,就是拖动或者滑动失败的方式if (swipeDir <= 0) {// this is a drag or failed swipe. recover// immediatelymCallback.clearView(mRecyclerView, prevSelected);// full cleanup will happen on onDrawOver} else {// wait until remove animation is complete.// 滑动动画结束后,将动画加入缓存mPendingCleanupmPendingCleanup.add(prevSelected.itemView);mIsPendingCleanup = true;if (swipeDir > 0) {// Animation might be ended by other animators// during a layout.// We defer callback to avoid editing adapter// during a layout.postDispatchSwipe(this, swipeDir);}}// removed from the list after it is drawn for the last// timeif (mOverdrawChild == prevSelected.itemView) {removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);}}};final long duration = mCallback.getAnimationDuration(mRecyclerView, animationType, targetTranslateX- currentTranslateX, targetTranslateY- currentTranslateY);rv.setDuration(duration);mRecoverAnimations.add(rv);rv.start();preventLayout = true;} else {removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);mCallback.clearView(mRecyclerView, prevSelected);}mSelected = null;}if (selected != null) {mSelectedFlags = (mCallback.getAbsoluteMovementFlags(mRecyclerView,selected) & actionStateMask) >> (mActionState * DIRECTION_FLAG_COUNT);// 选中的时候赋值mSelectedStartX=leftmSelectedStartX = selected.itemView.getLeft();// mSelectedStartY=topmSelectedStartY = selected.itemView.getTop();mSelected = selected;// 如果是拖动if (actionState == ACTION_STATE_DRAG) {// 回调长按的反馈mSelected.itemView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);}}final ViewParent rvParent = mRecyclerView.getParent();// 通知RecycleView不拦截子View的事件if (rvParent != null) {rvParent.requestDisallowInterceptTouchEvent(mSelected != null);}if (!preventLayout) {// 设置layoutManager条目动画可以执行mRecyclerView.getLayoutManager().requestSimpleAnimationsInNextLayout();}// 告诉mCallback选中的View已经改变了mCallback.onSelectedChanged(mSelected, mActionState);// 重新绘制,因为temTouchHelper extends// RecyclerView.ItemDecoration(想实现一些不为人知的秘密)mRecyclerView.invalidate();

这里的代码量也相当的多,这个方法的主要意思就是如果为滑动的事件那么为选中的itemView赋值,记录位置信息,并为选中view绑定动画(这个动画执行0-1的变化),最后回调mCallback.onSelectedChanged方法,,也就是说每次选中的子itemView改变的时候会调用这个方法(我们可以在这个方法里面记录被选中的子itemView),最后调用了
mRecyclerView.invalidate()进行重画,好这个方法也知道干什么了。也就是说onInterceptTouchEvent,主要实现了对选中itemView的各项赋值,只要存在满足条件的itemView,拦截事件成立,将执行onTouchEvent事件


public 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 = MotionEventCompat.findPointerIndex(event, 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) {Log.i("huoying", "touch:move");// 先更新位置再移动// 不断移动的时候改变选中的View的移动距离updateDxDy(event, mSelectedFlags, activePointerIndex);// 移动viewHoldermoveIfNecessary(viewHolder);mRecyclerView.removeCallbacks(mScrollRunnable);mScrollRunnable.run();// 最后重画数据mRecyclerView.invalidate();}break;}case MotionEvent.ACTION_CANCEL:case MotionEvent.ACTION_UP:if (mVelocityTracker != null) {mVelocityTracker.computeCurrentVelocity(1000,mRecyclerView.getMaxFlingVelocity());}Log.i("huoying", "touch: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 = MotionEventCompat.getPointerId(event,pointerIndex);if (pointerId == mActivePointerId) {if (mVelocityTracker != null) {mVelocityTracker.computeCurrentVelocity(1000,mRecyclerView.getMaxFlingVelocity());}// This was our active pointer going up. Choose a new// active pointer and adjust accordingly.final int newPointerIndex = pointerIndex == 0 ? 1 : 0;mActivePointerId = MotionEventCompat.getPointerId(event,newPointerIndex);updateDxDy(event, mSelectedFlags, pointerIndex);}break;}}}


既然拦截事件已经帮我们确定了要移动的itemView是谁了,那么onTouchEvent只要处理移动就好了,从效果图中可以看出左右滑动时,选中的itemView会左右移动并伴随着缩放效果,在ACTION_MOVE事件中updateDxDy(event, mSelectedFlags, activePointerIndex)用于不断改变手指离按下偏移了多少距离,然后调用moveIfNecessary方法,最后又调用了mRecyclerView.invalidate(),看来移动ItemView并不是简单的移动,它的操作可能在Ondraw方法中。接着看一下这三个方法

private void updateDxDy(MotionEvent ev, int directionFlags, int pointerIndex) {final float x = MotionEventCompat.getX(ev, pointerIndex);final float y = MotionEventCompat.getY(ev, 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);}}

这个方法很简单就是记录一下水平偏移量mDx ,竖直偏移量mDy ,再看下moveIfNecessary方法


if (mRecyclerView.isLayoutRequested()) {return;}if (mActionState != ACTION_STATE_DRAG) {return;}final float threshold = mCallback.getMoveThreshold(viewHolder);// 计算新的位置的left,topfinal 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();// 回调mCallback的onMove方法if (mCallback.onMove(mRecyclerView, viewHolder, target)) {// keep target visiblemCallback.onMoved(mRecyclerView, viewHolder, fromPosition, target,toPosition, x, y);}}
这个方法首先判断一下是否是拖动的状态,不是则直接返回,说明这个方法是为拖动状态准备的,看完滑动再看拖动,那么最后只剩下重画方法可以用来执行位置的偏移了,那么进入RecycleView看看它的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);}}

可以看到此方法除了调用父类的onDraw方法进行画图,还调用了画分割线的回调方法,对了ItemTouchHelper是继承ItemDecoration,好每次重画RecycleView的时候会回调到ItemTouchHelper中

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];}mCallback.onDraw(c, parent, mSelected, mRecoverAnimations,mActionState, dx, dy);}

这个方法计算一下选中的ItemView的新的位置偏移量,最后调用Callback的Ondraw

private 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();// 最后以anim.mX, anim.mY为最终的偏移值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);}}


每次重新选择itemView的时候,已经标记为选中的itemView绑定的anim会被移除,所以最后走slected!=null的onChildDraw方法

@Overridepublic void onChildDraw(Canvas c, RecyclerView recyclerView,ViewHolder viewHolder, float dX, float dY, int actionState,boolean isCurrentlyActive) {if(actionState==ItemTouchHelper.ACTION_STATE_SWIPE){float alpha = 1-Math.abs(dX)/viewHolder.itemView.getWidth();viewHolder.itemView.setAlpha(alpha);//1~0viewHolder.itemView.setScaleX(alpha);//1~0viewHolder.itemView.setScaleY(alpha);//1~0}super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState,isCurrentlyActive);}

这个方法是自己覆写的父类方法,可以看到当状态为滑动的时候用水平偏移量作为变量因子,偏移量越大透明度越大,缩放越明显,这也就有了效果图中的效果,最后又调用父类的方法

public void onChildDraw(Canvas c, RecyclerView recyclerView,ViewHolder viewHolder, float dX, float dY, int actionState,boolean isCurrentlyActive) {sUICallback.onDraw(c, recyclerView, viewHolder.itemView, dX, dY,actionState, isCurrentlyActive);}

sUICallback又是啥?


static {if (Build.VERSION.SDK_INT >= 21) {sUICallback = new ItemTouchUIUtilImpl.Lollipop();} else if (Build.VERSION.SDK_INT >= 11) {sUICallback = new ItemTouchUIUtilImpl.Honeycomb();} else {sUICallback = new ItemTouchUIUtilImpl.Gingerbread();}}
根据版本号生成不一样的工具类,当小于11时


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();}


版本号小于11的时候通过移动画布,来达到选中的itemView的移动效果,那么大于11的呢

public void onDraw(Canvas c, RecyclerView recyclerView, View view,float dX, float dY, int actionState, boolean isCurrentlyActive) {// 直接设置距离ViewCompat.setTranslationX(view, dX);ViewCompat.setTranslationY(view, dY);}

直接移动偏移量啊,也对看到这顿时明朗了,不断移动的view在3.0以上可以直接setTranslation方法,3.0以下移动画布,这是最原始移动View的方法了吧。接下来看一下拖动效果的实现,长按的时候才会产生拖动事件,那么找到手势识别器的实现



public void onLongPress(MotionEvent e) {Log.i("huoying", "长按");View child = findChildView(e);if (child != null) {ViewHolder vh = mRecyclerView.getChildViewHolder(child);if (vh != null) {//判断callBack中有没有设置DragFlagif (!mCallback.hasDragFlag(mRecyclerView, vh)) {return;}int pointerId = MotionEventCompat.getPointerId(e, 0);// Long press is deferred.// Check w/ active pointer id to avoid selecting after// motion// event is canceled.if (pointerId == mActivePointerId) {final int index = MotionEventCompat.findPointerIndex(e,mActivePointerId);final float x = MotionEventCompat.getX(e, index);final float y = MotionEventCompat.getY(e, index);mInitialTouchX = x;mInitialTouchY = y;mDx = mDy = 0f;if (DEBUG) {Log.d(TAG, "onlong press: x:" + mInitialTouchX+ ",y:" + mInitialTouchY);}if (mCallback.isLongPressDragEnabled()) {select(vh, ACTION_STATE_DRAG);}}}}}}

当发生长按的事件后,标记一下down事件的,x,y坐标,调用mCallback.isLongPressDragEnabled(),判断客户端是否允许长按事件,允许的话就调用选择子ItemView的方法,此时标记类型为ACTION_STATE_DRAG,那么移动的时候当然就会走


private void moveIfNecessary(ViewHolder viewHolder) {if (mRecyclerView.isLayoutRequested()) {return;}if (mActionState != ACTION_STATE_DRAG) {return;}final float threshold = mCallback.getMoveThreshold(viewHolder);// 计算新的位置的left,topfinal 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();// 回调mCallback的onMove方法if (mCallback.onMove(mRecyclerView, viewHolder, target)) {// keep target visiblemCallback.onMoved(mRecyclerView, viewHolder, fromPosition, target,toPosition, x, y);}}
这个方法根据选中的itemView的将要偏移的值,算出和其他的itemView那些发生了碰撞,如果发生了碰撞的话,回调mCallback.onMove方法,这个方法也是我们自己实现的

public boolean onMove(RecyclerView recyclerView, ViewHolder srcHolder, ViewHolder targetHolder) {//如果它们的viewType不一样不让他们替换if(srcHolder.getItemViewType()!=targetHolder.getItemViewType()){return false;}boolean result = moveListener.onItemMove(srcHolder.getAdapterPosition(), targetHolder.getAdapterPosition());return result;}


到这可以看出onMove方法会在拖动选中itemView与其他的itemView发生碰撞的时候将会回调,这里还有一个方法getBoundingBoxMargin的回调方法,此方法如果为正值,则碰撞的机会会增加,如果为负值碰撞的机会会减小,如果我们想增加机会或减小碰撞范围需重写此方法

public boolean onItemMove(int fromPosition, int toPosition) {// TODO Auto-generated method stub//交换集合的两个位置,通知RecycleView交换positionCollections.swap(messagemain_list, fromPosition, toPosition);notifyItemMoved(fromPosition, toPosition);return true;}

我们这个拖动的时候,满足碰撞的之后直接交换了两个子itemView的位置,所以才有了动态图中的效果,最终的位置移动还是通过了Ondraw()方法。好最后就剩下手指抬起了

if (mVelocityTracker != null) {mVelocityTracker.computeCurrentVelocity(1000,mRecyclerView.getMaxFlingVelocity());}Log.i("huoying", "touch:up");select(null, ACTION_STATE_IDLE);

最后设置状态为ACTION_STATE_IDLE,将选中的itemView置为null,最后手指抬起的时候将会执行动画RecoverAnimation


public void onAnimationEnd(ValueAnimatorCompat animation) {super.onAnimationEnd(animation);if (this.mOverridden) {return;}// 上面计算的swipeDir《=0的时候,就是拖动或者滑动失败的方式if (swipeDir <= 0) {// this is a drag or failed swipe. recover// immediatelymCallback.clearView(mRecyclerView, prevSelected);// full cleanup will happen on onDrawOver} else {// wait until remove animation is complete.// 滑动动画结束后,将动画加入缓存mPendingCleanupmPendingCleanup.add(prevSelected.itemView);mIsPendingCleanup = true;if (swipeDir > 0) {// Animation might be ended by other animators// during a layout.// We defer callback to avoid editing adapter// during a layout.postDispatchSwipe(this, swipeDir);}}// removed from the list after it is drawn for the last// timeif (mOverdrawChild == prevSelected.itemView) {removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);}}


而这个动画的最后将回调动画结束回调,如果此时滑动超过了子view宽度的一半的话就会回调postDispatchSwipe方法

mRecyclerView.post(new Runnable() {@Overridepublic void run() {if (mRecyclerView != null&& mRecyclerView.isAttachedToWindow()&& !anim.mOverridden&& anim.mViewHolder.getAdapterPosition() != RecyclerView.NO_POSITION) {final RecyclerView.ItemAnimator animator = mRecyclerView.getItemAnimator();// if animator is running or we have other active recover// animations, we try// not to call onSwiped because DefaultItemAnimator is not// good at merging// animations. Instead, we wait and batch.// 防止ItemAnimator的影响if ((animator == null || !animator.isRunning(null))&& !hasRunningRecoverAnim()) {// 滑动完成调用的方法Log.i("huoying", "swiped");mCallback.onSwiped(anim.mViewHolder, swipeDir);} else {mRecyclerView.post(this);}}}});}

也就是执行onSwiped回调,这个方法也是自己实现的,即水平滑动的距离超过ItemView宽度的一半的话,抬起手指的时候就会调用此方法移除当前的选中itemview


public void onSwiped(ViewHolder holder, int arg1) {moveListener.onItemRemove(holder.getAdapterPosition());}
分析到这ItemTouchHelper的思路已经通了,看大体的调用图



总结:

1、ItemTouchHelper将OnItemTouchListener注册进RecyclerView,将自身的ItemDecoration注册进RecyclerView

2、RecyclerView接收到触摸事件的时候先把事件交给ItemTouchHelper里的OnItemTouchListener处理

3、OnItemTouchListener在onInterceptTouchEvent中调用select()方法找到那个itemView可以被标记为选中

4、OnItemTouchListener在onTouchEvent不断通过updateDxDy(event, mSelectedFlags, activePointerIndex)方法计算偏移量,如果是拖动通过moveIfNecessary()方法计算有满足的碰撞吗,有的话就回调onMove方法交换位置,最后执行mRecyclerView.invalidate()重写画图

5、在RecyclerView的重画onDraw()方法中调用ItemTouchHelper的onDraw方法,最后回调Callback的onChildDraw方法将选中的itemView进行偏移





阅读全文
0 0
原创粉丝点击