??今天我們來(lái)學(xué)習(xí)一下RecyclerView另一個(gè)鮮為人知的輔助類--ItemTouchHelper。我們?cè)谧隽斜硪晥D,就比如說(shuō),ListView或者RecyclerView,通常會(huì)有兩種需求:1. 側(cè)滑刪除;2. 拖動(dòng)交換位置。對(duì)于第一種需求使用傳統(tǒng)的版本實(shí)現(xiàn)還比較簡(jiǎn)單,我們可以自定義ItemView來(lái)實(shí)現(xiàn);而第二種的話,可能就稍微有一點(diǎn)復(fù)雜,可能需要重寫LayoutManager。
??這些辦法也不否認(rèn)是有效的解決方案,但是是否是簡(jiǎn)單和低耦合性的辦法呢?當(dāng)然不是,踩過(guò)坑的同學(xué)應(yīng)該都知道,不管是自定義View還是自定義LayoutManager都不是一件簡(jiǎn)單的事情,其次,自定義ItemView導(dǎo)致Adapter的通用性降低。這些實(shí)現(xiàn)方式都是比較麻煩的。
??而谷歌爸爸真是貼心,知道我們都有這種需求,就小手一抖,隨便幫我們實(shí)現(xiàn)了一個(gè)Helper類,來(lái)減輕我們的工作量。這就是ItemTouchHelper的作用。
??本文打算從兩個(gè)方面來(lái)教大家認(rèn)識(shí)ItemTouchHelper類:
ItemTouchHelper的基本使用ItemTouchHelper的源碼分析
??本文參考資料:
1. 概述
??在正式介紹ItemTouchHelper之前,我們先來(lái)了解ItemTouchHelper是什么東西。
??從ItemTouchHelper的源碼中,我們可以看出來(lái),ItemTouchHelper繼承了ItemDecoration,根本上就是一個(gè)ItemDecoration。關(guān)于ItemDecoration的分析,有興趣的同學(xué)可以參考我的文章:RecyclerView 擴(kuò)展(一) - 手把手教你認(rèn)識(shí)ItemDecoration。
public class ItemTouchHelper extends RecyclerView.ItemDecoration
implements RecyclerView.OnChildAttachStateChangeListener {
}
??至于為什么ItemTouchHelper會(huì)繼承ItemDecoration,后面會(huì)詳細(xì)的解釋,這里就先賣一下關(guān)子。
??然后,我們先來(lái)看看ItemTouchHelper實(shí)現(xiàn)的效果,讓大家有一個(gè)直觀的體驗(yàn)。
??先是側(cè)滑刪除的效果:

??然后是拖動(dòng)交換位置:

??本文打算從上面兩種效果來(lái)介紹
ItemTouchHelper的使用。
2. ItemTouchHelper的基本使用
??既然是手把手教大家認(rèn)識(shí)ItemTouchHelper,所以自然需要介紹它的的基本使用,現(xiàn)在讓我們來(lái)看看究竟怎么使用ItemTouchHelper。
??在正式介紹ItemTouchHelper的基本使用之前,我們還必須了解一個(gè)類--ItemTouchHelper.Callback。ItemTouchHelper就是依靠這個(gè)類來(lái)實(shí)現(xiàn)側(cè)滑刪除和拖動(dòng)位置兩種效果的,我來(lái)看看它。
(1). ItemTouchHelper.Callback
??我們?cè)谑褂?code>ItemTouchHelper時(shí),必須自定義一個(gè)ItemTouchHelper.Callback,我們來(lái)了解一下其中比較重要的幾個(gè)方法。
| 方法名 | 作用 |
|---|---|
| getMovementFlags | 在此方法里面我們需要構(gòu)建兩個(gè)flag,一個(gè)是dragFlags,表示拖動(dòng)效果支持的方向,另一個(gè)是swipeFlags,表示側(cè)滑效果支持的方向。在我們的Demo中,拖動(dòng)執(zhí)行上下兩個(gè)方向,側(cè)滑執(zhí)行左右兩個(gè)方向,這些操作我們都可以在此方法里面定義。 |
| onMove | 當(dāng)拖動(dòng)效果已經(jīng)產(chǎn)生了,會(huì)回調(diào)此方法。在此方法里面,我們通常會(huì)更新數(shù)據(jù)源,就比如說(shuō),一個(gè)ItemView從0拖到了1位置,那么對(duì)應(yīng)的數(shù)據(jù)源也需要更改位置。 |
| onSwiped | 當(dāng)側(cè)滑效果以上產(chǎn)生了,會(huì)回調(diào)此方法。在此方法里面,我們也會(huì)更新數(shù)據(jù)源。與onMove方法不同到的是,我們?cè)谶@個(gè)方法里面從數(shù)據(jù)源里面移除相應(yīng)的數(shù)據(jù),然后調(diào)用notifyXXX方法就行了。 |
??對(duì)于ItemTouchHelper的基本使用來(lái)說(shuō),我們只需要了解這三個(gè)方法就已經(jīng)OK了。接下來(lái),我將正式介紹ItemTouchHelper的基本使用。
(2). 基本使用
??首先,我們需要自定義一個(gè)ItemTouchHelper.Callback,如下:
public class CustomItemTouchCallback extends ItemTouchHelper.Callback {
private final ItemTouchStatus mItemTouchStatus;
public CustomItemTouchCallback(ItemTouchStatus itemTouchStatus) {
mItemTouchStatus = itemTouchStatus;
}
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
// 上下拖動(dòng)
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
// 向左滑動(dòng)
int swipeFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
return makeMovementFlags(dragFlags, swipeFlags);
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
// 交換在數(shù)據(jù)源中相應(yīng)數(shù)據(jù)源的位置
return mItemTouchStatus.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
// 從數(shù)據(jù)源中移除相應(yīng)的數(shù)據(jù)
mItemTouchStatus.onItemRemove(viewHolder.getAdapterPosition());
}
}
??然后,我們?cè)谑褂?code>RecyclerView時(shí),添加這兩行代碼就行了:
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new CustomItemTouchCallback(mAdapter));
itemTouchHelper.attachToRecyclerView(mRecyclerView);
??最終的效果就是上面的動(dòng)圖展示的,是不是覺得非常的簡(jiǎn)單呢?接下來(lái),我將正式的分析ItemTouchHelper的源碼。
(4).源碼
??為了方便大家理解,我將我的代碼上傳到github,有興趣的同學(xué)可以看看:ItemTouchHelperDemo。
3. ItemTouchHelper的源碼分析
??我們從基本使用中了解到,ItemTouchHelper的使用是非常簡(jiǎn)單的,所以大家內(nèi)心有沒有一種好奇呢?那就是ItemTouchHelper究竟是怎么實(shí)現(xiàn),為什么兩個(gè)相對(duì)比較復(fù)雜的效果,通過(guò)幾行代碼就能實(shí)現(xiàn)呢?接下來(lái)的內(nèi)容就能找到答案。
(1). attachToRecyclerView方法
??我們都知道,ItemTouchHelper的入口方法就是attachToRecyclerView方法,接下來(lái),我們先來(lái)看看這個(gè)方法為我們做了哪些事情。
public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
if (mRecyclerView == recyclerView) {
return; // nothing to do
}
if (mRecyclerView != null) {
destroyCallbacks();
}
mRecyclerView = recyclerView;
if (recyclerView != 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();
}
}
private void setupCallbacks() {
ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());
mSlop = vc.getScaledTouchSlop();
mRecyclerView.addItemDecoration(this);
mRecyclerView.addOnItemTouchListener(mOnItemTouchListener);
mRecyclerView.addOnChildAttachStateChangeListener(this);
startGestureDetection();
}
private void startGestureDetection() {
mItemTouchHelperGestureListener = new ItemTouchHelperGestureListener();
mGestureDetector = new GestureDetectorCompat(mRecyclerView.getContext(),
mItemTouchHelperGestureListener);
}
??相對(duì)來(lái)說(shuō),attachToRecyclerView方法是比較簡(jiǎn)單的。這其中,我們發(fā)現(xiàn)ItemTouchHelper是通過(guò)ItemTouchListener接口來(lái)為每個(gè)ItemView處理事件,同時(shí),從這里我們可以看出來(lái),在ItemTouchHelper內(nèi)部還使用了GestureDetector,而這里GestureDetector的作用主要是來(lái)判斷ItemView是否進(jìn)行了長(zhǎng)按行為。
??ItemTouchHelper的分析重點(diǎn)應(yīng)該是事件處理,但是在這之前,我們先來(lái)看一個(gè)方法,這個(gè)方法非常的重要的。
(2). select方法
??當(dāng)我們的操作觸發(fā)了長(zhǎng)按或者側(cè)滑的行為,都會(huì)回調(diào)此方法,同時(shí)當(dāng)我們手勢(shì)釋放,也會(huì)回調(diào)此方法。
??所以從大的時(shí)機(jī)來(lái)看,當(dāng)手勢(shì)開始或者釋放都會(huì)回調(diào)select方法;而每個(gè)大時(shí)機(jī)又分為兩個(gè)小時(shí)機(jī),分別是長(zhǎng)按和側(cè)滑,分別表示拖動(dòng)交換位置和側(cè)滑刪除操作。
??在正式分析select方法的代碼之前,我們需要了解兩個(gè)東西:
selected表示被選中的ViewHolder。其中,selected如果為null,則表示當(dāng)前處于手勢(shì)(包括長(zhǎng)按和側(cè)滑)釋放時(shí)機(jī);反之,selected不為null,則表示當(dāng)前處于手勢(shì)開始的時(shí)機(jī)。actionState表示當(dāng)前的狀態(tài),一共有三個(gè)值可選,分別是:1.ACTION_STATE_IDLE表示沒有任何手勢(shì),此時(shí)selected對(duì)應(yīng)的應(yīng)當(dāng)是null;2.ACTION_STATE_SWIPE表示當(dāng)前ItemView處于側(cè)滑狀態(tài);3.ACTION_STATE_DRAG表示當(dāng)前ItemView處于拖動(dòng)狀態(tài)。在ItemTouchHelper內(nèi)部,就是通過(guò)這三個(gè)狀態(tài)來(lái)判斷ItemView處于什么狀態(tài)。
??接下來(lái)我們來(lái)看看select方法的代碼:
void select(ViewHolder selected, int actionState) {
if (selected == mSelected && actionState == mActionState) {
return;
}
mDragScrollStartTimeInMs = Long.MIN_VALUE;
final int prevActionState = mActionState;
endRecoverAnimation(selected, true);
mActionState = actionState;
// 如果當(dāng)前是拖動(dòng)行為,給RecyclerView設(shè)置一個(gè)ChildDrawingOrderCallback接口
// 主要是為了調(diào)整ItemView繪制的順序
if (actionState == ACTION_STATE_DRAG) {
mOverdrawChild = selected.itemView;
addChildDrawingOrderCallback();
}
int actionStateMask = (1 << (DIRECTION_FLAG_COUNT + DIRECTION_FLAG_COUNT * actionState))
- 1;
boolean preventLayout = false;
// 1.手勢(shì)釋放
if (mSelected != null) {
// ······
}
// 2. 手勢(shì)開始
// selected不為null表示手勢(shì)開始,反之selected為null表示手勢(shì)釋放
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();
}
??從上面的代碼中,我們可以總結(jié)出來(lái)幾個(gè)結(jié)論:
- 如果處于手勢(shì)開始階段,即
selected不為null,那么會(huì)通過(guò)getAbsoluteMovementFlags方法來(lái)獲取執(zhí)行我們?cè)O(shè)置的flag,從而就知道執(zhí)行哪些行為(側(cè)滑或者拖動(dòng))和方向(上、下、左和右)。同時(shí)還會(huì)記錄下被選中ItemView的位置。簡(jiǎn)而言之,就是一些變量的初始化。- 如果處于手勢(shì)釋放階段,即
selected為null,同時(shí)mSelected不為null,那么此時(shí)需要做的事情就稍微有一點(diǎn)復(fù)雜。手勢(shì)釋放之后,需要做的事情無(wú)非有兩件:1. 相關(guān)的ItemView到正確的位置,就比如說(shuō),如果滑動(dòng)條件不滿足,那么就返回原來(lái)的位置,這個(gè)就是一個(gè)動(dòng)畫;2. 清理操作,比如說(shuō)將mSelected重置為null之類的。
(3).如何判斷一個(gè)ItemView是否被選中
??我們知道,一旦調(diào)用selected就意味著一個(gè)ItemView被選中,接下來(lái)的就會(huì)隨著手勢(shì)出現(xiàn)側(cè)滑或者拖動(dòng)的效果了。但是怎么來(lái)判斷一個(gè)ItemView是否被選中,我們從代碼來(lái)看看,我們分兩步來(lái)理解:1.側(cè)滑的選中;2. 拖動(dòng)的選中。
A. 側(cè)滑
??判斷側(cè)滑行為是否選中主要在checkSelectForSwipe方法,我們來(lái)看看checkSelectForSwipe放大的代碼:
boolean checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) {
// 如果mSelected不為null表示已經(jīng)有ItemView被選中
// 同時(shí)從這里可以看出來(lái)Callback的isItemViewSwipeEnabled方法的作用
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);
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);
// 如果flag沒有支持側(cè)滑的方向值,那么返回為false
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;
}
// 這里主要是判斷一個(gè)滑動(dòng)是否符合側(cè)滑的條件
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);
// 表示當(dāng)前ItemView被側(cè)滑行為選中
select(vh, ACTION_STATE_SWIPE);
return true;
}
??checkSelectForSwipe方法的代碼相對(duì)來(lái)說(shuō)比較長(zhǎng),但是無(wú)非就是判斷當(dāng)前ItemView是否符合側(cè)滑行為,如果到最后符合的話,那么就會(huì)調(diào)用select方法來(lái)初始化一些值。
??同時(shí),我們看一下checkSelectForSwipe方法的調(diào)用時(shí)機(jī)只有兩個(gè)地方:
onTouchEvent方法onInterceptTouchEvent方法
??調(diào)用的時(shí)機(jī)也是比較正確的,至于為什么需要兩個(gè)地方來(lái)調(diào)用這個(gè)方法,我也不太清楚,估計(jì)做什么保險(xiǎn)操作吧。
B. 拖動(dòng)選中
??拖動(dòng)選中的時(shí)機(jī)比較簡(jiǎn)單,因?yàn)橥蟿?dòng)觸發(fā)的前提是長(zhǎng)按ItemView,所以我們直接在ItemTouchHelperGestureListener的onLongPress方法找到相關(guān)代碼:
@Override
public void onLongPress(MotionEvent e) {
if (!mShouldReactToLongPress) {
return;
}
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);
}
}
}
}
}
??這段代碼表達(dá)的意思非常簡(jiǎn)單,這里我就不多余的解釋了。從這里可以看出來(lái),最終還是調(diào)用了select方法表示選中一個(gè)ItemView。
(3). ItemView隨著手指滑動(dòng)
??我們知道了ItemTouchHelper怎么進(jìn)行手勢(shì)判斷來(lái)選中一個(gè)ItemView,選中之后的操作就是ItemView隨著手指滑動(dòng),我們來(lái)看看ItemView是怎么實(shí)現(xiàn)的。
??我們知道,隨著手指的滑動(dòng),onTouchEvent方法會(huì)被調(diào)用,我們來(lái)看看相關(guān)的代碼:
public void onTouchEvent(RecyclerView recyclerView, MotionEvent event) {
// ······
switch (action) {
case MotionEvent.ACTION_MOVE: {
// Find the index of the active pointer and fetch its position
if (activePointerIndex >= 0) {
updateDxDy(event, mSelectedFlags, activePointerIndex);
moveIfNecessary(viewHolder);
mRecyclerView.removeCallbacks(mScrollRunnable);
mScrollRunnable.run();
mRecyclerView.invalidate();
}
break;
}
// ······
}
}
??上面的代碼我將它分為4步:
- 更新
mDx和mDy的值。mDx和mDy表示手指在x軸和y軸上分別滑動(dòng)的距離。- 如果需要,移動(dòng)其他
ItemView的位置。這個(gè)主要針對(duì)拖動(dòng)行為。- 如果需要,滑動(dòng)
RecyclerView。這個(gè)主要針對(duì)拖動(dòng)行為,而這里滑動(dòng)RecyclerView的條件就是,RecyclerView本身有大量的數(shù)據(jù),一屏顯示不完,此時(shí)如果拖動(dòng)一個(gè)ItemView達(dá)到RecyclerView的底部或者頂部,會(huì)滑動(dòng)RecyclerView。- 更新被選中的
ItemView的位置。代碼體現(xiàn)在mRecyclerView.invalidate()。
??其中,更新mDx和mDy的值是通過(guò)updateDxDy方法來(lái)實(shí)現(xiàn)的,而updateDxDy方法方法比較簡(jiǎn)單,這里就不展開了。
??我們?cè)賮?lái)看看第二步,移動(dòng)其他ItemView的位置主要是通過(guò)moveIfNecessary方法實(shí)現(xiàn)的。我們來(lái)看看具體的代碼:
void moveIfNecessary(ViewHolder viewHolder) {
// ······
// 以上都是不符合move的條件
// 1.尋找可能會(huì)交換位置的ItemView
List<ViewHolder> swapTargets = findSwapTargets(viewHolder);
if (swapTargets.size() == 0) {
return;
}
// 2.找到符合條件交換的ItemView
// 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();
// 3.回調(diào)Callback里面的onMove方法,這個(gè)方法需要我們手動(dòng)實(shí)現(xiàn)
if (mCallback.onMove(mRecyclerView, viewHolder, target)) {
// 保證target的可見
// keep target visible
mCallback.onMoved(mRecyclerView, viewHolder, fromPosition,
target, toPosition, x, y);
}
}
??如上就是moveIfNecessary方法的代碼,這里講它分為3步:
- 調(diào)用
findSwapTarget方法,尋找可能會(huì)跟選中的ItemView交換位置的ItemView。這里判斷的條件是只要選中的ItemView跟某一個(gè)ItemView重疊,那么這個(gè)ItemView可能會(huì)跟選中的ItemView交換位置。- 調(diào)用Callback的
chooseDropTarget方法來(lái)找到符合交換條件的ItemView。這里符合的條件是指,選中的ItemView的bottom大于目標(biāo)ItemView的bottom或者ItemView的top大于目標(biāo)ItemView的top。通常來(lái)說(shuō),我們可以重寫chooseDropTarget方法,來(lái)定義什么條件下就交換位置。- 回調(diào)
Callback的onMove方法,這個(gè)方法需要我們自己實(shí)現(xiàn)。這里需要注意的是,如果onMove方法返回為true的話,會(huì)調(diào)用Callback另一個(gè)onMove方法來(lái)保證target可見。為什么必須保證target可見呢?從官方文檔上來(lái)看的話,如果target不可見,在某些滑動(dòng)的情形下,target會(huì)被remove掉(回收掉),從而導(dǎo)致drag過(guò)早的停止。
??關(guān)于ItemTouchHelper是怎么來(lái)選擇交換位置的ItemView,重點(diǎn)就在findSwapTarget方法和chooseDropTarget方法。其中findSwapTarget方法是找到可能會(huì)交換位置的ItemView,chooseDropTarget方法是找到會(huì)交換位置的ItemView,這是兩個(gè)方法的不同點(diǎn)。同時(shí),如果此時(shí)在拖動(dòng),但是拖動(dòng)的ItemView還未達(dá)到交換條件,也就是跟另一個(gè)ItemView只是重疊了一小部分,這種情況下,findSwapTargets方法返回的集合不為空,但是chooseDropTarget方法尋找的ItemView為空。
??然后就是第三步,第三步的作用是當(dāng)ItemView拖動(dòng)到邊緣,如果此時(shí)RecyclerView可以滑動(dòng),那么RecyclerView會(huì)滾動(dòng)。具體的實(shí)現(xiàn)是在mScrollRunnable的run方法調(diào)用:
final Runnable mScrollRunnable = new Runnable() {
@Override
public void run() {
if (mSelected != null && scrollIfNecessary()) {
if (mSelected != null) { //it might be lost during scrolling
moveIfNecessary(mSelected);
}
mRecyclerView.removeCallbacks(mScrollRunnable);
ViewCompat.postOnAnimation(mRecyclerView, this);
}
}
};
??在run方法里面通過(guò)scrollIfNecessary方法來(lái)判斷RecyclerView是否滾動(dòng),如果需要滾動(dòng),scrollIfNecessary方法會(huì)自動(dòng)完成滾動(dòng)操作。
??最后一步就是ItemView位置的更新,也就是mRecyclerView.invalidate()的執(zhí)行。這里需要理解的是,為什么通過(guò)invalidate方法就能更新ItemView的位置呢?因?yàn)?code>ItemView在隨著手指移動(dòng)時(shí),變化的是translationX和translationY兩個(gè)屬性,所以只需要調(diào)用invalidate方法就行。調(diào)用invalidate方法之后,相當(dāng)于RecyclerView會(huì)重新繪制一次,那么所有ItemDecoration的onDraw和onDrawOver方法都會(huì)被調(diào)用,而恰好的是,ItemTouchHelper就是一個(gè)ItemDecoration。我們想要知道ItemView是怎么隨著手指移動(dòng)的,答案就在onDraw方法里面:
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
// ······
mCallback.onDraw(c, parent, mSelected,
mRecoverAnimations, mActionState, dx, dy);
}
??在onDraw方法里面,調(diào)用了Callback的onDraw方法。我們來(lái)看看Callback的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);
c.restoreToCount(count);
}
if (selected != null) {
final int count = c.save();
onChildDraw(c, parent, selected, dX, dY, actionState, true);
c.restoreToCount(count);
}
}
??代碼還是比較長(zhǎng),但是表示的意思是非常簡(jiǎn)單的。就是調(diào)用onChildDraw方法,將所有正在交換位置的ItemView和被選中的ItemView作為參數(shù)傳遞過(guò)去。
??而在onChildDraw方法里面,調(diào)用了ItemTouchUIUtil的onDraw方法。我們從ItemTouchUiUtil的實(shí)現(xiàn)類BaseImpl找到答案:
@Override
public void onDraw(Canvas c, RecyclerView recyclerView, View view,
float dX, float dY, int actionState, boolean isCurrentlyActive) {
view.setTranslationX(dX);
view.setTranslationY(dY);
}
??在這里改變了每個(gè)ItemView的translationX和translationY,從而實(shí)現(xiàn)了ItemView隨著手指移動(dòng)的效果。
??從這里,我們可以看出來(lái),一旦調(diào)用RecyclerView的invalidate方法,ItemTouchHelper的onDraw方法和onDrawOver方法都會(huì)被執(zhí)行。這個(gè)可能就是ItemTouchHelper繼承ItemDecoration的原因吧。
(4).為什么拖動(dòng)的ItemView始終在其他ItemView的上面?
??當(dāng)我們?cè)谏舷峦蟿?dòng)的時(shí)候,我們發(fā)現(xiàn)一個(gè)問(wèn)題,就是拖動(dòng)的ItemView始終在其他ItemView的上面。這里,我們不禁疑惑,我們都知道,在ViewGroup里面,所有的child都有繪制順序。通常來(lái)說(shuō),先添加的child先繪制,后添加的child后繪制,在RecyclerView中也是不例外,上面的ItemView先繪制,而下面的ItemView后繪制。而在這個(gè)拖動(dòng)效果中,為什么不符合這個(gè)規(guī)則呢?我們來(lái)看看ItemTouchHelper是怎么幫忙實(shí)現(xiàn)的。
??答案得分為兩個(gè)種情況,一種是Api小于21,一種是Api大于等于21。
??我們先來(lái)看看Api小于21的情況。這個(gè)得從addChildDrawingOrderCallback方法里面去尋找答案:
private void addChildDrawingOrderCallback() {
if (Build.VERSION.SDK_INT >= 21) {
return; // we use elevation on Lollipop
}
if (mChildDrawingOrderCallback == null) {
mChildDrawingOrderCallback = new RecyclerView.ChildDrawingOrderCallback() {
@Override
public int onGetChildDrawingOrder(int childCount, int i) {
if (mOverdrawChild == null) {
return i;
}
int childPosition = mOverdrawChildPosition;
if (childPosition == -1) {
childPosition = mRecyclerView.indexOfChild(mOverdrawChild);
mOverdrawChildPosition = childPosition;
}
if (i == childCount - 1) {
return childPosition;
}
return i < childPosition ? i : i + 1;
}
};
}
mRecyclerView.setChildDrawingOrderCallback(mChildDrawingOrderCallback);
}
??實(shí)現(xiàn)的原理就是給RecyclerView設(shè)置了一個(gè)ChildDrawingOrderCallback接口來(lái)改變child的繪制順序,這樣能保證被選中的ItemView后于重疊的ItemView繪制,這樣就實(shí)現(xiàn)了被選中的ItemView始終在上面。
??不過(guò)使用ChildDrawingOrderCallback接口時(shí),我們需要注意的是:要想是接口有效,必須保證所有child的elevation是一樣的,如果不一樣,那么elevation優(yōu)先級(jí)更高。
??從上面的注意點(diǎn),我們應(yīng)該都知道Api大于等于21時(shí),使用的是什么方式來(lái)實(shí)現(xiàn)的吧。沒錯(cuò)就是通過(guò)改變 ItemView的elevation值實(shí)現(xiàn)的。我們來(lái)看看具體實(shí)現(xiàn),在Api21Impl的onDraw方法里面:
@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);
}
??因?yàn)檫@里使用的是ViewCompcat,所以當(dāng)Api小于21時(shí),調(diào)用setElevation是無(wú)效的。如上就是Api大于等于21時(shí)實(shí)現(xiàn)被選中的ItemView在所有ItemView上面的代碼。
(5). 手勢(shì)釋放之后
??不管是拖動(dòng)還是側(cè)滑,當(dāng)我們手勢(shì)釋放之后,做的操作無(wú)非兩種:1. 回到原位;2.移動(dòng)到正確的位置。那這部分的具體實(shí)現(xiàn)在哪里呢?沒錯(cuò),就在我們之前分析過(guò)的select方法里面,此時(shí)看select方法代碼時(shí),我們需得注意兩個(gè)點(diǎn):
- 此時(shí),參數(shù)
selected為null。- 此時(shí),變量
mSelected不為null。
??然后,我們?cè)趤?lái)看看相關(guān)代碼:
void select(ViewHolder selected, int actionState) {
// ······
if (mSelected != null) {
final ViewHolder prevSelected = mSelected;
if (prevSelected.itemView.getParent() != null) {
// 1. 計(jì)算需要移動(dòng)的距離
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];
// 2.創(chuàng)建動(dòng)畫
final RecoverAnimation rv = new RecoverAnimation(prevSelected, animationType,
prevActionState, currentTranslateX, currentTranslateY,
targetTranslateX, targetTranslateY) {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
if (this.mOverridden) {
return;
}
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.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);
// 3.執(zhí)行動(dòng)畫
rv.start();
preventLayout = true;
} else {
removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);
mCallback.clearView(mRecyclerView, prevSelected);
}
mSelected = null;
}
// ······
}
??上面的代碼還是比較長(zhǎng),我簡(jiǎn)單的將它分為3步,分別是:
- 計(jì)算
ItemView此時(shí)需要移動(dòng)的距離。- 根據(jù)計(jì)算出來(lái)的距離,創(chuàng)建動(dòng)畫。
- 執(zhí)行動(dòng)畫,讓
ItemView回到正確的位置。
??而這三步的具體實(shí)現(xiàn)都是比較簡(jiǎn)單的,在這里就不過(guò)多的解釋了。
4.總結(jié)
??到此為止,ItemTouchHelper就差不多了,在這里我對(duì)ItemTouchHelper做一個(gè)簡(jiǎn)單的總結(jié)。
- 我們使用
ItemTouchHelper時(shí),需要實(shí)現(xiàn)一個(gè)ItemTouchHelper.Callback類。在這個(gè)實(shí)現(xiàn)類里面,我們需要實(shí)現(xiàn) 三個(gè)方法,分別是:1.getMovementFlags,主要是設(shè)置ItemTouchHelper執(zhí)行那些行為和方向;2.onMove方法,表示當(dāng)前有兩個(gè)ItemView發(fā)生了交換,此時(shí)需要我們更新數(shù)據(jù)源;3.onSwiped方法,表示當(dāng)前有ItemView被側(cè)滑刪除,也需要我們更新數(shù)據(jù)源。ItemTouochHelper是通過(guò)ItemTouchListener來(lái)獲取每個(gè)ItemView的事件,通過(guò)GestureDetector來(lái)判斷長(zhǎng)按行為。ItemTouchHelper是通過(guò)改變ItemView的translationX和translationY屬性值,進(jìn)而改變每個(gè)ItemView的位置。ItemTouchHelper是通過(guò)ChildDrawingOrderCallback接口和Elevation來(lái)改變ItemView的繪制順序的。