版權聲明:本文為部落客原創文章,未經部落客允許不得轉載。
本文純個人學習筆記,由于水準有限,難免有所出錯,有發現的可以交流一下。
一、ItemTouchHelper 的使用
1.效果
RecycleView 通過 ItemTouchHelper 實作上下交換,滑動删除的效果。
側滑點選删除。
2.RecycleView 的 demo
先來一個簡單的 RecycleView 的例子,分割線直接采用了鴻洋大神從 LinearLayoutCompat 源碼中分離的 DividerItemDecoration。
MainActivity:
public class MainActivity extends AppCompatActivity {
private RecyclerView recyclerview;
private ItemTouchHelper itemTouchHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
recyclerview = (RecyclerView)findViewById(R.id.recyclerview);
MyAdapter adapter = new MyAdapter();
recyclerview.setLayoutManager(new LinearLayoutManager(this));
//繪制分割線
recyclerview.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST));
recyclerview.setAdapter(adapter);
}
}
DividerItemDecoration:
public class DividerItemDecoration extends RecyclerView.ItemDecoration {
private static final int[] ATTRS = new int[]{
android.R.attr.listDivider
};
public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
private Drawable mDivider;
private int mOrientation;
public DividerItemDecoration(Context context, int orientation) {
final TypedArray a = context.obtainStyledAttributes(ATTRS);
mDivider = a.getDrawable();
a.recycle();
setOrientation(orientation);
}
public void setOrientation(int orientation) {
if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
throw new IllegalArgumentException("invalid orientation");
}
mOrientation = orientation;
}
@Override
public void onDraw(Canvas c, RecyclerView parent) {
Log.v("onDraw", "onDraw()");
if (mOrientation == VERTICAL_LIST) {
drawVertical(c, parent);
} else {
drawHorizontal(c, parent);
}
}
public void drawVertical(Canvas c, RecyclerView parent) {
final int left = parent.getPaddingLeft();
final int right = parent.getWidth() - parent.getPaddingRight();
final int childCount = parent.getChildCount();
for (int i = ; i < childCount; i++) {
final View child = parent.getChildAt(i);
android.support.v7.widget.RecyclerView v = new android.support.v7.widget.RecyclerView(parent.getContext());
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
final int top = child.getBottom() + params.bottomMargin;
final int bottom = top + mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
public void drawHorizontal(Canvas c, RecyclerView parent) {
final int top = parent.getPaddingTop();
final int bottom = parent.getHeight() - parent.getPaddingBottom();
final int childCount = parent.getChildCount();
for (int i = ; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
final int left = child.getRight() + params.rightMargin;
final int right = left + mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
@Override
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
if (mOrientation == VERTICAL_LIST) {
outRect.set(, , , mDivider.getIntrinsicHeight());
} else {
outRect.set(, , mDivider.getIntrinsicWidth(), );
}
}
}
MyAdapter:
public class MyAdapter extends Adapter<MyAdapter.MyHolder>{
private List<String> list;
public MyAdapter() {
//建立假資料
list = new ArrayList<>();
for (int i = ; i < ; i ++) {
list.add("item " + i);
}
}
@Override
public int getItemCount() {
return list.size();
}
@Override
public void onBindViewHolder(final MyHolder holder, int position) {
holder.tv_name.setText(list.get(position));
}
@Override
public MyHolder onCreateViewHolder(ViewGroup parent, int arg1) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.listitem, parent, false);
return new MyHolder(view);
}
class MyHolder extends ViewHolder {
public TextView tv_name;
public MyHolder(View itemView) {
super(itemView);
tv_name = (TextView)itemView.findViewById(R.id.tv_name);
}
}
}
效果:
代碼也比較簡單,布局檔案就不貼出來。
3.添加拖拽
添加拖拽效果需要用到 ItemTouchHelper 這個類,這個是谷歌提供的實作 Recyclerview 拖拽效果的幫助類。
這是 ItemTouchHelper 的構造函數,它需要一個 Callback 的參數,Callback 是一個抽象類,用來實作與使用者進行互動(即怎麼拖拽)。
public ItemTouchHelper(Callback callback) {
mCallback = callback;
}
自定義 MessageItemTouchCallback:
public class MessageItemTouchCallback extends ItemTouchHelper.Callback {
/**
* 擷取移動跟拖拽的标志,設定哪些方向可以移動,哪些方向可以拖拽
* @param recyclerView
* @param viewHolder
* @return
*/
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
//設定可拖拽方向為上下
int dragFlags = ItemTouchHelper.UP|ItemTouchHelper.DOWN;
//設定可滑動方向為左
int swipeFlags = ItemTouchHelper.LEFT;
return makeMovementFlags(dragFlags, swipeFlags);
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
return false;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
}
}
在 MainActivity 中引用:
ItemTouchHelper.Callback callback = new MessageItemTouchCallback();
itemTouchHelper = new ItemTouchHelper(callback);
itemTouchHelper.attachToRecyclerView(recyclerview);
效果:
在這裡已經支援拖拽和滑動的效果,隻是拖拽手指松開後,item 優化自動回到原來的位置;滑動結束後,會出現空白,一是資料源沒有重新整理,二是 RecycleView 沒有重新整理。
4.效果實作
在 MessageItemTouchCallback 中還有兩個方法,onMove 和 onSwiped,這分别在拖拽跟滑動完成之後調用。為了代碼寫的優雅和較好的封裝性,這邊 item 的回調再采用一個接口進行回調。
ItemTouchHelperAdapterCallback:
public interface ItemTouchHelperAdapterCallback {
/**
* 當拖拽的時候回調
* @param fromPosition
* @param toPosition
* @return
*/
boolean onItemMove(int fromPosition, int toPosition);
/**
* 當側滑删除動作的時候回調
* @param adapterPosition
*/
void onItemSwiped(int adapterPosition);
}
為 MessageItemTouchCallback 添加 onMove 方法和 onSwiped 方法的實作。
MessageItemTouchCallback:
public class MessageItemTouchCallback extends ItemTouchHelper.Callback {
private ItemTouchHelperAdapterCallback adapterCallback;
public MessageItemTouchCallback(ItemTouchHelperAdapterCallback adapterCallback) {
this.adapterCallback = adapterCallback;
}
/**
* 擷取移動跟拖拽的标志,設定哪些方向可以移動,哪些方向可以拖拽
* @param recyclerView 目前 recyclerView
* @param viewHolder 目前操作的 viewHolder
* @return
*/
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
//設定可拖拽方向為上下
int dragFlags = ItemTouchHelper.UP|ItemTouchHelper.DOWN;
//設定可滑動方向為左
int swipeFlags = ItemTouchHelper.LEFT;
return makeMovementFlags(dragFlags, swipeFlags);
}
/**
* 處理拖拽事件
* @param recyclerView 目前 recyclerView
* @param viewHolder 目前拖拽的 viewHolder
* @param target 要拖拽去的目标 viewHolder
* @return
*/
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
//監聽滑動(水準方向、垂直方向)
//1.讓資料集合中的兩個資料進行位置交換
//2.同時還要重新整理RecyclerView
adapterCallback.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
return false;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
// 滑動删除的動作的時候回調
//1.删除資料集合裡面的position位置的資料
//2.重新整理adapter
adapterCallback.onItemSwiped(viewHolder.getAdapterPosition());
}
}
讓 MyAdapter 實作 ItemTouchHelperAdapterCallback 接口。
MyAdapter:
public class MyAdapter extends Adapter<MyAdapter.MyHolder> implements ItemTouchHelperAdapterCallback{
private List<String> list;
...
@Override
public boolean onItemMove(int fromPosition, int toPosition) {
//讓資料集合中的兩個資料進行位置交換
Collections.swap(list, fromPosition, toPosition);
//重新整理 adapter
notifyItemMoved(fromPosition, toPosition);
return false;
}
@Override
public void onItemSwiped(int adapterPosition) {
//删除資料集合裡面的 position位置的資料
list.remove(adapterPosition);
//重新整理 adapter
notifyItemRemoved(adapterPosition);
}
}
在 MainActivity 中初始化 MessageItemTouchCallback 傳入 adapter 作為參數。
public class MainActivity extends AppCompatActivity {
private RecyclerView recyclerview;
private ItemTouchHelper itemTouchHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
ItemTouchHelper.Callback callback = new MessageItemTouchCallback(adapter);
itemTouchHelper = new ItemTouchHelper(callback);
itemTouchHelper.attachToRecyclerView(recyclerview);
}
}
效果:
簡單的幾行代碼就實作了比較酷炫的效果,這是谷歌全幫我們封裝好了工具,是以可以很友善的使用。
二、ItemTouchHelper 源碼分析
ItemTouchHelper.Callback callback = new MessageItemTouchCallback(adapter);
itemTouchHelper = new ItemTouchHelper(callback);
itemTouchHelper.attachToRecyclerView(recyclerview);
這是 ItemTouchHelper 的使用的代碼,我們從 ItemTouchHelper 的 attachToRecyclerView 方法開始分析。
ItemTouchHelper 的 attachToRecyclerView:
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();
}
}
attachToRecyclerView 方法主要有兩大部分,destroyCallbacks() 和 setupCallbacks()。
ItemTouchHelper 的 destroyCallbacks:
private void destroyCallbacks() {
mRecyclerView.removeItemDecoration(this);
mRecyclerView.removeOnItemTouchListener(mOnItemTouchListener);
mRecyclerView.removeOnChildAttachStateChangeListener(this);
// clean all attached
final int recoverAnimSize = mRecoverAnimations.size();
for (int i = recoverAnimSize - ; i >= ; i--) {
final RecoverAnimation recoverAnimation = mRecoverAnimations.get();
mCallback.clearView(mRecyclerView, recoverAnimation.mViewHolder);
}
mRecoverAnimations.clear();
mOverdrawChild = null;
mOverdrawChildPosition = -;
releaseVelocityTracker();
}
destroyCallbacks 主要是進行一些初始化操作,移除監聽、參數置空等。
ItemTouchHelper 的 setupCallbacks:
private void setupCallbacks() {
ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());
mSlop = vc.getScaledTouchSlop();
mRecyclerView.addItemDecoration(this);
mRecyclerView.addOnItemTouchListener(mOnItemTouchListener);
mRecyclerView.addOnChildAttachStateChangeListener(this);
initGestureDetector();
}
setupCallbacks 是真正的将 ItemTouchHelper 和 RecycleView 綁定的操作。
1.mRecyclerView.addItemDecoration(this);
在 setupCallbacks 方法裡面可以看見調用了 addItemDecoration 這個方法,這個在最開始 RecycleView 的 demo 裡面是進行設定分割線的,ItemTouchHelper 也繼承了 RecyclerView.ItemDecoration 這個抽象類。但是這裡不是進行設定分割線。
很多人都以為 RecyclerView.ItemDecoration 就是用來設定 RecycleView 的分割線的,其實不是,隻是因為 RecyclerView.ItemDecoration 的 onDraw 方法有 Canvas 和 RecyclerView,我們可以用這個實作分割線,僅此而已,不是說 RecyclerView.ItemDecoration 就是為了實作分割線。
RecyclerView.ItemDecoration 的作用是對 RecyclerView 進行裝飾,分割線隻是裝飾的一部分。
2.mRecyclerView.addOnItemTouchListene
繼續 ItemTouchHelper 的 setupCallbacks 方法往下,調用 mRecyclerView.addOnItemTouchListener(mOnItemTouchListener)。看名字就知道這是設定觸摸事件的監聽,不論拖拽還是滑動動畫,觸摸事件的監聽都是核心。
addOnItemTouchListener 的參數 mOnItemTouchListener:
/**
* 打斷觸摸事件 TouchEvent
* 主要是手指剛觸摸的時候和手指離開的時候
* 傳回 true 則表示要即将消費這個事件
*/
@Override
public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent event) {
mGestureDetector.onTouchEvent(event);
if (DEBUG) {
Log.d(TAG, "intercept: x:" + event.getX() + ",y:" + event.getY() + ", " + event);
}
final int action = event.getActionMasked();
//手指按下的時候
if (action == MotionEvent.ACTION_DOWN) {
mActivePointerId = event.getPointerId();
//記錄觸摸點的坐标
mInitialTouchX = event.getX();
mInitialTouchY = event.getY();
obtainVelocityTracker();
//mSelected == null 則說明是第一根手指選中的 item
if (mSelected == null) {
final RecoverAnimation animation = findAnimation(event);
if (animation != null) {
mInitialTouchX -= animation.mX;
mInitialTouchY -= animation.mY;
endRecoverAnimation(animation.mViewHolder, true);
if (mPendingCleanup.remove(animation.mViewHolder.itemView)) {
mCallback.clearView(mRecyclerView, animation.mViewHolder);
}
//設定被選中的 mViewHolder
select(animation.mViewHolder, animation.mActionState);
//計算實際要移動的距離
updateDxDy(event, mSelectedFlags, );
}
}
} 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) {
// 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 >= ) {
checkSelectForSwipe(action, event, index);
}
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(event);
}
return mSelected != null;
}
@Override
public void onTouchEvent(RecyclerView recyclerView, MotionEvent event) {
mGestureDetector.onTouchEvent(event);
if (DEBUG) {
Log.d(TAG,
"on touch: x:" + mInitialTouchX + ",y:" + mInitialTouchY + ", :" + event);
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(event);
}
if (mActivePointerId == ACTIVE_POINTER_ID_NONE) {
return;
}
final int action = event.getActionMasked();
final int activePointerIndex = event.findPointerIndex(mActivePointerId);
if (activePointerIndex >= ) {
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 >= ) {
//計算實際要移動的距離
updateDxDy(event, mSelectedFlags, activePointerIndex);
//實作被選中的 item 移到邊沿的時候執行快速移動效果
///檢查是否需要進行 item 的交換
//要的話調用 CallBack 的 onMove 進行交換
moveIfNecessary(viewHolder);
//重新開啟 mScrollRunnable 線程,
//執行真正的滾動
mRecyclerView.removeCallbacks(mScrollRunnable);
mScrollRunnable.run();
//會調用 mRecyclerView 的 onDraw()方法:
mRecyclerView.invalidate();
}
break;
}
case MotionEvent.ACTION_CANCEL:
if (mVelocityTracker != null) {
mVelocityTracker.clear();
}
// fall through
case MotionEvent.ACTION_UP:
select(null, ACTION_STATE_IDLE);
mActivePointerId = ACTIVE_POINTER_ID_NONE;
brak;
case MotionEvent.ACTION_POINTER_UP: {
final int pointerIndex = event.getActionIndex();
final int pointerId = event.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
final int newPointerIndex = pointerIndex == ? : ;
mActivePointerId = event.getPointerId(newPointerIndex);
updateDxDy(event, mSelectedFlags, pointerIndex);
}
break;
}
}
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (!disallowIntercept) {
return;
}
select(null, ACTION_STATE_IDLE);
}
};
ItemTouchHelper 的 updateDxDy:
void updateDxDy(MotionEvent ev, int directionFlags, int pointerIndex) {
final float x = ev.getX(pointerIndex);
final float y = ev.getY(pointerIndex);
// Calculate the distance moved
mDx = x - mInitialTouchX;
mDy = y - mInitialTouchY;
if ((directionFlags & LEFT) == ) {
mDx = Math.max(, mDx);
}
if ((directionFlags & RIGHT) == ) {
mDx = Math.min(, mDx);
}
if ((directionFlags & UP) == ) {
mDy = Math.max(, mDy);
}
if ((directionFlags & DOWN) == ) {
mDy = Math.min(, mDy);
}
}
updateDxDy 是根據前面設定的允許拖拽和滑動方向,進行計算偏移距離,設定有效方向标志位之是以生效也是因為這個方法的原因。
3.手指在螢幕上移動
addOnItemTouchListener 的參數 mOnItemTouchListener:
/**
* 打斷觸摸事件 TouchEvent
* 主要是手指剛觸摸的時候和手指離開的時候
* 傳回 true 則表示要即将消費這個事件
*/
@Override
public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent event) {
...
@Override
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 >= ) {
//計算實際要移動的距離
updateDxDy(event, mSelectedFlags, activePointerIndex);
//實作被選中的 item 移到邊沿的時候執行快速移動效果
///檢查是否需要進行 item 的交換
//要的話調用 CallBack 的 onMove 進行交換
moveIfNecessary(viewHolder);
//重新開啟 mScrollRunnable 線程,
//執行真正的滾動
mRecyclerView.removeCallbacks(mScrollRunnable);
mScrollRunnable.run();
//會調用 mRecyclerView 的 onDraw()方法:
mRecyclerView.invalidate();
}
break;
}
}
}
};
ItemTouchHelper 的 mScrollRunnable:
final Runnable mScrollRunnable = new Runnable() {
@Override
public void run() {
//scrollIfNecessary 是判斷 RecycleView 是否需要進行滾動,需要的話調用 scrollBy 進行滾動
if (mSelected != null && scrollIfNecessary()) {
if (mSelected != null) { //it might be lost during scrolling
//檢查是否需要進行 item 的交換
//要的話調用 CallBack 的 onMove 進行交換
moveIfNecessary(mSelected);
}
mRecyclerView.removeCallbacks(mScrollRunnable);
//相當于 handle.POSTDelay(this)
//遞歸調用 mScrollRunnable
ViewCompat.postOnAnimation(mRecyclerView, this);
}
}
};
item 移到邊沿滾動效果:
ViewCompat.postOnAnimation(mRecyclerView, this) 會調用 RecycleView 的 onDraw方法。
RecycleView 的 onDraw
@Override
public void onDraw(Canvas c) {
super.onDraw(c);
final int count = mItemDecorations.size();
//周遊調用 ItemDecoration 的 onDraw 方法
for (int i = ; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
在 RecycleView 的 onDraw 裡面周遊調用 ItemDecoration 的 onDraw 方法。是以我們添加多個 ItemDecoration 進行修飾的額時候(包括分割線),在這裡都會進行繪制,調用的是同一個畫布,如果被覆寫,就是 ItemDecoration 之間的算法問題。
前面 setupCallbacks 方法中提到,ItemTouchHelper 也繼承了 RecyclerView.ItemDecoration,同時被添加到 RecycleView 的 mItemDecorations,是以 ItemTouchHelper 的 onDraw 方法在 RecycleView 的 onDraw中被調用到。
ItemTouchHelper 的 onDraw:
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
// we don't know if RV changed something so we should invalidate this index.
mOverdrawChildPosition = -;
float dx = , dy = ;
if (mSelected != null) {
getSelectedDxDy(mTmpPosition);
dx = mTmpPosition[];
dy = mTmpPosition[];
}
//調用 ItemTouchHelper.CallBack 的 onDraw
mCallback.onDraw(c, parent, mSelected,
mRecoverAnimations, mActionState, dx, dy);
}
ItemTouchHelper.CallBack 的 onDraw 會調用 ItemTouchHelper.CallBack 的 onChildDraw 方法。
ItemTouchHelper.CallBack 的 onChildDraw:
public void onChildDraw(Canvas c, RecyclerView recyclerView,
ViewHolder viewHolder,
float dX, float dY, int actionState, boolean isCurrentlyActive) {
sUICallback.onDraw(c, recyclerView, viewHolder.itemView, dX, dY, actionState,
isCurrentlyActive);
}
如果說我們沒有重寫 CallBack 的 onChildDraw 方法,那将調用預設的 onChildDraw,也就是上面這段代碼。繼續往下分析。
看一下 sUICallback 在 ItemTouchHelper 中的具體實作:
static {
if (Build.VERSION.SDK_INT >= ) {
sUICallback = new ItemTouchUIUtilImpl.Api21Impl();
} else {
sUICallback = new ItemTouchUIUtilImpl.BaseImpl();
}
}
Api21Impl 繼承自 BaseImpl, Api21Impl 的 onDraw 方法末尾也調用了 BaseImpl 的 onDraw 方法。
BaseImpl 的 onDraw:
@Override
public void onDraw(Canvas c, RecyclerView recyclerView, View view,
float dX, float dY, int actionState, boolean isCurrentlyActive) {
view.setTranslationX(dX);
view.setTranslationY(dY);
}
看到這裡就很明顯了,上面的拖拽跟滑動,不過是通過算法計算出移動的距離,最後 item 調用 setTranslationX 和 setTranslationY 進行偏移。
在不同版本 v7 包源碼略有不同,但差別不是很大。
4.手指和螢幕分離
addOnItemTouchListener 的參數 mOnItemTouchListener:
private final OnItemTouchListener mOnItemTouchListener = new OnItemTouchListener() {
...
switch (action) {
...
case MotionEvent.ACTION_UP:
select(null, ACTION_STATE_IDLE);
mActivePointerId = ACTIVE_POINTER_ID_NONE;
brak;
}
};
在 ACTION_UP 事件的時候,主要調用了 select 這個方法。
ItemTouchHelper 的 select:
void select(ViewHolder selected, int actionState) {
...
getSelectedDxDy(mTmpPosition);
final float currentTranslateX = mTmpPosition[];
final float currentTranslateY = mTmpPosition[];
//RecoverAnimation 就是對一個屬性動畫的封裝
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 <= ) {
// 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 > ) {
// Animation might be ended by other animators during a layout.
// We defer callback to avoid editing adapter during a layout.
//這是支援滑動後松開手處理,在裡面調用 onSwiped
postDispatchSwipe(this, swipeDir);
}
}
// removed from the list after it is drawn for the last time
if (mOverdrawChild == prevSelected.itemView) {
removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);
}
}
};
//動畫執行時間,可以通過重寫 CallBack 的 getAnimationDuration 進行修改
final long duration = mCallback.getAnimationDuration(mRecyclerView, animationType,
targetTranslateX - currentTranslateX, targetTranslateY - currentTranslateY);
rv.setDuration(duration);
mRecoverAnimations.add(rv);
rv.start();
preventLayout = true;
} else {
removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);
mCallback.clearView(mRecyclerView, prevSelected);
}
mSelected = null;
}
...
}
ItemTouchHelper 的 postDispatchSwipe:
void postDispatchSwipe(final RecoverAnimation anim, final int swipeDir) {
// wait until animations are complete.
mRecyclerView.post(new Runnable() {
@Override
public void run() {
if (mRecyclerView != null && mRecyclerView.isAttachedToWindow()
&& !anim.mOverridden
&& anim.mViewHolder.getAdapterPosition() != RecyclerView.NO_POSITION) {
final RecyclerView.ItemAnimator animator = mRecyclerView.getItemAnimator();
// if animator is running or we have other active recover animations, we try
// not to call onSwiped because DefaultItemAnimator is not good at merging
// animations. Instead, we wait and batch.
if ((animator == null || !animator.isRunning(null))
&& !hasRunningRecoverAnim()) {
mCallback.onSwiped(anim.mViewHolder, swipeDir);
} else {
mRecyclerView.post(this);
}
}
}
});
}
postDispatchSwipe 方法主要是調用了 onSwiped,是以 onSwiped 是在滑動動畫執行完之後調用。
三、ItemTouchHelper 拓展
1.修改滑動動畫
先來看一下效果:
直接在上面的代碼基礎上進行修改,新增 Adapter 的布局檔案,這個布局檔案使用 FrameLayout,在原先的 item 布局下再放上一層兩個按鈕的布局,當向左滑動的時候,則把上層的 item 向左滑動,下層的兩個按鈕布局就顯現出來。
list_item_main.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#FF4444">
<LinearLayout
android:id="@+id/view_list_repo_action_container"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="right"
android:orientation="horizontal">
<TextView
android:id="@+id/view_list_repo_action_delete"
android:layout_width="80dp"
android:layout_height="match_parent"
android:gravity="center"
android:padding="12dp"
android:text="Delete"
android:textColor="@android:color/white"/>
<TextView
android:id="@+id/view_list_repo_action_update"
android:layout_width="80dp"
android:layout_height="match_parent"
android:background="#8BC34A"
android:gravity="center"
android:padding="12dp"
android:text="Refresh"
android:textColor="@android:color/white"/>
</LinearLayout>
<include layout="@layout/listitem"/>
</FrameLayout>
MessageItemTouchCallback :
public class MessageItemTouchCallback extends ItemTouchHelper.Callback {
...
@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
if (dY != && dX == ) {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
MyAdapter.MyHolder holder = (MyAdapter.MyHolder) viewHolder;
if (dX < -holder.mActionContainer.getWidth()) {
//最多偏移 mActionContainer 的寬度
dX =- holder.mActionContainer.getWidth();
}
holder.mViewContent.setTranslationX(dX);
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
}
}
MessageItemTouchCallback 需要重寫 onChildDraw,對手指滑動的時候 item 繪制進行重新定義。
2.點選事件
上面代碼運作的時候,滑動動畫是有了,但是點選事件還沒辦法傳遞到 item。這是在 RecycleView 中設定的 mOnItemTouchListener 并沒有把事件繼續往子 View 分發。
RecycleView 的 dispatchOnItemTouchIntercept:
private boolean dispatchOnItemTouchIntercept(MotionEvent e) {
final int action = e.getAction();
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_DOWN) {
mActiveOnItemTouchListener = null;
}
final int listenerCount = mOnItemTouchListeners.size();
for (int i = ; i < listenerCount; i++) {
final OnItemTouchListener listener = mOnItemTouchListeners.get(i);
if (listener.onInterceptTouchEvent(this, e) && action != MotionEvent.ACTION_CANCEL) {
mActiveOnItemTouchListener = listener;
return true;
}
}
return false;
RecycleView 的 dispatchOnItemTouchIntercept 會對所有儲存的 OnItemTouchListener 按順序進行周遊,當 OnItemTouchListener 的 onInterceptTouchEvent 放回 true 的時候,就把這個 OnItemTouchListener 設定為真正的 OnItemTouchListener (這個才是真正生效的)。
在上面使用了 ItemTouchHelper,這也是真正生效的 OnItemTouchListener ,但是 ItemTouchHelper 在處理觸摸事件時候沒有繼續往子 View 進行事件分發,是以子 View 是無法擷取到觸發事件。
這邊采用複制 ItemTouchHelper,然後對 ItemTouchHelper 源碼進行修改的方式。
在 ItemTouchHelper 的 mOnItemTouchListener 中,添加對 MotionEvent.ACTION_UP 事件的處理,當 item 是滑動後的,且事件是 MotionEvent.ACTION_UP,則進行對子 View 的觸摸事件分發。
修改後 ItemTouchHelper 的 mOnItemTouchListener :
private final OnItemTouchListener mOnItemTouchListener = new OnItemTouchListener() {
//新增 boolean 标志位
boolean mClick = false;
@Override
public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent event) {
mGestureDetector.onTouchEvent(event);
if (DEBUG) {
Log.d(TAG, "intercept: x:" + event.getX() + ",y:" + event.getY() + ", " + event);
}
final int action = event.getActionMasked();
if (action == MotionEvent.ACTION_DOWN) {
mActivePointerId = event.getPointerId();
mInitialTouchX = event.getX();
mInitialTouchY = event.getY();
//表示已經按下去了
mClick = true;
obtainVelocityTracker();
if (mSelected == null) {
final RecoverAnimation animation = findAnimation(event);
if (animation != null) {
mInitialTouchX -= animation.mX;
mInitialTouchY -= animation.mY;
endRecoverAnimation(animation.mViewHolder, true);
if (mPendingCleanup.remove(animation.mViewHolder.itemView)) {
mCallback.clearView(mRecyclerView, animation.mViewHolder);
}
select(animation.mViewHolder, animation.mActionState);
updateDxDy(event, mSelectedFlags, );
}
}
} else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
//手指擡起來,即 click 事件
//進行事件分發,要放在 select方法前面,否則 mSelected 會被置空
if (mClick && action == MotionEvent.ACTION_UP) {
doChildClickEvent(event);
}
mActivePointerId = ACTIVE_POINTER_ID_NONE;
select(null, ACTION_STATE_IDLE);
} 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 >= ) {
checkSelectForSwipe(action, event, index);
}
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(event);
}
return mSelected != null;
}
@Override
public void onTouchEvent(RecyclerView recyclerView, MotionEvent event) {
mGestureDetector.onTouchEvent(event);
if (DEBUG) {
Log.d(TAG,
"on touch: x:" + mInitialTouchX + ",y:" + mInitialTouchY + ", :" + event);
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(event);
}
if (mActivePointerId == ACTIVE_POINTER_ID_NONE) {
return;
}
final int action = event.getActionMasked();
final int activePointerIndex = event.findPointerIndex(mActivePointerId);
if (activePointerIndex >= ) {
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 >= ) {
//設定标志位為 false,這樣隻接收滑動後的點選事件
mClick = false;
updateDxDy(event, mSelectedFlags, activePointerIndex);
moveIfNecessary(viewHolder);
mRecyclerView.removeCallbacks(mScrollRunnable);
mScrollRunnable.run();
mRecyclerView.invalidate();
}
break;
}
case MotionEvent.ACTION_CANCEL:
if (mVelocityTracker != null) {
mVelocityTracker.clear();
}
// fall through
case MotionEvent.ACTION_UP:
//手指擡起來,即 click 事件
//進行事件分發,要放在 select方法前面,否則 mSelected 會被置空
if (mClick) {
doChildClickEvent(event);
}
mClick = false;
select(null, ACTION_STATE_IDLE);
mActivePointerId = ACTIVE_POINTER_ID_NONE;
break;
case MotionEvent.ACTION_POINTER_UP: {
//設定标志位為 false,這樣隻接收滑動後的點選事件
mClick = false;
final int pointerIndex = event.getActionIndex();
final int pointerId = event.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
final int newPointerIndex = pointerIndex == ? : ;
mActivePointerId = event.getPointerId(newPointerIndex);
updateDxDy(event, mSelectedFlags, pointerIndex);
}
break;
}
}
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (!disallowIntercept) {
return;
}
select(null, ACTION_STATE_IDLE);
}
};
/**
* 新增事件分發方法
* @param event
*/
private void doChildClickEvent(MotionEvent event) {
if (mSelected == null){
return;
}
View consumeEventView = mSelected.itemView;
if (consumeEventView instanceof ViewGroup) {
consumeEventView = findConsumView((ViewGroup) consumeEventView, event.getRawX(), event.getRawY());
}
//找到要分發的 View
if (consumeEventView != null) {
//performClick 會調用到 mOnClickListener.onClick();
consumeEventView.performClick();
}
}
/**
* 新增擷取點選的 View
* @param parent
* @param x
* @param y
*/
private View findConsumView(ViewGroup parent, float x, float y) {
for (int i = ; i < parent.getChildCount(); i ++) {
View child = parent.getChildAt(i);
//控件不可見,跳過
if (child.getVisibility() != View.VISIBLE) {
continue;
}
//如果是 ViewGroup,進行遞歸
if (child instanceof ViewGroup ){
child = findConsumView((ViewGroup) child, x, y);
if (child != null) {
return child;
}
} else {
if (isInBounds((int)x, (int)y, child)) {
return child;
}
}
}
//子 View 都沒有的時候判斷本身
if (isInBounds((int)x, (int)y, parent)) {
return parent;
}
return null;
}
/**
* 新增判斷點是否在子 View 上
* @param x
* @param y
* @param child
*/
private boolean isInBounds(int x, int y, View child) {
int[] location = new int[];
child.getLocationOnScreen(location);
Rect rect = new Rect(location[], location[], location[] + child.getWidth(), location[] + child.getHeight());
if (rect.contains(x, y) && ViewCompat.hasOnClickListeners(child) &&
child.getVisibility() == View.VISIBLE) {
return true;
}
return false;
}
這樣在 item 中就可以接收到點選事件,如果說單純是為了解決這個問題,也可以值重寫 mOnItemTouchListener, 然後自己調用 mRecyclerView.addOnItemTouchListener(mOnItemTouchListener)。
在 MyAdapter 中添加監聽事件:
public class MyAdapter extends Adapter<MyAdapter.MyHolder> implements ItemTouchHelperAdapterCallback{
...
@Override
public void onBindViewHolder(final MyHolder holder, final int position) {
holder.tv_name.setText(list.get(position));
//添加監聽
holder.tv_delete.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
doDelete(holder.getAdapterPosition());
}
});
}
...
private void doDelete(int adapterPosition) {
list.remove(adapterPosition);
notifyItemRemoved(adapterPosition);
}
}
效果:
3.複用問題
由于 RecycleView 的複用機制,在一個 item 滑動後進行整個 RecycleView 的滾動,會導緻後面複用出現顯示問題。
複用導緻顯示問題:
我們需要記錄被滑動的 item,當進行滾動等操作時候需要對這個 item 進行還原或回收。
定義個全局變量表示已經被滑動的 item,根據上面的源碼分析,我們要記錄這個滑動的 item,可以在滑動動畫執行完之後進行指派。
ItemTouchHelper:
ViewHolder mPreOpened = null;
...
void select(ViewHolder selected, int actionState) {
...
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 <= ) {
// 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;
//把目前執行動畫的 Item 儲存下來
mPreOpened = prevSelected;
if (swipeDir > ) {
// 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);
}
}
};
...
}
對 RecycleView 添加滾動監聽,判斷是否有已經滑動的 item,有的話進行還原。
ItemTouchHelper 的 attachToRecyclerView:
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();
//添加滾動監聽
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_DRAGGING && mPreOpened != null) {
closeOpenedPreItem();
}
}
});
}
}
/**
* 新增關閉動畫的方法
*/
private void closeOpenedPreItem() {
final View view = getItemFrontView(mPreOpened);
if (mPreOpened == null || view == null){
return;
}
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, "translationX", view.getTranslationX(), f);
objectAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
if (mPreOpened != null){
mCallback.clearView(mRecyclerView, mPreOpened);
}
if (mPreOpened != null){
mPendingCleanup.remove(mPreOpened.itemView);
}
endRecoverAnimation(mPreOpened, true);
mPreOpened = mSelected;
}
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
}
});
objectAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
objectAnimator.start();
}
/**
* 新增擷取 item 最上面的子 View
* @param viewHolder
* @return
*/
public View getItemFrontView(ViewHolder viewHolder) {
if (viewHolder == null){
return null;
}
if (viewHolder.itemView instanceof ViewGroup &&
((ViewGroup) viewHolder.itemView).getChildCount() > ) {
ViewGroup viewGroup = (ViewGroup) viewHolder.itemView;
return viewGroup.getChildAt(viewGroup.getChildCount() - );
} else {
return viewHolder.itemView;
}
}
這裡隻能記錄一個被滑動了的 item ,為了避免有多個 item 被滑動的時候無法全部還原,在重新選擇滑動 item 的時候,也進行判斷。
ItemTouchHelper 的 checkSelectForSwipe:
/**
* Checks whether we should select a View for swiping.
*/
boolean checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) {
...
mDx = mDy = f;
mActivePointerId = motionEvent.getPointerId();
select(vh, ACTION_STATE_SWIPE);
//重新選擇滑動的 Item 時候,清空前面選擇的 Item 動畫
if (mPreOpened != null && mPreOpened != vh && mPreOpened != null) {
closeOpenedPreItem();
}
return true;
}
效果:
四、附
代碼連結:http://download.csdn.net/download/qq_18983205/10104020