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.csdn.net/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);
}
}
}
});
}