??今天我們來學(xué)習(xí)一下RecyclerView
另一個鮮為人知的輔助類--ItemTouchHelper
两嘴。我們在做列表視圖嘿棘,就比如說智嚷,ListView
或者RecyclerView
馍悟,通常會有兩種需求:1. 側(cè)滑刪除狮含;2. 拖動交換位置柒傻。對于第一種需求使用傳統(tǒng)的版本實現(xiàn)還比較簡單孝赫,我們可以自定義ItemView
來實現(xiàn);而第二種的話红符,可能就稍微有一點(diǎn)復(fù)雜青柄,可能需要重寫LayoutManager
。
??這些辦法也不否認(rèn)是有效的解決方案预侯,但是是否是簡單和低耦合性的辦法呢致开?當(dāng)然不是,踩過坑的同學(xué)應(yīng)該都知道萎馅,不管是自定義View
還是自定義LayoutManager
都不是一件簡單的事情双戳,其次,自定義ItemView
導(dǎo)致Adapter
的通用性降低校坑。這些實現(xiàn)方式都是比較麻煩的拣技。
??而谷歌爸爸真是貼心,知道我們都有這種需求耍目,就小手一抖膏斤,隨便幫我們實現(xiàn)了一個Helper類,來減輕我們的工作量邪驮。這就是ItemTouchHelper
的作用莫辨。
??本文打算從兩個方面來教大家認(rèn)識ItemTouchHelper
類:
ItemTouchHelper
的基本使用ItemTouchHelper
的源碼分析
??本文參考資料:
1. 概述
??在正式介紹ItemTouchHelper
之前,我們先來了解ItemTouchHelper
是什么東西毅访。
??從ItemTouchHelper
的源碼中沮榜,我們可以看出來,ItemTouchHelper
繼承了ItemDecoration
喻粹,根本上就是一個ItemDecoration
蟆融。關(guān)于ItemDecoration
的分析,有興趣的同學(xué)可以參考我的文章:RecyclerView 擴(kuò)展(一) - 手把手教你認(rèn)識ItemDecoration守呜。
public class ItemTouchHelper extends RecyclerView.ItemDecoration
implements RecyclerView.OnChildAttachStateChangeListener {
}
??至于為什么ItemTouchHelper
會繼承ItemDecoration
型酥,后面會詳細(xì)的解釋山憨,這里就先賣一下關(guān)子。
??然后弥喉,我們先來看看ItemTouchHelper
實現(xiàn)的效果郁竟,讓大家有一個直觀的體驗。
??先是側(cè)滑刪除的效果:
??然后是拖動交換位置:
??本文打算從上面兩種效果來介紹
ItemTouchHelper
的使用由境。
2. ItemTouchHelper的基本使用
??既然是手把手教大家認(rèn)識ItemTouchHelper
棚亩,所以自然需要介紹它的的基本使用,現(xiàn)在讓我們來看看究竟怎么使用ItemTouchHelper
虏杰。
??在正式介紹ItemTouchHelper
的基本使用之前讥蟆,我們還必須了解一個類--ItemTouchHelper.Callback
。ItemTouchHelper
就是依靠這個類來實現(xiàn)側(cè)滑刪除和拖動位置兩種效果的嘹屯,我來看看它攻询。
(1). ItemTouchHelper.Callback
??我們在使用ItemTouchHelper
時,必須自定義一個ItemTouchHelper.Callback
州弟,我們來了解一下其中比較重要的幾個方法。
方法名 | 作用 |
---|---|
getMovementFlags | 在此方法里面我們需要構(gòu)建兩個flag低零,一個是dragFlags婆翔,表示拖動效果支持的方向,另一個是swipeFlags掏婶,表示側(cè)滑效果支持的方向啃奴。在我們的Demo中,拖動執(zhí)行上下兩個方向雄妥,側(cè)滑執(zhí)行左右兩個方向最蕾,這些操作我們都可以在此方法里面定義。 |
onMove | 當(dāng)拖動效果已經(jīng)產(chǎn)生了老厌,會回調(diào)此方法瘟则。在此方法里面,我們通常會更新數(shù)據(jù)源枝秤,就比如說醋拧,一個ItemView 從0拖到了1位置,那么對應(yīng)的數(shù)據(jù)源也需要更改位置淀弹。 |
onSwiped | 當(dāng)側(cè)滑效果以上產(chǎn)生了丹壕,會回調(diào)此方法。在此方法里面薇溃,我們也會更新數(shù)據(jù)源菌赖。與onMove 方法不同到的是,我們在這個方法里面從數(shù)據(jù)源里面移除相應(yīng)的數(shù)據(jù)沐序,然后調(diào)用notifyXXX 方法就行了琉用。 |
??對于ItemTouchHelper
的基本使用來說忿峻,我們只需要了解這三個方法就已經(jīng)OK了。接下來辕羽,我將正式介紹ItemTouchHelper
的基本使用逛尚。
(2). 基本使用
??首先,我們需要自定義一個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) {
// 上下拖動
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
// 向左滑動
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());
}
}
??然后绰寞,我們在使用RecyclerView
時,添加這兩行代碼就行了:
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new CustomItemTouchCallback(mAdapter));
itemTouchHelper.attachToRecyclerView(mRecyclerView);
??最終的效果就是上面的動圖展示的铣口,是不是覺得非常的簡單呢滤钱?接下來,我將正式的分析ItemTouchHelper
的源碼脑题。
(4).源碼
??為了方便大家理解件缸,我將我的代碼上傳到github,有興趣的同學(xué)可以看看:ItemTouchHelperDemo叔遂。
3. ItemTouchHelper的源碼分析
??我們從基本使用中了解到他炊,ItemTouchHelper
的使用是非常簡單的,所以大家內(nèi)心有沒有一種好奇呢已艰?那就是ItemTouchHelper
究竟是怎么實現(xiàn)痊末,為什么兩個相對比較復(fù)雜的效果,通過幾行代碼就能實現(xiàn)呢哩掺?接下來的內(nèi)容就能找到答案凿叠。
(1). attachToRecyclerView方法
??我們都知道,ItemTouchHelper
的入口方法就是attachToRecyclerView
方法嚼吞,接下來盒件,我們先來看看這個方法為我們做了哪些事情。
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);
}
??相對來說舱禽,attachToRecyclerView
方法是比較簡單的炒刁。這其中,我們發(fā)現(xiàn)ItemTouchHelper
是通過ItemTouchListener
接口來為每個ItemView
處理事件呢蔫,同時切心,從這里我們可以看出來,在ItemTouchHelper
內(nèi)部還使用了GestureDetector
片吊,而這里GestureDetector
的作用主要是來判斷ItemView
是否進(jìn)行了長按行為绽昏。
??ItemTouchHelper
的分析重點(diǎn)應(yīng)該是事件處理,但是在這之前俏脊,我們先來看一個方法全谤,這個方法非常的重要的。
(2). select方法
??當(dāng)我們的操作觸發(fā)了長按或者側(cè)滑的行為爷贫,都會回調(diào)此方法认然,同時當(dāng)我們手勢釋放补憾,也會回調(diào)此方法。
??所以從大的時機(jī)來看卷员,當(dāng)手勢開始或者釋放都會回調(diào)select
方法盈匾;而每個大時機(jī)又分為兩個小時機(jī),分別是長按和側(cè)滑毕骡,分別表示拖動交換位置和側(cè)滑刪除操作削饵。
??在正式分析select
方法的代碼之前,我們需要了解兩個東西:
selected
表示被選中的ViewHolder
未巫。其中窿撬,selected
如果為null,則表示當(dāng)前處于手勢(包括長按和側(cè)滑)釋放時機(jī)叙凡;反之劈伴,selected
不為null,則表示當(dāng)前處于手勢開始的時機(jī)握爷。actionState
表示當(dāng)前的狀態(tài)跛璧,一共有三個值可選,分別是:1.ACTION_STATE_IDLE
表示沒有任何手勢饼拍,此時selected
對應(yīng)的應(yīng)當(dāng)是null赡模;2.ACTION_STATE_SWIPE
表示當(dāng)前ItemView
處于側(cè)滑狀態(tài);3.ACTION_STATE_DRAG
表示當(dāng)前ItemView
處于拖動狀態(tài)师抄。在ItemTouchHelper
內(nèi)部,就是通過這三個狀態(tài)來判斷ItemView
處于什么狀態(tà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)前是拖動行為,給RecyclerView設(shè)置一個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.手勢釋放
if (mSelected != null) {
// ······
}
// 2. 手勢開始
// selected不為null表示手勢開始叨吮,反之selected為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();
}
??從上面的代碼中,我們可以總結(jié)出來幾個結(jié)論:
- 如果處于手勢開始階段瞬矩,即
selected
不為null茶鉴,那么會通過getAbsoluteMovementFlags
方法來獲取執(zhí)行我們設(shè)置的flag,從而就知道執(zhí)行哪些行為(側(cè)滑或者拖動)和方向(上景用、下涵叮、左和右)。同時還會記錄下被選中ItemView
的位置伞插。簡而言之割粮,就是一些變量的初始化。- 如果處于手勢釋放階段媚污,即
selected
為null舀瓢,同時mSelected
不為null,那么此時需要做的事情就稍微有一點(diǎn)復(fù)雜耗美。手勢釋放之后京髓,需要做的事情無非有兩件:1. 相關(guān)的ItemView
到正確的位置航缀,就比如說,如果滑動條件不滿足堰怨,那么就返回原來的位置芥玉,這個就是一個動畫;2. 清理操作备图,比如說將mSelected
重置為null之類的灿巧。
(3).如何判斷一個ItemView
是否被選中
??我們知道,一旦調(diào)用selected
就意味著一個ItemView
被選中诬烹,接下來的就會隨著手勢出現(xiàn)側(cè)滑或者拖動的效果了砸烦。但是怎么來判斷一個ItemView
是否被選中,我們從代碼來看看绞吁,我們分兩步來理解:1.側(cè)滑的選中幢痘;2. 拖動的選中。
A. 側(cè)滑
??判斷側(cè)滑行為是否選中主要在checkSelectForSwipe
方法家破,我們來看看checkSelectForSwipe
放大的代碼:
boolean checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) {
// 如果mSelected不為null表示已經(jīng)有ItemView被選中
// 同時從這里可以看出來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;
}
// 這里主要是判斷一個滑動是否符合側(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
方法的代碼相對來說比較長,但是無非就是判斷當(dāng)前ItemView
是否符合側(cè)滑行為汰聋,如果到最后符合的話门粪,那么就會調(diào)用select
方法來初始化一些值。
??同時烹困,我們看一下checkSelectForSwipe
方法的調(diào)用時機(jī)只有兩個地方:
onTouchEvent
方法onInterceptTouchEvent
方法
??調(diào)用的時機(jī)也是比較正確的玄妈,至于為什么需要兩個地方來調(diào)用這個方法,我也不太清楚髓梅,估計做什么保險操作吧拟蜻。
B. 拖動選中
??拖動選中的時機(jī)比較簡單,因為拖動觸發(fā)的前提是長按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á)的意思非常簡單酝锅,這里我就不多余的解釋了。從這里可以看出來奢方,最終還是調(diào)用了select
方法表示選中一個ItemView
搔扁。
(3). ItemView隨著手指滑動
??我們知道了ItemTouchHelper
怎么進(jìn)行手勢判斷來選中一個ItemView
,選中之后的操作就是ItemView
隨著手指滑動蟋字,我們來看看ItemView
是怎么實現(xiàn)的稿蹲。
??我們知道,隨著手指的滑動愉老,onTouchEvent
方法會被調(diào)用场绿,我們來看看相關(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軸上分別滑動的距離。- 如果需要焰盗,移動其他
ItemView
的位置璧尸。這個主要針對拖動行為。- 如果需要熬拒,滑動
RecyclerView
爷光。這個主要針對拖動行為,而這里滑動RecyclerView
的條件就是澎粟,RecyclerView
本身有大量的數(shù)據(jù)蛀序,一屏顯示不完,此時如果拖動一個ItemView
達(dá)到RecyclerView
的底部或者頂部活烙,會滑動RecyclerView
徐裸。- 更新被選中的
ItemView
的位置。代碼體現(xiàn)在mRecyclerView.invalidate()
啸盏。
??其中重贺,更新mDx
和mDy
的值是通過updateDxDy
方法來實現(xiàn)的,而updateDxDy
方法方法比較簡單回懦,這里就不展開了气笙。
??我們再來看看第二步,移動其他ItemView
的位置主要是通過moveIfNecessary
方法實現(xiàn)的怯晕。我們來看看具體的代碼:
void moveIfNecessary(ViewHolder viewHolder) {
// ······
// 以上都是不符合move的條件
// 1.尋找可能會交換位置的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方法潜圃,這個方法需要我們手動實現(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
方法舟茶,尋找可能會跟選中的ItemView
交換位置的ItemView
谭期。這里判斷的條件是只要選中的ItemView
跟某一個ItemView
重疊,那么這個ItemView
可能會跟選中的ItemView
交換位置吧凉。- 調(diào)用Callback的
chooseDropTarget
方法來找到符合交換條件的ItemView
崇堵。這里符合的條件是指,選中的ItemView
的bottom
大于目標(biāo)ItemView
的bottom
或者ItemView
的top
大于目標(biāo)ItemView
的top
客燕。通常來說,我們可以重寫chooseDropTarget
方法狰贯,來定義什么條件下就交換位置也搓。- 回調(diào)
Callback
的onMove
方法,這個方法需要我們自己實現(xiàn)。這里需要注意的是涵紊,如果onMove
方法返回為true的話傍妒,會調(diào)用Callback
另一個onMove
方法來保證target可見。為什么必須保證target可見呢摸柄?從官方文檔上來看的話颤练,如果target不可見,在某些滑動的情形下驱负,target會被remove掉(回收掉)嗦玖,從而導(dǎo)致drag過早的停止患雇。
??關(guān)于ItemTouchHelper
是怎么來選擇交換位置的ItemView
,重點(diǎn)就在findSwapTarget
方法和chooseDropTarget
方法宇挫。其中findSwapTarget
方法是找到可能會交換位置的ItemView
苛吱,chooseDropTarget
方法是找到會交換位置的ItemView
,這是兩個方法的不同點(diǎn)楞黄。同時恤磷,如果此時在拖動什猖,但是拖動的ItemView
還未達(dá)到交換條件,也就是跟另一個ItemView
只是重疊了一小部分援所,這種情況下,findSwapTargets
方法返回的集合不為空欣除,但是chooseDropTarget
方法尋找的ItemView
為空住拭。
??然后就是第三步,第三步的作用是當(dāng)ItemView
拖動到邊緣耻涛,如果此時RecyclerView
可以滑動废酷,那么RecyclerView
會滾動。具體的實現(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
方法里面通過scrollIfNecessary
方法來判斷RecyclerView
是否滾動抹缕,如果需要滾動,scrollIfNecessary
方法會自動完成滾動操作澈蟆。
??最后一步就是ItemView
位置的更新,也就是mRecyclerView.invalidate()
的執(zhí)行卓研。這里需要理解的是趴俘,為什么通過invalidate
方法就能更新ItemView
的位置呢?因為ItemView
在隨著手指移動時奏赘,變化的是translationX
和translationY
兩個屬性寥闪,所以只需要調(diào)用invalidate
方法就行。調(diào)用invalidate
方法之后磨淌,相當(dāng)于RecyclerView
會重新繪制一次疲憋,那么所有ItemDecoration
的onDraw
和onDrawOver
方法都會被調(diào)用,而恰好的是梁只,ItemTouchHelper
就是一個ItemDecoration
缚柳。我們想要知道ItemView
是怎么隨著手指移動的,答案就在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
方法秋忙。我們來看看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);
}
}
??代碼還是比較長,但是表示的意思是非常簡單的构舟。就是調(diào)用onChildDraw
方法灰追,將所有正在交換位置的ItemView
和被選中的ItemView
作為參數(shù)傳遞過去。
??而在onChildDraw
方法里面,調(diào)用了ItemTouchUIUtil
的onDraw
方法弹澎。我們從ItemTouchUiUtil
的實現(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);
}
??在這里改變了每個ItemView
的translationX
和translationY
,從而實現(xiàn)了ItemView
隨著手指移動的效果朴下。
??從這里,我們可以看出來裁奇,一旦調(diào)用RecyclerView
的invalidate
方法桐猬,ItemTouchHelper
的onDraw
方法和onDrawOver
方法都會被執(zhí)行。這個可能就是ItemTouchHelper
繼承ItemDecoration
的原因吧刽肠。
(4).為什么拖動的ItemView始終在其他ItemView的上面溃肪?
??當(dāng)我們在上下拖動的時候,我們發(fā)現(xiàn)一個問題音五,就是拖動的ItemView
始終在其他ItemView
的上面惫撰。這里,我們不禁疑惑躺涝,我們都知道厨钻,在ViewGroup
里面,所有的child
都有繪制順序坚嗜。通常來說夯膀,先添加的child
先繪制,后添加的child
后繪制苍蔬,在RecyclerView
中也是不例外诱建,上面的ItemView
先繪制,而下面的ItemView
后繪制碟绑。而在這個拖動效果中俺猿,為什么不符合這個規(guī)則呢?我們來看看ItemTouchHelper
是怎么幫忙實現(xiàn)的格仲。
??答案得分為兩個種情況押袍,一種是Api小于21,一種是Api大于等于21凯肋。
??我們先來看看Api小于21的情況谊惭。這個得從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);
}
??實現(xiàn)的原理就是給RecyclerView
設(shè)置了一個ChildDrawingOrderCallback
接口來改變child
的繪制順序,這樣能保證被選中的ItemView
后于重疊的ItemView
繪制侮东,這樣就實現(xiàn)了被選中的ItemView
始終在上面午笛。
??不過使用ChildDrawingOrderCallback
接口時,我們需要注意的是:要想是接口有效苗桂,必須保證所有child
的elevation
是一樣的,如果不一樣告组,那么elevation
優(yōu)先級更高煤伟。
??從上面的注意點(diǎn),我們應(yīng)該都知道Api大于等于21時,使用的是什么方式來實現(xiàn)的吧便锨。沒錯就是通過改變 ItemView
的elevation
值實現(xiàn)的围辙。我們來看看具體實現(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);
}
??因為這里使用的是ViewCompcat
放案,所以當(dāng)Api小于21時姚建,調(diào)用setElevation
是無效的。如上就是Api大于等于21時實現(xiàn)被選中的ItemView
在所有ItemView上面的代碼吱殉。
(5). 手勢釋放之后
??不管是拖動還是側(cè)滑掸冤,當(dāng)我們手勢釋放之后,做的操作無非兩種:1. 回到原位友雳;2.移動到正確的位置稿湿。那這部分的具體實現(xiàn)在哪里呢?沒錯押赊,就在我們之前分析過的select
方法里面饺藤,此時看select
方法代碼時,我們需得注意兩個點(diǎn):
- 此時流礁,參數(shù)
selected
為null涕俗。- 此時,變量
mSelected
不為null神帅。
??然后再姑,我們在來看看相關(guān)代碼:
void select(ViewHolder selected, int actionState) {
// ······
if (mSelected != null) {
final ViewHolder prevSelected = mSelected;
if (prevSelected.itemView.getParent() != null) {
// 1. 計算需要移動的距離
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)建動畫
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í)行動畫
rv.start();
preventLayout = true;
} else {
removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);
mCallback.clearView(mRecyclerView, prevSelected);
}
mSelected = null;
}
// ······
}
??上面的代碼還是比較長,我簡單的將它分為3步枕稀,分別是:
- 計算
ItemView
此時需要移動的距離询刹。- 根據(jù)計算出來的距離,創(chuàng)建動畫萎坷。
- 執(zhí)行動畫凹联,讓
ItemView
回到正確的位置。
??而這三步的具體實現(xiàn)都是比較簡單的哆档,在這里就不過多的解釋了蔽挠。
4.總結(jié)
??到此為止,ItemTouchHelper
就差不多了瓜浸,在這里我對ItemTouchHelper
做一個簡單的總結(jié)澳淑。
- 我們使用
ItemTouchHelper
時,需要實現(xiàn)一個ItemTouchHelper.Callback
類插佛。在這個實現(xiàn)類里面杠巡,我們需要實現(xiàn) 三個方法,分別是:1.getMovementFlags
,主要是設(shè)置ItemTouchHelper
執(zhí)行那些行為和方向雇寇;2.onMove
方法氢拥,表示當(dāng)前有兩個ItemView
發(fā)生了交換蚌铜,此時需要我們更新數(shù)據(jù)源;3.onSwiped
方法嫩海,表示當(dāng)前有ItemView
被側(cè)滑刪除冬殃,也需要我們更新數(shù)據(jù)源。ItemTouochHelper
是通過ItemTouchListener
來獲取每個ItemView
的事件叁怪,通過GestureDetector
來判斷長按行為审葬。ItemTouchHelper
是通過改變ItemView
的translationX
和translationY
屬性值,進(jìn)而改變每個ItemView
的位置奕谭。ItemTouchHelper
是通過ChildDrawingOrderCallback
接口和Elevation
來改變ItemView
的繪制順序的涣觉。