ItemTouchHelper手势的实现都在匿名内部类mOnItemTouchListener中实现,
private final OnItemTouchListener mOnItemTouchListener
= new OnItemTouchListener()
onInterceptTouchEvent在mSelect不为空的时候(找到对应的接管vh,并执行过select方法)会返回true 拦截处理所有TouchEvent 分析见手势入口分析
@Override
public void onTouchEvent(RecyclerView recyclerView, MotionEvent event) {
mGestureDetector.onTouchEvent(event); //主要实现onLongPress的监听
//监听实现在内部类ItemTouchHelperGestureListener 判定长按选中的vh
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); //取action
final int activePointerIndex = event.findPointerIndex(mActivePointerId);
if (activePointerIndex >= 0) { //若pointerId合法不为-1
checkSelectForSwipe(action, event, activePointerIndex); //检查是否是一个Swipe手势 具体分析见前一篇ItemTouchHelper手势入口的分析
}
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) {
updateDxDy(event, mSelectedFlags, activePointerIndex); //更新距离down的触摸点滚动偏移
//mInitialTouchX mInitialTouchY在onInterceptTouchEvent down的时候
//与监听实现在内部类ItemTouchHelperGestureListener的onLongPress监听的时候赋值
moveIfNecessary(viewHolder); //判断是否需要回调onMove onSwipe
mRecyclerView.removeCallbacks(mScrollRunnable); //mScrollRunnable递归调用屏幕边缘拖拽处理 另有blog讲解
mScrollRunnable.run();
mRecyclerView.invalidate(); //执行RecyclerView的onDraw()方法
}
break;
}
case MotionEvent.ACTION_CANCEL:
if (mVelocityTracker != null) {
mVelocityTracker.clear();
}
// fall through
case MotionEvent.ACTION_UP:
select(null, ACTION_STATE_IDLE); //关键方法 传入null
mActivePointerId = ACTIVE_POINTER_ID_NONE;
break;
case MotionEvent.ACTION_POINTER_UP: {
final int pointerIndex = MotionEventCompat.getActionIndex(event);
final int pointerId = event.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mActivePointerId = event.getPointerId(newPointerIndex);
updateDxDy(event, mSelectedFlags, pointerIndex);
}
break;
}
}
}
长按的监听
private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener {
ItemTouchHelperGestureListener() {
}
@Override
public boolean onDown(MotionEvent e) {
return true;
}
@Override
public void onLongPress(MotionEvent e) { //该方法回调前提可在http://blog.youkuaiyun.com/sonic_storm/article/details/74452615 里查看
View child = findChildView(e);
if (child != null) {
ViewHolder vh = mRecyclerView.getChildViewHolder(child);
if (vh != null) {
if (!mCallback.hasDragFlag(mRecyclerView, vh)) {
return;
}
int pointerId = e.getPointerId(0);
// Long press is deferred.
// Check w/ active pointer id to avoid selecting after motion
// event is canceled.
if (pointerId == mActivePointerId) {
final int index = e.findPointerIndex(mActivePointerId);
final float x = e.getX(index);
final float y = e.getY(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);
}
}
}
}
}
}
我们先来看与Action Move相关的
updateDxDy方法更新距离down的触摸点滚动偏移 实现很简单 记录在mDx mDy
void updateDxDy(MotionEvent ev, int directionFlags, int pointerIndex) {
final float x = ev.getX(pointerIndex);
final float y = ev.getY(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);
}
}
moveIfNecessary这个方法判断是否需要回调onMove
/**
* Checks if we should swap w/ another view holder.
*/
void moveIfNecessary(ViewHolder viewHolder) {
if (mRecyclerView.isLayoutRequested()) {
return;
}
if (mActionState != ACTION_STATE_DRAG) {
return;
}
final float threshold = mCallback.getMoveThreshold(viewHolder);
final int x = (int) (mSelectedStartX + mDx);
final int y = (int) (mSelectedStartY + mDy);
if (Math.abs(y - viewHolder.itemView.getTop()) < viewHolder.itemView.getHeight() * threshold
&& Math.abs(x - viewHolder.itemView.getLeft())
< viewHolder.itemView.getWidth() * threshold) {
return;
}
List<ViewHolder> swapTargets = findSwapTargets(viewHolder);
if (swapTargets.size() == 0) {
return;
}
// may swap.
ViewHolder target = mCallback.chooseDropTarget(viewHolder, swapTargets, x, y);
if (target == null) {
mSwapTargets.clear();
mDistances.clear();
return;
}
final int toPosition = target.getAdapterPosition();
final int fromPosition = viewHolder.getAdapterPosition();
if (mCallback.onMove(mRecyclerView, viewHolder, target)) {
// keep target visible
mCallback.onMoved(mRecyclerView, viewHolder, fromPosition,
target, toPosition, x, y);
}
}
move的时候最终会调用RecyclerView的invalidate方法唤起绘制
执行onDraw(canvas)方法:多个itemDecoration会根据各自的实现依次绘制画布
@Override
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);
}
}
这里注意invalidate的调用是在onTouchEvent里,我们知道一旦有一个OnItemTouchListener的onInterceptTouchEvent返回true,那么只有这个listener的onTouchEvent会被调用,保证了不会多次绘制(若在onInterceptTouchEvent)里就可能多次调用了
ItemTouchHelper也是一个ItemDecoration,绑定在上面的setup方法里
那么看一下它的实现算法
@Override
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 dy
dx = mTmpPosition[0];
dy = mTmpPosition[1];
}
mCallback.onDraw(c, parent, mSelected,
mRecoverAnimations, mActionState, dx, dy); //回调交给CallBack完成
来看callBack里的onDraw是有缺省实现滴,也就是super.onDraw()就是下面这个代码了 我们可以重写它
void onDraw(Canvas c, RecyclerView parent, ViewHolder selected,
List<ItemTouchHelper.RecoverAnimation> recoverAnimationList,
int actionState, float dX, float dY) {
final int recoverAnimSize = recoverAnimationList.size();
for (int i = 0; i < recoverAnimSize; i++) {
final ItemTouchHelper.RecoverAnimation anim = recoverAnimationList.get(i);
anim.update();
final int count = c.save();
onChildDraw(c, parent, anim.mViewHolder, anim.mX, anim.mY, anim.mActionState,
false); //调用onChildDraw
c.restoreToCount(count);
}
if (selected != null) {
final int count = c.save();
onChildDraw(c, parent, selected, dX, dY, actionState, true);
c.restoreToCount(count);
}
}
google推荐你重写这个方法修改你的UI响应
默认的实现是dx dy移动多少childView就横移多少
由于item是一条一条绘制的,所以这个类还关注拖动的时候,谁先画谁后画
这取决于android.view.ViewGroup#getChildDrawingOrder方法 你可以修改它
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);
}
private static final ItemTouchUIUtil sUICallback; 的实现是ItemTouchUIUtilImpl类里做的兼容实现
就看一下最新的5.0的实现吧
class ItemTouchUIUtilImpl {
static class Lollipop extends Honeycomb {
@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);
}
private float findMaxElevation(RecyclerView recyclerView, View itemView) {
final int childCount = recyclerView.getChildCount();
float max = 0;
for (int i = 0; i < childCount; i++) {
final View child = recyclerView.getChildAt(i);
if (child == itemView) {
continue;
}
final float elevation = ViewCompat.getElevation(child);
if (elevation > max) {
max = elevation;
}
}
return max;
}
实际上5.0的onDraw调用的是他的父类也就是更低版本的onDraw方法的实现
@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);
}
也就是设置了横移属性咯
更老的版本没有属性动画,那只好挪挪画布来实现咯
static class Gingerbread implements ItemTouchUIUtil {
private void draw(Canvas c, RecyclerView parent, View view,
float dX, float dY) {
c.save();
c.translate(dX, dY);
parent.drawChild(c, view, 0);
c.restore();
}
再来看Action Up处理的核心方法
select
/**
* Starts dragging or swiping the given View. Call with null if you want to clear it.
*
* @param selected The ViewHolder to drag or swipe. Can be null if you want to cancel the
* current action
* @param actionState The type of action
*/
void select(ViewHolder selected, int actionState) {
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) {
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:
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;
}
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) {
// this is a drag or failed swipe. recover immediately //这是一个没成功的移除或者就只是个拖动 回复就好
mCallback.clearView(mRecyclerView, prevSelected); //调用CallBack的clearView 缺省实现也在ItemTouchUIUtil里 展示原itemView
// full cleanup will happen on onDrawOver
} else {
// wait until remove animation is complete. //动画结束就swipe
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); //CallBack的Duration是在这里设置的,可自定义 重写
mRecoverAnimations.add(rv); //记录在list里
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 = selected.itemView.getLeft();
mSelectedStartY = selected.itemView.getTop();
mSelected = selected;
if (actionState == ACTION_STATE_DRAG) {
mSelected.itemView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
}
}
final ViewParent rvParent = mRecyclerView.getParent();
if (rvParent != null) {
rvParent.requestDisallowInterceptTouchEvent(mSelected != null);
}
if (!preventLayout) {
mRecyclerView.getLayoutManager().requestSimpleAnimationsInNextLayout();
}
mCallback.onSelectedChanged(mSelected, mActionState);
mRecyclerView.invalidate();
}
RecoverAnimation implements AnimatorListenerCompat 私有内部类 封装动画
看postDispatchSwipe的实现 递归调用runnable 确保在动画移除的时候必定回调onSwipe
注意 这里并没有移除mRecoverAnimations里的vh
所以用户必须在onSwipe里实现对对应vh的移除
onChildViewDetach回调里会将对应vh的view在mRecoverAnimations里的记录移除
void postDispatchSwipe(final RecoverAnimation anim, final int swipeDir) {
// wait until animations are complete.
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.
if ((animator == null || !animator.isRunning(null))
&& !hasRunningRecoverAnim()) {
mCallback.onSwiped(anim.mViewHolder, swipeDir); //CallBack的回调 移除回调
} else {
mRecyclerView.post(this);
}
}
}
});
}