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

在写上一篇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开始绑定ItemTouchHelper
			setupCallbacks();
		}
	}

/*
	 * 绑定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 类来处理的,好接下来验证推论


@Override
	public 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,哪一个就等于mActiveOnItemTouchListener
			if (listener.onInterceptTouchEvent(this, e)
					&& action != MotionEvent.ACTION_CANCEL) {
				mActiveOnItemTouchListener = listener;
				return true;
			}
		}
		return false;
	}

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

public boolean onTouchEvent(MotionEvent e) {
		if (mLayoutFrozen || mIgnoreMotionEventTillDown) {
			return false;
		}
		// 先看看itemView要不要处理itemtouch
		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事件采用同样的方式将事件传给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 item
				final 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不支持滑动直接返回false
		if (mSelected != null || action != MotionEvent.ACTION_MOVE
				|| mActionState == ACTION_STATE_DRAG
				|| !mCallback.isItemViewSwipeEnabled()) {
			return false;
		}
		// 如果当前mRecyclerView的状态是正在拖动的状态返回false
		if (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 direction
		final 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_SWIPE
		select(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 animations
		endRecoverAnimation(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) {
			// 上一个选中的mSelected
			final ViewHolder prevSelected = mSelected;
			if (prevSelected.itemView.getParent() != null) {
				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:
					// 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) {
					@Override
					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
							// immediately
							mCallback.clearView(mRecyclerView, prevSelected);
							// full cleanup will happen on onDrawOver
						} else {
							// wait until remove animation is complete.
							// 滑动动画结束后,将动画加入缓存mPendingCleanup
							mPendingCleanup.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
						// time
						if (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=left
			mSelectedStartX = selected.itemView.getLeft();
			// mSelectedStartY=top
			mSelectedStartY = 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 position

				if (activePointerIndex >= 0) {
					Log.i("huoying", "touch:move");
					// 先更新位置再移动
					// 不断移动的时候改变选中的View的移动距离
					updateDxDy(event, mSelectedFlags, activePointerIndex);
					// 移动viewHolder
					moveIfNecessary(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 moved
		mDx = 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,top
		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 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 visible
			mCallback.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是继承 I temDecoration,好每次重画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 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方法

@Override
	public 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~0
			viewHolder.itemView.setScaleX(alpha);//1~0
			viewHolder.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中有没有设置DragFlag
					if (!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,top
		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 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 visible
			mCallback.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交换position
			Collections.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
							// immediately
							mCallback.clearView(mRecyclerView, prevSelected);
							// full cleanup will happen on onDrawOver
						} else {
							// wait until remove animation is complete.
							// 滑动动画结束后,将动画加入缓存mPendingCleanup
							mPendingCleanup.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
						// time
						if (mOverdrawChild == prevSelected.itemView) {
							removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);
						}
					}


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

mRecyclerView.post(new Runnable() {
			@Override
			public 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的思路已经通了,看大体的调用图

RecyclerView源码详解(第一篇ItemTouchHelper源码详解)_第1张图片

总结:

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进行偏移





你可能感兴趣的:(android高级组件原理篇)