在写上一篇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<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 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<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);
}
}
@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<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 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的思路已经通了,看大体的调用图
总结:
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进行偏移