一.簡介
???????本文主角是ItemTouchHelper
,它是RecyclerView對于item交互處理的一個輔助類,主要用于拖拽以及滑動處理。關于RecyclerView的分析可參考文章RecyclerView顯示及緩存機制
???????以接口實現的方式,起到了配置簡單、邏輯解耦、職責分明的效果,并且支持所有的布局方式。
???????功能包括如下:
二.功能實現
2.1.實現接口
???????自定義一個類,實現ItemTouchHelper.Callback
接口,然后在實現方法中根據需求簡單配置即可,代碼如下:
public class TaskItemTouchHelper extends ItemTouchHelper.Callback {
}
???????ItemTouchHelper.Callback必須實現的3個方法:
???????? getMovementFlags
???????? onMove
???????? onSwiped
???????其他方法還有onSelectedChanged、clearView、getSwipeThreshold等
???????接下來對上述方法進行一一描述分析:
2.1.1.getMovementFlags
???????用于創建交互方式,交互方式分為兩種:
???????a.拖拽:網格布局支持上下左右,列表只支持上下(UP、DOWN)
???????b.滑動:支持上下左右滑動(LEFT、UP、RIGHT、DOWN)
???????方法實現如下:
/**
* Called first when Callback, it is used to determine action and direction for current
* function: drag and swipe action
*/
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
//direction:up,down,left,right
//constants
// ItemTouchHelper.UP 0x0001
// ItemTouchHelper.DOWN 0x0010
// ItemTouchHelper.LEFT 0x0100
// ItemTouchHelper.RIGHT 0x1000
//listen for direction of drag
int dragFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
//listen for direction of swipe
int swipeFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
return makeMovementFlags(dragFlags, swipeFlags);
}
???????通過makeMovementFlags
把結果返回回去,makeMovementFlags接收兩個參數,dragFlags
和swipeFlags
,即拖拽和滑動組合的標志位。
2.1.2.onMove
???????拖拽時回調,這里我們主要對起始位置和目標位置的item做一個數據交換,然后刷新視圖顯示。
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder srcHolder,RecyclerView.ViewHolder targetHolder) {
// call adapter.notifyItemMoved(from,to) continuously when drag process
if (srcHolder.getItemViewType() != targetHolder.getItemViewType()) {
return false;
}
// call adapter.notifyItemMoved(from,to) continuously when drag process,
// callback onItemMove to implements class
return moveListener.onItemMove(srcHolder.getAdapterPosition(),
targetHolder.getAdapterPosition());
}
???????通過獲取起始位置,不斷調用adapter的notifyItemMoved()對UI進行刷新。
2.1.3.onSwiped
???????滑動時回調,這個回調方法里主要是做數據和視圖的更新操作。
@Override
public void onSwiped(RecyclerView.ViewHolder holder, int direction) {
//listen for swipe up:1.delete data;2.call adapter.notifyItemRemove(position);
moveListener.onItemRemove(holder.getAdapterPosition());
}}
???????比如:滑動刪除某個item,監聽到滿足swipe()的閾值后進行刪除操作;
2.2.關聯RecyclerView
???????上面接口實現部分已經實現了,那么如何將ItemTouchHelper與RecyclerView建立關聯呢?
???????接下來就是把這個輔助類綁定到RecyclerView,代碼實現如下:
mTaskAdapter = new TaskAdapter(this, availableList);
ItemTouchHelper.Callback callback = new TaskItemTouchHelper(mTaskAdapter);
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);
itemTouchHelper.attachToRecyclerView(mRecyclerView);
???????關聯只需要調用attachToRecyclerView
就好了。
2.3.設置分割線
???????通過RecyclerView的抽象靜態內部類ItemDecoration來實現;
public class SpaceItemDecoration extends RecyclerView.ItemDecoration {
private int mItemSpace;
private int mTopSpace;
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
calculateOutRect(outRect, view, parent);
}
public SpaceItemDecoration(int space, int topSpace) {
this.mItemSpace = space;
this.mTopSpace = topSpace;
}
private void calculateOutRect(Rect outRect, View view, RecyclerView parent) {
RecyclerView.Adapter adapter = parent.getAdapter();
if (adapter != null) {
int count = adapter.getItemCount();
int position = parent.getChildPosition(view);
switch (count) {
case 1:
outRect.top = (SCREEN_HEIGHT - ITEM_HEIGHT) / 2;
outRect.right = (SCREEN_WIDTH - ITEM_WIDTH) / 2;
break;
case 2:
if (position % 2 == 0) {
outRect.top = mTopSpace;
} else {
outRect.top = 0;
}
outRect.right = (SCREEN_WIDTH - ITEM_WIDTH) / 2;
break;
case 3:
case 4:
if (position % 2 == 0) {
outRect.top = mTopSpace;
} else {
outRect.top = 0;
}
if (position < 2) {
outRect.right = (SCREEN_WIDTH - ITEM_WIDTH * 2 - mItemSpace) / 2;
}
outRect.left = mItemSpace;
break;
default:
if (position % 2 == 0) {
outRect.top = mTopSpace;
} else {
outRect.top = 0;
}
if (position < 2) {
outRect.right = mItemSpace;
}
outRect.left = mItemSpace;
break;
}
}
}
}
???????代碼實現也比較簡單,通過addItemDecoration()來建立關聯就可以了;
TaskGridLayoutManager gridLayoutManager = new TaskGridLayoutManager(this, 2,
RecyclerView.HORIZONTAL, true);
mRecyclerView.setLayoutManager(gridLayoutManager);
mRecyclerView.addItemDecoration(new SpaceItemDecoration(100, 150));
???????傳入不同Item之間的間隔,比如:top和gap,在加載Item時會回調getItemOffsets來獲取該Item對應的Rect,然后再內部進行處理最終確定Item對應的Rect就可以了。
2.4.UI強調
???????在平時的滑動或拖拽交互中,為了區分選中的Item,會對其進行強調,比如:選中的item放大、背景高亮等。
???????此處會用到ItemTouchHelper.Callback中的其他兩個方法,onSelectedChanged
和clearView
,在選中時改變視圖顯示,結束時再進行恢復。
2.4.1.onSelectedChanged
???????拖拽或滑動發生改變時回調,這時我們可以修改item的視圖;
@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
//judge state for selected
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
swipeAnimation(viewHolder, true);
}
super.onSelectedChanged(viewHolder, actionState);
}
???????actionState對應三種state:
???????ACTION_STATE_IDLE:空閑狀態
???????ACTION_STATE_SWIPE:滑動狀態
???????ACTION_STATE_DRAG:拖拽狀態
2.4.2.clearView
???????拖拽或滑動結束時回調,這時我們要把改變后的item視圖恢復到初始狀態
@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
swipeAnimation(viewHolder, false);
super.clearView(recyclerView, viewHolder);
}
???????在拖拽或滑動開始和結束時,通過swipeAnimation()進行強調:
private void swipeAnimation(RecyclerView.ViewHolder viewHolder, boolean isStart) {
float[] floatPram;
if (isStart) {
floatPram = new float[]{1.0f, 1.05f};
} else {
floatPram = new float[]{1.05f, 1.0f};
}
AnimatorSet animatorSet = new AnimatorSet();
ObjectAnimator scaleX = ObjectAnimator.ofFloat(viewHolder.itemView, "scaleX", floatPram);
ObjectAnimator scaleY = ObjectAnimator.ofFloat(viewHolder.itemView, "scaleY", floatPram);
scaleX.setDuration(200);
scaleY.setDuration(200);
animatorSet.playTogether(scaleX,scaleY);
animatorSet.start();
}
三.源碼分析
???????在前面的功能實現代碼中,可以看到,在創建完ItenTouchHelper及Callback后,會通過attachToRecyclerView()建立關聯,本文就從該方法開始分析:
3.1.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();
}
}
???????從該方法可以看到,主要操作如下:
???????a.首先進行重復判斷,如果是相同的recyclerview,直接返回;
???????b.如果mRecyclerView不為空,調用了destroyCallbacks,在destroyCallbacks里面對mRecyclerView進行了一些移除和回收操作,說明只能綁定到一個RecyclerView;同時,注意這里判斷的主體是mRecyclerView,不是我們傳進來的recyclerView,而且我們傳進來的recyclerView是支持Nullable的,所以我們可以傳個空值走到destroyCallbacks里來做解綁操作
???????c.最后當傳入的recyclerView不為空時,調用setupCallbacks();
???????前面分析到,destroyCallbacks會進行一些移除和回收操作,那么setupCallbacks()應該是執行初始化操作,一起看一下具體實現:
3.2.setupCallbacks
private void setupCallbacks() {
ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());
mSlop = vc.getScaledTouchSlop();
mRecyclerView.addItemDecoration(this);
mRecyclerView.addOnItemTouchListener(mOnItemTouchListener);
mRecyclerView.addOnChildAttachStateChangeListener(this);
startGestureDetection();
}
???????可以看到,在setupCallbacks()內部會執行了四個方法:
???????1.執行addItemDecoration(this)將自身加入到RecyclerView中的mItemDecorations進行管理,注意:ItemTouchHelper是繼承了RecyclerView.ItemDecoration;
???????2.執行addOnItemTouchListener()對RecyclerView的Item觸摸事件進行監聽處理;
???????3.執行addOnChildAttachStateChangeListener對child View是否移除Window進行回調監聽;
???????4.執行startGestureDetection()來進行手勢識別監聽;
3.3.mOnItemTouchListener
private final OnItemTouchListener mOnItemTouchListener = new OnItemTouchListener() {
@Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,
@NonNull MotionEvent event) {
mGestureDetector.onTouchEvent(event);
final int action = event.getActionMasked();
if (action == MotionEvent.ACTION_DOWN) {
.................
if (mSelected == null) {
final RecoverAnimation animation = findAnimation(event);
if (animation != null) {
.................
select(animation.mViewHolder, animation.mActionState);
updateDxDy(event, mSelectedFlags, 0);
}
}
} else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
mActivePointerId = ACTIVE_POINTER_ID_NONE;
select(null, ACTION_STATE_IDLE);
} else if (mActivePointerId != ACTIVE_POINTER_ID_NONE) {
final int index = event.findPointerIndex(mActivePointerId);
if (index >= 0) {
checkSelectForSwipe(action, event, index);
}
}
}
@Override
public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent event) {
mGestureDetector.onTouchEvent(event);
..................
final int action = event.getActionMasked();
final int activePointerIndex = event.findPointerIndex(mActivePointerId);
if (activePointerIndex >= 0) {
checkSelectForSwipe(action, event, activePointerIndex);
}
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) {
moveIfNecessary(viewHolder);
}
break;
}
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (!disallowIntercept) {
return;
}
select(null, ACTION_STATE_IDLE);
}
};
???????該方法主要功能就是監聽MotionEvent進行判斷處理,然后執行對不同的手勢進行不同的處理,主要有以下三個方法:
???????a.select
???????b.checkSelectForSwipe
???????c.moveIfNecessary
3.3.1.select
void select(@Nullable ViewHolder selected, int actionState) {
if (selected == mSelected && actionState == mActionState) {
return;
}
.........
if (mSelected != null) {
if (prevSelected.itemView.getParent() != null) {
final float targetTranslateX, targetTranslateY;
switch (swipeDir) {
case LEFT:
case RIGHT:
case START:
case END:
targetTranslateY = 0;
targetTranslateX = Math.signum(mDx) * mRecyclerView.getWidth(); break;
............
}
...........
} else {
removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView); mCallback.clearView(mRecyclerView, prevSelected);
}
}
..........
mCallback.onSelectedChanged(mSelected, mActionState);
mRecyclerView.invalidate();
}
???????在該方法內,這里面主要是在拖拽或滑動時對translateX/Y
的計算和處理,然后通過mCallback.clearView和mCallback.onSelectedChanged進行回調,最后調用invalidate()實時刷新。
3.3.2.checkSelectForSwipe
void checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) {
............//進行過濾判斷
final ViewHolder vh = findSwipedView(motionEvent);
final int movementFlags = mCallback.getAbsoluteMovementFlags(mRecyclerView, vh);
final int swipeFlags = (movementFlags & ACTION_MODE_SWIPE_MASK)
>> (DIRECTION_FLAG_COUNT * ACTION_STATE_SWIPE);
if (swipeFlags == 0) {
return;
}
......................
//最終滿足swipe的操作
select(vh, ACTION_STATE_SWIPE);
}
???????該方法時對滑動處理的check,最后也是收斂到select()方法統一處理。
3.3.3.moveIfNecessary
void moveIfNecessary(ViewHolder viewHolder) {
if (mRecyclerView.isLayoutRequested()) {
return;
}
if (mActionState != ACTION_STATE_DRAG) {
return;
}
.............
//最終滿足onMove交換的操作
if (mCallback.onMove(mRecyclerView, viewHolder, target)) {
// keep target visible
mCallback.onMoved(mRecyclerView, viewHolder, fromPosition,target, toPosition, x, y);
}
}
???????該方法時檢查拖拽時是否需要交換item,通過mCallback.onMoved進行回調。
3.4.startGestureDetection
private void startGestureDetection() {
mItemTouchHelperGestureListener = new ItemTouchHelperGestureListener();
mGestureDetector = new GestureDetectorCompat(mRecyclerView.getContext(),
mItemTouchHelperGestureListener);
}
3.4.1.ItemTouchHelperGestureListener
private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener {
...........
@Override
public void onLongPress(MotionEvent e) {
.......
View child = findChildView(e);
if (child != null) {
ViewHolder vh = mRecyclerView.getChildViewHolder(child);
if (vh != null) {
...........
if (pointerId == mActivePointerId) {
...............
if (mCallback.isLongPressDragEnabled()) {
select(vh, ACTION_STATE_DRAG);
}
}
}
}
}
}
???????此處主要是對長按事件的處理,最后也是收斂到select()方法統一處理。
簡單總結
???????a.綁定RecyclerView
???????b.注冊觸摸手勢監聽
???????c.根據手勢,先是內部處理各種校驗、位置計算、動畫處理、刷新等,然后回調給ItemTouchHelper.Callback
3.5.ItemDecoration
???????前面講到,本地繼承RecyclerView.ItemDecoration,重寫getItemOffsets可以設置分割線,接下來看一下具體的邏輯實現:
3.5.1.addItemDecoration
public void addItemDecoration(@NonNull ItemDecoration decor) {
addItemDecoration(decor, -1);
}
public void addItemDecoration(@NonNull ItemDecoration decor, int index) {
if (mLayout != null) {
mLayout.assertNotInLayoutOrScroll("Cannot add item decoration during a scroll or"+ " layout");
}
if (mItemDecorations.isEmpty()) {
setWillNotDraw(false);
}
if (index < 0) {
mItemDecorations.add(decor);
} else {
mItemDecorations.add(index, decor);
}
markItemDecorInsetsDirty();
requestLayout();
}
???????可以看到,在執行addItemDecoration()會將該decor加入到mItemDecorations中進行管理,從此處可以看到,ItemDecoration可以是多個,可以單獨對Rect的各個區域進行單獨處理,在存儲之后,看一下是如何用到的?
???????我們知道,ItemDecoration是在RecyclerView進行加載ViewHolder時進行使用,因為要確定各個ViewHolder的顯示位置及顯示大小,那根據這個思路就好分析了,這里要說明一下,RecyclerView的ViewHolder的顯示是由LayoutManager來進行管理的,中間的過程就不一一陳述了,可以直接閱讀源碼,此處直接從LayoutManager的measureChild進行分析:
3.5.2.measureChild
public void measureChild(@NonNull View child, int widthUsed, int heightUsed) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;
final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
getPaddingLeft() + getPaddingRight() + widthUsed, lp.width,
canScrollHorizontally());
final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
getPaddingTop() + getPaddingBottom() + heightUsed, lp.height,
canScrollVertically());
if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
child.measure(widthSpec, heightSpec);
}
}
???????可以看到,在該方法內會調用RecyclerView的getItemDecorInsetsForChild()來獲取到child view對應的Rect,然后來計算child view需要顯示的區域;
3.5.3.getItemDecorInsetsForChild
Rect getItemDecorInsetsForChild(View child) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
.................
final Rect insets = lp.mDecorInsets;
insets.set(0, 0, 0, 0);
final int decorCount = mItemDecorations.size();
for (int i = 0; i < decorCount; i++) {
mTempRect.set(0, 0, 0, 0);
mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
insets.left += mTempRect.left;
insets.top += mTempRect.top;
insets.right += mTempRect.right;
insets.bottom += mTempRect.bottom;
}
lp.mInsetsDirty = false;
return insets;
}
???????可以看到,在該方法內會遍歷mItemDecorations,通過ItemDecoration的getItemOffsets來獲取對應的mTempRect,在賦值給insets,最終返回;
3.5.4.getItemOffsets
???????在本地實現getItemOffsets時,會先調用super.getItemOffsets(),主要用來對Rect進行置空,主要作用是確保每個Item不會相互影響,即每個item的顯示區域可以隨意定義;
@Deprecated
public void getItemOffsets(@NonNull Rect outRect, int itemPosition,@NonNull RecyclerView parent) {
outRect.set(0, 0, 0, 0);
}
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,@NonNull RecyclerView parent, @NonNull State state) {
getItemOffsets(outRect, ((LayoutParams)view.getLayoutParams()).getViewLayoutPosition(),
parent);
}
???????以上就是RecycleView UI實現拖拽即滑動處理的實現及部分源碼實現分析!