ItemTouchHelper 与 RecyclerView 产生关联是通过 ItemTouchHelper 的 attachToRecyclerView() 方法,把 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();
}
}
它调用了 setupCallbacks() 方法,这个是重点,我们看看它的逻辑
private void setupCallbacks() {
ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());
mSlop = vc.getScaledTouchSlop();
mRecyclerView.addItemDecoration(this);
mRecyclerView.addOnItemTouchListener(mOnItemTouchListener);
mRecyclerView.addOnChildAttachStateChangeListener(this);
initGestureDetector();
}
private void initGestureDetector() {
if (mGestureDetector != null) {
return;
}
mGestureDetector = new GestureDetectorCompat(mRecyclerView.getContext(),
new ItemTouchHelperGestureListener());
}
这里面重点关注三行代码,分别是 addItemDecoration(this) 、 addOnItemTouchListener(mOnItemTouchListener) 和 initGestureDetector(),这三行可以说是包含了 item 横滑拖拽的逻辑,先看看 addItemDecoration(this) ,我们知道,ItemTouchHelper 继承了 ItemDecoration,通过 addItemDecoration() 方法把它添加到 RecyclerView 中,那么在 item 绘制时,会回调 ItemDecoration 中相应的方法,我们看看重点的三个方法
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
outRect.setEmpty();
}
这个 Rect 对象里面数据皆为0,再看看 onDraw() 、onDrawOver() 方法
public 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);
}
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);
}
通过观察可以发现,这两个方法里面的逻辑基本一样,只是最终分别调用不同的回调方法,我们看看 getSelectedDxDy(mTmpPosition) 方法,在这个方法中,会把 item 的当前位移偏量计算出来,放到 mTmpPosition 数组中,然后赋值给 dx 和 dy,至于是如何计算的,后面再分析。获取到偏移量后,然后调用 mCallback 的回调,我们看看 mCallback 是什么,它是 ItemTouchHelper 的内部类 Callback,也是上一篇文中的 SimpleItemTouchHelperCallback,我们看看调用的方法
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();
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);
}
}
先不管 for 循环中的 ItemTouchHelper.RecoverAnimation 对象,这是个类似辅助动画,我们看最后一行diamante,尤其是 onChildDraw() 方法,
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);
}
同理, onDrawOver() 对应的是
public void onChildDrawOver(Canvas c, RecyclerView recyclerView, ViewHolder viewHolder,
float dX, float dY, int actionState, boolean isCurrentlyActive) {
sUICallback.onDrawOver(c, recyclerView, viewHolder.itemView, dX, dY, actionState, isCurrentlyActive);
}
看看 sUICallback 这个对象又是什么,原来是 ItemTouchUIUtil,它是在静态代码块中被创建对象
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();
}
}
这里是做了版本兼容,我们看看 ItemTouchUIUtilImpl 代码,android版本小于11,使用 Gingerbread 对象,它里面最终调用 draw() 方法
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();
}
代码意思是通过画布保存,然后位移画布,绘制后,还原画布层,这样就实现了item的位移;再看看版本 11 和 21 的两个类,Lollipop 继承了 Honeycomb,直接看 Honeycomb 代码
@Override
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);
}
版本 11 以上,添加了 setTranslationX() 位移方法,这里通过这个方法实现位移,效率更高;看看 Lollipop 中方法
@Override
public 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);
}
这个里面在 Honeycomb 的基础上,添加了坐标轴Z轴的坐标,这样view之间会显示出层次感; onDrawOver() 方法为空,默认无功能实现。如果我们想在item横滑时,让它随着位移变的透明,可以通过重写 ItemTouchHelper.Callback 方法中 onChildDraw() 方法,如果不重写这个方法,item 只会左右位移,重写时判断当前是否是左右拖拽,然后根据位移 dx 占宽的值,来显示透明度,
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
float dX, float dY, int actionState, boolean isCurrentlyActive) {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
float alpha = 1 - Math.abs(dX) / viewHolder.itemView.getWidth();
viewHolder.itemView.setAlpha(alpha);
}
}
这里可能有同学就有疑问了,既然 ItemTouchHelper 是 ItemDecoration 类型,并且刚才的方法是在 onDraw() 方法中调用,是不是说只要 RecyclerView 滑动,就会调用onChildDraw()方法?我们注意 mCallback 中 onDraw() 方法,调用 onChildDraw() 有个前提 if (selected != null),当 RecyclerView 自身滑动时,这个 selected 是空的,当我们按着 item 拖拽时,满足条件 selected 才会有值。
看看 mRecyclerView.addOnItemTouchListener(mOnItemTouchListener) 代码,
private final OnItemTouchListener mOnItemTouchListener = new OnItemTouchListener() {
@Override
public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent event) {
mGestureDetector.onTouchEvent(event);
final int action = MotionEventCompat.getActionMasked(event);
if (action == MotionEvent.ACTION_DOWN) {
...
} 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) {
final int index = MotionEventCompat.findPointerIndex(event, mActivePointerId);
if (index >= 0) {
checkSelectForSwipe(action, event, index);
}
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(event);
}
return mSelected != null;
}
@Override
public void onTouchEvent(RecyclerView recyclerView, MotionEvent event) {
mGestureDetector.onTouchEvent(event);
...
if (activePointerIndex >= 0) {
checkSelectForSwipe(action, event, activePointerIndex);
}
ViewHolder viewHolder = mSelected;
if (viewHolder == null) {
return;
}
switch (action) {
case MotionEvent.ACTION_MOVE: {
if (activePointerIndex >= 0) {
updateDxDy(event, mSelectedFlags, activePointerIndex);
moveIfNecessary(viewHolder);
mRecyclerView.removeCallbacks(mScrollRunnable);
mScrollRunnable.run();
mRecyclerView.invalidate();
}
break;
}
...
}
}
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (!disallowIntercept) {
return;
}
select(null, ACTION_STATE_IDLE);
}
};
这里面是触摸事件机制,通过 checkSelectForSwipe(action, event, index) 方法 调用 select(vh, ACTION_STATE_SWIPE) 方法,开启横滑事件
private boolean checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) {
...
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;
}
...
select(vh, ACTION_STATE_SWIPE);
return true;
}
findSwipedView(motionEvent) 根据手指坐标,找到对应的 item,然后返回对应的 ViewHolder;是否允许横滑,看看 mCallback.getAbsoluteMovementFlags(mRecyclerView, vh) 代码
final int getAbsoluteMovementFlags(RecyclerView recyclerView, ViewHolder viewHolder) {
final int flags = getMovementFlags(recyclerView, viewHolder);
return convertToAbsoluteDirection(flags, ViewCompat.getLayoutDirection(recyclerView));
}
最终通过 getMovementFlags() 方法返回值,通过位运算来决定,就是上一章中重写的方法,我们可以自定义滑动允许的方向;然后就是计算滑动的距离,看是否满足滑动的要求,最终一切符合条件,调用 select() 方法,传入 ACTION_STATE_SWIPE 值。 select() 方法稍后分析,先看看 setupCallbacks() 中最后一行代码 initGestureDetector() 方法
private void initGestureDetector() {
if (mGestureDetector != null) {
return;
}
mGestureDetector = new GestureDetectorCompat(mRecyclerView.getContext(),
new ItemTouchHelperGestureListener());
}
private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener {
@Override
public void onLongPress(MotionEvent e) {
View child = findChildView(e);
if (child != null) {
ViewHolder vh = mRecyclerView.getChildViewHolder(child);
if (vh != null) {
if (!mCallback.hasDragFlag(mRecyclerView, vh)) {
return;
}
int pointerId = MotionEventCompat.getPointerId(e, 0);
if (pointerId == mActivePointerId) {
...
if (mCallback.isLongPressDragEnabled()) {
select(vh, ACTION_STATE_DRAG);
}
}
}
}
}
}
这里是个手势事件,长按时生效,根据手指按下坐标找到 item 及对应的 ViewHolder,然后根据mCallback.hasDragFlag(mRecyclerView, vh) 判断是否允许拖拽,紧接着判断位移量,如果符合条件调用 select(vh, ACTION_STATE_DRAG) 方法。通过上面的代码,我们知道不论是item滑动或是拖动,有几个限制,一个是 ItemTouchHelper.Callback 中的 getMovementFlags() 方法,控制横滑和拖拽的方向;另一个是 isLongPressDragEnabled() 和 isItemViewSwipeEnabled() 方法,必须返回 true 时才会最终允许拖拽和滑动。 item 滑动或拖拽或还原,都是调用 select() 方法,出入 ACTION_STATE_IDLE、ACTION_STATE_SWIPE、ACTION_STATE_DRAG 值。
private void select(ViewHolder selected, int actionState) {
if (selected == mSelected && actionState == mActionState) {
return;
}
...
if (mSelected != null) {
...
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;
}
if (swipeDir <= 0) {
mCallback.clearView(mRecyclerView, prevSelected);
} else {
mPendingCleanup.add(prevSelected.itemView);
mIsPendingCleanup = true;
if (swipeDir > 0) {
postDispatchSwipe(this, swipeDir);
}
}
...
}
};
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;
}
...
mRecyclerView.invalidate();
}
此方法调用时,最终会回调 mCallback.onSelectedChanged() 方法,我们可以在这里做一些UI操作。 item 横滑,手指松开时,会使用 RecoverAnimation 这个对象;如果是拖拽,swipeDir 值为0,如果是横滑,且满足了移除的条件,默认是横滑的距离大于item宽度的一半,则 swipeDir 值大于0,否则也为0。如果为0调用 mCallback.clearView() 方法,可以在里面还原UI操作。如果 swipeDir 大于0,则会执行 postDispatchSwipe() 方法,最终执行 mCallback.onSwiped() 回调,我们肯可以在这个里面做数据删除操作。
swip 和 drag 的过程在哪呢?还得看 onTouchEvent() 中 ACTION_MOVE 部分的代码,updateDxDy() 中更新 mDx mDy 的值;moveIfNecessary() 这个会做拖拽的判断,会不停执行,最终满足条件的话会执行 mCallback.onMove() 回调,我们可以在里面做集合中数据的交换及刷新UI。至于item的横滑,记得 onDraw() 方法中有个获取位移的方法 getSelectedDxDy()
private void getSelectedDxDy(float[] outPosition) {
if ((mSelectedFlags & (LEFT | RIGHT)) != 0) {
outPosition[0] = mSelectedStartX + mDx - mSelected.itemView.getLeft();
} else {
outPosition[0] = ViewCompat.getTranslationX(mSelected.itemView);
}
if ((mSelectedFlags & (UP | DOWN)) != 0) {
outPosition[1] = mSelectedStartY + mDy - mSelected.itemView.getTop();
} else {
outPosition[1] = ViewCompat.getTranslationY(mSelected.itemView);
}
}
这里面会去计算偏移量,然后通过 ItemTouchUIUtilImpl 对象中方法去位移。我们在调用 CallBack 中 onDraw() 方法,也会用到 RecoverAnimation
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();
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);
}
}
mRecoverAnimations 这个集合是在 select() 中添加对象,所以在 onDraw() 中就有 RecoverAnimation 对象了,但这个执行是手指头松开后才会执行。
最后稍微分析一下 RecoverAnimation 这个类。看看它的构造方法
public RecoverAnimation(ViewHolder viewHolder, int animationType,
int actionState, float startDx, float startDy, float targetX, float targetY) {
...
mValueAnimator = AnimatorCompatHelper.emptyValueAnimator();
mValueAnimator.addUpdateListener(
new AnimatorUpdateListenerCompat() {
@Override
public void onAnimationUpdate(ValueAnimatorCompat animation) {
setFraction(animation.getAnimatedFraction());
}
});
mValueAnimator.setTarget(viewHolder.itemView);
mValueAnimator.addListener(this);
setFraction(0f);
}
重点关注 mValueAnimator = AnimatorCompatHelper.emptyValueAnimator() 这行代码,
public final class AnimatorCompatHelper {
private final static AnimatorProvider IMPL;
static {
if (Build.VERSION.SDK_INT >= 12) {
IMPL = new HoneycombMr1AnimatorCompatProvider();
} else {
IMPL = new DonutAnimatorCompatProvider();
}
}
public static ValueAnimatorCompat emptyValueAnimator() {
return IMPL.emptyValueAnimator();
}
private AnimatorCompatHelper() {}
public static void clearInterpolator(View view) {
IMPL.clearInterpolator(view);
}
}
原来是个类似工厂模式,做了版本兼容,看返回的对象类型是 ValueAnimatorCompat ,说明两个类都实现了 AnimatorProvider 接口,都先看高版本的,
class HoneycombMr1AnimatorCompatProvider implements AnimatorProvider {
private TimeInterpolator mDefaultInterpolator;
@Override
public ValueAnimatorCompat emptyValueAnimator() {
return new HoneycombValueAnimatorCompat(ValueAnimator.ofFloat(0f, 1f));
}
static class HoneycombValueAnimatorCompat implements ValueAnimatorCompat {
final Animator mWrapped;
public HoneycombValueAnimatorCompat(Animator wrapped) {
mWrapped = wrapped;
}
...
@Override
public void addUpdateListener(final AnimatorUpdateListenerCompat animatorUpdateListener) {
if (mWrapped instanceof ValueAnimator) {
((ValueAnimator) mWrapped).addUpdateListener(
new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
animatorUpdateListener
.onAnimationUpdate(HoneycombValueAnimatorCompat.this);
}
});
}
}
}
static class AnimatorListenerCompatWrapper implements Animator.AnimatorListener {
public AnimatorListenerCompatWrapper(
AnimatorListenerCompat wrapped, ValueAnimatorCompat valueAnimatorCompat) {
mWrapped = wrapped;
mValueAnimatorCompat = valueAnimatorCompat;
}
...
}
}
这个明显是个包装类,把 AnimatorListenerCompat 包装到 AnimatorListenerCompatWrapper 里面掉用,用 HoneycombValueAnimatorCompat 把 ValueAnimator 封装了一层,通过 ValueAnimator 的从 0 到 1 的变化,监听变化的方法 addUpdateListener() 回调中,调用 AnimatorUpdateListenerCompat 的 onAnimationUpdate() 方法,至于
AnimatorUpdateListenerCompat 回调,就是RecoverAnimation构造方法中的回调,animation.getAnimatedFraction() 获取的实际上是 ValueAnimator.getAnimatedFraction(),这个值可以理解为进度条,从0开始,到1结束。所以 RecoverAnimation 构造中的 addUpdateListener() 方法就是实时更新 setFraction() 中 mFraction 的值,此时看
public void update() {
if (mStartDx == mTargetX) {
mX = ViewCompat.getTranslationX(mViewHolder.itemView);
} else {
mX = mStartDx + mFraction * (mTargetX - mStartDx);
}
if (mStartDy == mTargetY) {
mY = ViewCompat.getTranslationY(mViewHolder.itemView);
} else {
mY = mStartDy + mFraction * (mTargetY - mStartDy);
}
}
如果开始和结束值不一样,则计算公式是 开始值 + (差值 * 进度),这一看就是匀速的变化,功能类似插值器 LinearInterpolator;当取消动画时 setFraction(1f)。再分析分析 DonutAnimatorCompatProvider 这个类,由于 11 之前系统源码中没有 ValueAnimator 这个类,所以在这里用 Handler + Runnable 来实现相同效果,
class DonutAnimatorCompatProvider implements AnimatorProvider {
@Override
public ValueAnimatorCompat emptyValueAnimator() {
return new DonutFloatValueAnimator();
}
private static class DonutFloatValueAnimator implements ValueAnimatorCompat {
...
private Runnable mLoopRunnable = new Runnable() {
@Override
public void run() {
long dt = getTime() - mStartTime;
float fraction = dt * 1f / mDuration;
if (fraction > 1f || mTarget.getParent() == null) {
fraction = 1f;
}
mFraction = fraction;
notifyUpdateListeners();
if (mFraction >= 1f) {
dispatchEnd();
} else {
mTarget.postDelayed(mLoopRunnable, 16);
}
}
};
@Override
public void start() {
if (mStarted) {
return;
}
mStarted = true;
dispatchStart();
mFraction = 0f;
mStartTime = getTime();
mTarget.postDelayed(mLoopRunnable, 16);
}
...
}
}
这里是封装了各种监听,比如 start() 方法中,触发 dispatchStart() 方法,触发开始监听,初始化 mFraction 为0,mStartTime 记录当前时间,然后用 View 的 postDelayed() 方法,延迟 16 毫米执行,它的原理还是借助 Handler 来执行,看看 mLoopRunnable 这里面逻辑:获取当前距离开始的时间差 dt,计算出进度值 fraction,调用 notifyUpdateListeners()方法,在这里里面遍历 AnimatorUpdateListenerCompat 监听即可,回调 onAnimationUpdate() 方法,进度条没超过1时,继续执行 mTarget.postDelayed(mLoopRunnable, 16) 方法,循环执行;到 1 时,执行 dispatchEnd() 方法,动画结束回调。