ItemTouchHelper是v7包RecyclerView的ItemDecoration接口的一個實現,其前身是v4包的ViewDragHelper(可在任意ViewGroup中使用)在DrawerLayout、SlidingPaneLayout源碼中都有應用。
兩者使用方法也很類似,都是實現各自的Callback
ItemTouchHelper手勢的實現都在匿名內部類mOnItemTouchListener中實現,
private final OnItemTouchListener mOnItemTouchListener
= new OnItemTouchListener()
與RecyclerView的綁定,則在attachToRecyclerView方法中的setupCallbacks()中
add到RV中
/**
* Attaches the ItemTouchHelper to the provided RecyclerView. If TouchHelper is already
* attached to a RecyclerView, it will first detach from the previous one. You can call this
* method with {@code null} to detach it from the current RecyclerView.
*
* @param recyclerView The RecyclerView instance to which you want to add this helper or
* {@code null} if you want to remove ItemTouchHelper from the current
* 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(); //初始化 setup
}
}
private void setupCallbacks() {
ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());
mSlop = vc.getScaledTouchSlop(); //初始化TouchSlop
mRecyclerView.addItemDecoration(this); //綁定Callback.OnDraw供實現onChildDraw
mRecyclerView.addOnItemTouchListener(mOnItemTouchListener); //綁定手勢監聽
mRecyclerView.addOnChildAttachStateChangeListener(this);
initGestureDetector();
}
private void destroyCallbacks() {
mRecyclerView.removeItemDecoration(this); //避免重複綁定
mRecyclerView.removeOnItemTouchListener(mOnItemTouchListener); //避免重複綁定
mRecyclerView.removeOnChildAttachStateChangeListener(this); //避免重複綁定
// clean all attached
final int recoverAnimSize = mRecoverAnimations.size();
for (int i = recoverAnimSize - 1; i >= 0; i--) {
final RecoverAnimation recoverAnimation = mRecoverAnimations.get(0);
mCallback.clearView(mRecyclerView, recoverAnimation.mViewHolder);
}
mRecoverAnimations.clear();
mOverdrawChild = null;
mOverdrawChildPosition = -1;
releaseVelocityTracker();
}
進入正題
嚴格的說,ItemTouchHelper的手勢正式接管方法是select()方法,而通過手勢開始(達到某種條件)而進入select的入口總共3個
1、長按
2、與rv垂直方向的swipe移動(超過touchSlop的Move)
3、按下一個正在做恢復動畫的vh(對RecoverAnimation沒做完的vh觸發Down事件)
既然是手勢接管,毫無疑問,這些入口的入口一定來自mOnItemTouchListener的OnInterceptTouchEvent方法
@Override
public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent event) {
mGestureDetector.onTouchEvent(event);//主要實現onLongPress的監聽
這裏就是入口1,下有對於ItemTouchHelperGestureListener的onLongPress具體分析 //如果是個長按 那麼攔截短路掉RV自己的滑動
//並判定Down選中的vh ItemTouchHelper正式接管
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 = event.getPointerId(0);
下面記錄down的位置,注意這裏是DOWN的時候無條件記錄!!!因爲接管手勢是未來的事,之後的位移比對等等都來自這個初始記錄值
長按的時候這個初始值還會被替換,(因爲長按的起始是長按600ms而不是DOWN)
如果這裏代碼沒有被執行 startDrag startSwipe等方法在外部調用是無法被正確工作的!
startDrag等方法工作前提的說明見我的另一篇點擊打開鏈接
mInitialTouchX = event.getX();
mInitialTouchY = event.getY();
obtainVelocityTracker();
if (mSelected == null) {
final RecoverAnimation animation = findAnimation(event);
下面是入口3,一個並不常見的場景 if (animation != null) { //特別注意!!! 這裏並不是一個主流程的手勢接管入口
//該段代碼僅處理DOWN按下一個正在做恢復動畫的vh的場景!
//千萬不要誤解,通常DOWN的時候是沒有恢復動畫的,所以mSelected不會被賦值
//intercept也就不會成功,後面的手勢OnItemTouchListener會正常執行onIntercepter方法
//若都返回false 也會傳遞到vh的itemView的DispatchTouchEvent裏
mInitialTouchX -= animation.mX;
mInitialTouchY -= animation.mY;
endRecoverAnimation(animation.mViewHolder, true); //肯定把正在進行的恢復動畫先幹掉咯
if (mPendingCleanup.remove(animation.mViewHolder.itemView)) {
//mPendingCleanup 存儲動畫之後就應該detach並被cleanup的Views
mCallback.clearView(mRecyclerView, animation.mViewHolder); //如果正在被移除 恢復vh
}
select(animation.mViewHolder, animation.mActionState); //調用select 接管!
updateDxDy(event, mSelectedFlags, 0);
}
}
}
處理cancel與擡起
else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
mActivePointerId = ACTIVE_POINTER_ID_NONE; //如果是cancel或up 那麼select(null)
select(null, ACTION_STATE_IDLE);
}
再下面就是Swipe的入口了 入口2 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 = event.findPointerIndex(mActivePointerId);
if (DEBUG) {
Log.d(TAG, "pointer index " + index);
}
if (index >= 0) { //檢驗PointerId合法性 不是ACTIVE_POINTER_ID_NONE -1
checkSelectForSwipe(action, event, index); //Swipe的入口
//前提:(沒有接管過mSelect爲空、必須是Move事件、支持swipe、拖動超過Slop且與Rv方向垂直)
}
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(event);
}
return mSelected != null; //特別特別注意: mSelected是否爲空決定是否接管!!!(select方法是否調用)
}
onIntercepterTouchEvent 到此結束 注意它的返回值
下面來看一下入口1和入口2的具體實現
長按手勢的入口ItemTouchHelperGestureListener, 也就是GestureDetector的回調實現。
private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener {
ItemTouchHelperGestureListener() {
}
@Override
public boolean onDown(MotionEvent e) {
return true;
}
@Override
public void onLongPress(MotionEvent e) {
View child = findChildView(e);
//先找點了誰(vh) 點到花花草草(divider分割線之類)可不作數哦
if (child != null) {
ViewHolder vh = mRecyclerView.getChildViewHolder(child);
if (vh != null) {
//注意 這個hasDragFlag方法和hasSwipeFlag一樣調用public可重寫的getAbsoluteMovementFlags方法
//入參有vh 也就是說可以像checkSelectForSwipe定製swipe方向一樣
//定製各個vh自己的move方向
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()) {//最關鍵的校驗 如果你通過重寫這方法
//禁用了LongPressDragEnabled HOHO 那它肯定也就不拖動了
select(vh, ACTION_STATE_DRAG);
}
}
}
}
}
}
接下來看Move事件Swipe的入口 /**
* Checks whether we should select a View for swiping.
*/
boolean checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) {
if (mSelected != null || action != MotionEvent.ACTION_MOVE
|| mActionState == ACTION_STATE_DRAG || !mCallback.isItemViewSwipeEnabled()) {
return false;
}
if (mRecyclerView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) {
return false;
}
final ViewHolder vh = findSwipedView(motionEvent);//檢驗手勢目前是否符合Swipe 下有分析
if (vh == null) {
return false;
}
//之前校驗了一堆 如果已經有mSelected 那麼不用再校驗了
//校驗只檢驗Move事件 且itemTouchHelper必須是拖動狀態
//Callback必須支持Swipe isItemViewSwipeEnabled沒有被重寫flase
//如果RecyclerView已經處理了拖動狀態————對不起也不行
//如果點擊的地方沒有viewHoler不行(比如點到了divider?)
final int movementFlags = mCallback.getAbsoluteMovementFlags(mRecyclerView, vh);
//獲取支持的swipe方向,注意!這裏getAbsoluteMovementFlags入參有vh,也就是說你
//可以定製每一個vh的swipe或者move方向(見OnLongPress)
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 = motionEvent.getX(pointerIndex);
final float y = motionEvent.getY(pointerIndex);
// Calculate the distance moved
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) {
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;
}
}
mDx = mDy = 0f;
mActivePointerId = motionEvent.getPointerId(0);
select(vh, ACTION_STATE_SWIPE);
return true;
}
檢驗這個手勢是否是swipe一個ViewHolder
private ViewHolder findSwipedView(MotionEvent motionEvent) {
final RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager();
if (mActivePointerId == ACTIVE_POINTER_ID_NONE) { //再次校驗pointerId是否有效
return null;
}
final int pointerIndex = motionEvent.findPointerIndex(mActivePointerId);
final float dx = motionEvent.getX(pointerIndex) - mInitialTouchX;
final float dy = motionEvent.getY(pointerIndex) - mInitialTouchY;
final float absDx = Math.abs(dx);
final float absDy = Math.abs(dy);
if (absDx < mSlop && absDy < mSlop) { //判斷是否超過TouchSlop
return null;
}
if (absDx > absDy && lm.canScrollHorizontally()) { //如果RecyclerView滑動的同一個方向
//的位移更大,那麼這應該交給RV處理,這裏不是個swipe
return null;
} else if (absDy > absDx && lm.canScrollVertically()) {
return null;
}
View child = findChildView(motionEvent);
if (child == null) {
return null;
}
return mRecyclerView.getChildViewHolder(child);
}
下面分析一下最重要的接管方法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) { //如果原來有接管目標vh的 那麼那個vh要壽終正寢 完成後事動畫
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) { //如果原來有接管對象vh的
animationType = ANIMATION_TYPE_DRAG; //拖動的變狀態爲拖動動畫
} else if (swipeDir > 0) {
animationType = ANIMATION_TYPE_SWIPE_SUCCESS; //swipe的變狀態爲swipe動畫
} 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
//如果是個拖拉(onMove)或者沒有過閾值的swipe 是沒有動畫的
//立即恢復
mCallback.clearView(mRecyclerView, prevSelected);
// full cleanup will happen on onDrawOver
} else {
// wait until remove animation is complete.
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.
//動畫可能在佈局過程中由其他animators完成。
//所以這裏用遞歸判斷isRunning!!!,以避免在佈局期間編輯適配器。
//保證onSwipe的回調(特別注意:動畫結束不意味着mRecoverAnimations裏
//對應的vh被清除! 依賴於onSwipe裏移除對應數據而移除vh之後執行
//onChildViewDetachedFromWindow的回調裏endRecoverAnimation()移除)
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的getAnimationDuration方法可以重寫
//自定義動畫時間
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 = 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) { //如果select有值 那麼Disallow攔截 這個號理解
rvParent.requestDisallowInterceptTouchEvent(mSelected != null);
}
if (!preventLayout) {
mRecyclerView.getLayoutManager().requestSimpleAnimationsInNextLayout();
}
mCallback.onSelectedChanged(mSelected, mActionState); //注意 這裏接管和放開接管都有CallBack回調
//可以重寫的!!!
mRecyclerView.invalidate();
}