今天咱們來學習一下RecyclerView
另外一個不爲人知的輔助類--ItemTouchHelper
。咱們在作列表視圖,就好比說,ListView
或者RecyclerView
,一般會有兩種需求:1. 側滑刪除;2. 拖動交換位置。對於第一種需求使用傳統的版本實現還比較簡單,咱們能夠自定義ItemView
來實現;而第二種的話,可能就稍微有一點複雜,可能須要重寫LayoutManager
。git
這些辦法也不否定是有效的解決方案,可是是不是簡單和低耦合性的辦法呢?固然不是,踩過坑的同窗應該都知道,不論是自定義View
仍是自定義LayoutManager
都不是一件簡單的事情,其次,自定義ItemView
致使Adapter
的通用性下降。這些實現方式都是比較麻煩的。github
而谷歌爸爸真是貼心,知道咱們都有這種需求,就小手一抖,隨便幫咱們實現了一個Helper類,來減輕咱們的工做量。這就是ItemTouchHelper
的做用。bash
本文打算從兩個方面來教你們認識ItemTouchHelper
類:app
ItemTouchHelper
的基本使用ItemTouchHelper
的源碼分析
本文參考資料:ide
在正式介紹ItemTouchHelper
以前,咱們先來了解ItemTouchHelper
是什麼東西。源碼分析
從ItemTouchHelper
的源碼中,咱們能夠看出來,ItemTouchHelper
繼承了ItemDecoration
,根本上就是一個ItemDecoration
。關於ItemDecoration
的分析,有興趣的同窗能夠參考個人文章:RecyclerView 擴展(一) - 手把手教你認識ItemDecoration。post
public class ItemTouchHelper extends RecyclerView.ItemDecoration
implements RecyclerView.OnChildAttachStateChangeListener {
}
複製代碼
至於爲何ItemTouchHelper
會繼承ItemDecoration
,後面會詳細的解釋,這裏就先賣一下關子。學習
而後,咱們先來看看ItemTouchHelper
實現的效果,讓你們有一個直觀的體驗。fetch
先是側滑刪除的效果: 動畫
而後是拖動交換位置: 本文打算從上面兩種效果來介紹ItemTouchHelper
的使用。
既然是手把手教你們認識ItemTouchHelper
,因此天然須要介紹它的的基本使用,如今讓咱們來看看究竟怎麼使用ItemTouchHelper
。
在正式介紹ItemTouchHelper
的基本使用以前,咱們還必須瞭解一個類--ItemTouchHelper.Callback
。ItemTouchHelper
就是依靠這個類來實現側滑刪除和拖動位置兩種效果的,我來看看它。
咱們在使用ItemTouchHelper
時,必須自定義一個ItemTouchHelper.Callback
,咱們來了解一下其中比較重要的幾個方法。
方法名 | 做用 |
---|---|
getMovementFlags | 在此方法裏面咱們須要構建兩個flag,一個是dragFlags,表示拖動效果支持的方向,另外一個是swipeFlags,表示側滑效果支持的方向。在咱們的Demo中,拖動執行上下兩個方向,側滑執行左右兩個方向,這些操做咱們均可以在此方法裏面定義。 |
onMove | 當拖動效果已經產生了,會回調此方法。在此方法裏面,咱們一般會更新數據源,就好比說,一個ItemView 從0拖到了1位置,那麼對應的數據源也須要更改位置。 |
onSwiped | 當側滑效果以上產生了,會回調此方法。在此方法裏面,咱們也會更新數據源。與onMove 方法不一樣到的是,咱們在這個方法裏面從數據源裏面移除相應的數據,而後調用notifyXXX 方法就好了。 |
對於ItemTouchHelper
的基本使用來講,咱們只須要了解這三個方法就已經OK了。接下來,我將正式介紹ItemTouchHelper
的基本使用。
首先,咱們須要自定義一個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) {
// 交換在數據源中相應數據源的位置
return mItemTouchStatus.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
// 從數據源中移除相應的數據
mItemTouchStatus.onItemRemove(viewHolder.getAdapterPosition());
}
}
複製代碼
而後,咱們在使用RecyclerView
時,添加這兩行代碼就好了:
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new CustomItemTouchCallback(mAdapter));
itemTouchHelper.attachToRecyclerView(mRecyclerView);
複製代碼
最終的效果就是上面的動圖展現的,是否是以爲很是的簡單呢?接下來,我將正式的分析ItemTouchHelper
的源碼。
爲了方便你們理解,我將個人代碼上傳到github,有興趣的同窗能夠看看:ItemTouchHelperDemo。
咱們從基本使用中瞭解到,ItemTouchHelper
的使用是很是簡單的,因此你們心裏有沒有一種好奇呢?那就是ItemTouchHelper
到底是怎麼實現,爲何兩個相對比較複雜的效果,經過幾行代碼就能實現呢?接下來的內容就能找到答案。
咱們都知道,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
方法是比較簡單的。這其中,咱們發現ItemTouchHelper
是經過ItemTouchListener
接口來爲每一個ItemView
處理事件,同時,從這裏咱們能夠看出來,在ItemTouchHelper
內部還使用了GestureDetector
,而這裏GestureDetector
的做用主要是來判斷ItemView
是否進行了長按行爲。
ItemTouchHelper
的分析重點應該是事件處理,可是在這以前,咱們先來看一個方法,這個方法很是的重要的。
當咱們的操做觸發了長按或者側滑的行爲,都會回調此方法,同時當咱們手勢釋放,也會回調此方法。
因此從大的時機來看,當手勢開始或者釋放都會回調select
方法;而每一個大時機又分爲兩個小時機,分別是長按和側滑,分別表示拖動交換位置和側滑刪除操做。
在正式分析select
方法的代碼以前,咱們須要瞭解兩個東西:
selected
表示被選中的ViewHolder
。其中,selected
若是爲null,則表示當前處於手勢(包括長按和側滑)釋放時機;反之,selected
不爲null,則表示當前處於手勢開始的時機。actionState
表示當前的狀態,一共有三個值可選,分別是:1.ACTION_STATE_IDLE
表示沒有任何手勢,此時selected
對應的應當是null;2.ACTION_STATE_SWIPE
表示當前ItemView
處於側滑狀態;3.ACTION_STATE_DRAG
表示當前ItemView
處於拖動狀態。在ItemTouchHelper
內部,就是經過這三個狀態來判斷ItemView
處於什麼狀態。
接下來咱們來看看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;
// 若是當前是拖動行爲,給RecyclerView設置一個ChildDrawingOrderCallback接口
// 主要是爲了調整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();
}
複製代碼
從上面的代碼中,咱們能夠總結出來幾個結論:
- 若是處於手勢開始階段,即
selected
不爲null,那麼會經過getAbsoluteMovementFlags
方法來獲取執行咱們設置的flag,從而就知道執行哪些行爲(側滑或者拖動)和方向(上、下、左和右)。同時還會記錄下被選中ItemView
的位置。簡而言之,就是一些變量的初始化。- 若是處於手勢釋放階段,即
selected
爲null,同時mSelected
不爲null,那麼此時須要作的事情就稍微有一點複雜。手勢釋放以後,須要作的事情無非有兩件:1. 相關的ItemView
到正確的位置,就好比說,若是滑動條件不知足,那麼就返回原來的位置,這個就是一個動畫;2. 清理操做,好比說將mSelected
重置爲null之類的
ItemView
是否被選中 咱們知道,一旦調用selected
就意味着一個ItemView
被選中,接下來的就會隨着手勢出現側滑或者拖動的效果了。可是怎麼來判斷一個ItemView
是否被選中,咱們從代碼來看看,咱們分兩步來理解:1.側滑的選中;2. 拖動的選中。
判斷側滑行爲是否選中主要在checkSelectForSwipe
方法,咱們來看看checkSelectForSwipe
放大的代碼:
boolean checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) {
// 若是mSelected不爲null表示已經有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沒有支持側滑的方向值,那麼返回爲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;
}
// 這裏主要是判斷一個滑動是否符合側滑的條件
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);
// 表示當前ItemView被側滑行爲選中
select(vh, ACTION_STATE_SWIPE);
return true;
}
複製代碼
checkSelectForSwipe
方法的代碼相對來講比較長,可是無非就是判斷當前ItemView
是否符合側滑行爲,若是到最後符合的話,那麼就會調用select
方法來初始化一些值。 同時,咱們看一下checkSelectForSwipe
方法的調用時機只有兩個地方:
onTouchEvent
方法onInterceptTouchEvent
方法
調用的時機也是比較正確的,至於爲何須要兩個地方來調用這個方法,我也不太清楚,估計作什麼保險操做吧。
拖動選中的時機比較簡單,由於拖動觸發的前提是長按ItemView
,因此咱們直接在ItemTouchHelperGestureListener
的onLongPress
方法找到相關代碼:
@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);
}
}
}
}
}
複製代碼
這段代碼表達的意思很是簡單,這裏我就很少餘的解釋了。從這裏能夠看出來,最終仍是調用了select
方法表示選中一個ItemView
。
咱們知道了ItemTouchHelper
怎麼進行手勢判斷來選中一個ItemView
,選中以後的操做就是ItemView
隨着手指滑動,咱們來看看ItemView
是怎麼實現的。
咱們知道,隨着手指的滑動,onTouchEvent
方法會被調用,咱們來看看相關的代碼:
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
自己有大量的數據,一屏顯示不完,此時若是拖動一個ItemView
達到RecyclerView
的底部或者頂部,會滑動RecyclerView
。- 更新被選中的
ItemView
的位置。代碼體如今mRecyclerView.invalidate()
。
其中,更新mDx
和mDy
的值是經過updateDxDy
方法來實現的,而updateDxDy
方法方法比較簡單,這裏就不展開了。
咱們再來看看第二步,移動其餘ItemView
的位置主要是經過moveIfNecessary
方法實現的。咱們來看看具體的代碼:
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.回調Callback裏面的onMove方法,這個方法須要咱們手動實現
if (mCallback.onMove(mRecyclerView, viewHolder, target)) {
// 保證target的可見
// keep target visible
mCallback.onMoved(mRecyclerView, viewHolder, fromPosition,
target, toPosition, x, y);
}
}
複製代碼
如上就是moveIfNecessary
方法的代碼,這裏講它分爲3步:
- 調用
findSwapTarget
方法,尋找可能會跟選中的ItemView
交換位置的ItemView
。這裏判斷的條件是隻要選中的ItemView
跟某一個ItemView
重疊,那麼這個ItemView
可能會跟選中的ItemView
交換位置。- 調用Callback的
chooseDropTarget
方法來找到符合交換條件的ItemView
。這裏符合的條件是指,選中的ItemView
的bottom
大於目標ItemView
的bottom
或者ItemView
的top
大於目標ItemView
的top
。一般來講,咱們能夠重寫chooseDropTarget
方法,來定義什麼條件下就交換位置。- 回調
Callback
的onMove
方法,這個方法須要咱們本身實現。這裏須要注意的是,若是onMove
方法返回爲true的話,會調用Callback
另外一個onMove
方法來保證target可見。爲何必須保證target可見呢?從官方文檔上來看的話,若是target不可見,在某些滑動的情形下,target會被remove掉(回收掉),從而致使drag過早的中止。
關於ItemTouchHelper
是怎麼來選擇交換位置的ItemView
,重點就在findSwapTarget
方法和chooseDropTarget
方法。其中findSwapTarget
方法是找到可能會交換位置的ItemView
,chooseDropTarget
方法是找到會交換位置的ItemView
,這是兩個方法的不一樣點。同時,若是此時在拖動,可是拖動的ItemView
還未達到交換條件,也就是跟另外一個ItemView
只是重疊了一小部分,這種狀況下,findSwapTargets
方法返回的集合不爲空,可是chooseDropTarget
方法尋找的ItemView
爲空。
而後就是第三步,第三步的做用是當ItemView
拖動到邊緣,若是此時RecyclerView
能夠滑動,那麼RecyclerView
會滾動。具體的實現是在mScrollRunnable
的run
方法調用:
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()
的執行。這裏須要理解的是,爲何經過invalidate
方法就能更新ItemView
的位置呢?由於ItemView
在隨着手指移動時,變化的是translationX
和translationY
兩個屬性,因此只須要調用invalidate
方法就行。調用invalidate
方法以後,至關於RecyclerView
會從新繪製一次,那麼全部ItemDecoration
的onDraw
和onDrawOver
方法都會被調用,而剛好的是,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
方法裏面,調用了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);
}
}
複製代碼
代碼仍是比較長,可是表示的意思是很是簡單的。就是調用onChildDraw
方法,將全部正在交換位置的ItemView
和被選中的ItemView
做爲參數傳遞過去。
而在onChildDraw
方法裏面,調用了ItemTouchUIUtil
的onDraw
方法。咱們從ItemTouchUiUtil
的實現類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
,從而實現了ItemView
隨着手指移動的效果。
從這裏,咱們能夠看出來,一旦調用RecyclerView
的invalidate
方法,ItemTouchHelper
的onDraw
方法和onDrawOver
方法都會被執行。這個可能就是ItemTouchHelper
繼承ItemDecoration
的緣由吧。
當咱們在上下拖動的時候,咱們發現一個問題,就是拖動的ItemView
始終在其餘ItemView
的上面。這裏,咱們不由疑惑,咱們都知道,在ViewGroup
裏面,全部的child
都有繪製順序。一般來講,先添加的child
先繪製,後添加的child
後繪製,在RecyclerView
中也是不例外,上面的ItemView
先繪製,而下面的ItemView
後繪製。而在這個拖動效果中,爲何不符合這個規則呢?咱們來看看ItemTouchHelper
是怎麼幫忙實現的。
答案得分爲兩個種狀況,一種是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);
}
複製代碼
實現的原理就是給RecyclerView
設置了一個ChildDrawingOrderCallback
接口來改變child
的繪製順序,這樣能保證被選中的ItemView
後於重疊的ItemView
繪製,這樣就實現了被選中的ItemView
始終在上面。
不過使用ChildDrawingOrderCallback
接口時,咱們須要注意的是:要想是接口有效,必須保證全部child
的elevation
是同樣的,若是不同,那麼elevation
優先級更高。
從上面的注意點,咱們應該都知道Api大於等於21時,使用的是什麼方式來實現的吧。沒錯就是經過改變 ItemView
的elevation
值實現的。咱們來看看具體實現,在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
,因此當Api小於21時,調用setElevation
是無效的。如上就是Api大於等於21時實現被選中的ItemView
在全部ItemView上面的代碼。
不論是拖動仍是側滑,當咱們手勢釋放以後,作的操做無非兩種:1. 回到原位;2.移動到正確的位置。那這部分的具體實如今哪裏呢?沒錯,就在咱們以前分析過的select
方法裏面,此時看select
方法代碼時,咱們需得注意兩個點:
- 此時,參數
selected
爲null。- 此時,變量
mSelected
不爲null。
而後,咱們在來看看相關代碼:
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.建立動畫
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.執行動畫
rv.start();
preventLayout = true;
} else {
removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);
mCallback.clearView(mRecyclerView, prevSelected);
}
mSelected = null;
}
// ······
}
複製代碼
上面的代碼仍是比較長,我簡單的將它分爲3步,分別是:
- 計算
ItemView
此時須要移動的距離。- 根據計算出來的距離,建立動畫。
- 執行動畫,讓
ItemView
回到正確的位置。
而這三步的具體實現都是比較簡單的,在這裏就不過多的解釋了。
到此爲止,ItemTouchHelper
就差很少了,在這裏我對ItemTouchHelper
作一個簡單的總結。
- 咱們使用
ItemTouchHelper
時,須要實現一個ItemTouchHelper.Callback
類。在這個實現類裏面,咱們須要實現 三個方法,分別是:1.getMovementFlags
,主要是設置ItemTouchHelper
執行那些行爲和方向;2.onMove
方法,表示當前有兩個ItemView
發生了交換,此時須要咱們更新數據源;3.onSwiped
方法,表示當前有ItemView
被側滑刪除,也須要咱們更新數據源。ItemTouochHelper
是經過ItemTouchListener
來獲取每一個ItemView
的事件,經過GestureDetector
來判斷長按行爲。ItemTouchHelper
是經過改變ItemView
的translationX
和translationY
屬性值,進而改變每一個ItemView
的位置。ItemTouchHelper
是經過ChildDrawingOrderCallback
接口和Elevation
來改變ItemView
的繪製順序的。