版权声明:本文为博主原创文章,未经博主允许不得转载。
本文纯个人学习笔记,由于水平有限,难免有所出错,有发现的可以交流一下。
一、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(0);
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 = 0; 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 = 0; 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(0, 0, 0, mDivider.getIntrinsicHeight());
} else {
outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
}
}
}
MyAdapter:
public class MyAdapter extends Adapter<MyAdapter.MyHolder>{
private List<String> list;
public MyAdapter() {
//建立假数据
list = new ArrayList<>();
for (int i = 0; i < 20; 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 - 1; i >= 0; i--) {
final RecoverAnimation recoverAnimation = mRecoverAnimations.get(0);
mCallback.clearView(mRecyclerView, recoverAnimation.mViewHolder);
}
mRecoverAnimations.clear();
mOverdrawChild = null;
mOverdrawChildPosition = -1;
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(0);
//记录触摸点的坐标
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, 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) {
// 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 >= 0) {
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 >= 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) {
//计算实际要移动的距离
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 == 0 ? 1 : 0;
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) == 0) {
mDx = Math.max(0, mDx);
}
if ((directionFlags & RIGHT) == 0) {
mDx = Math.min(0, mDx);
}
if ((directionFlags & UP) == 0) {
mDy = Math.max(0, mDy);
}
if ((directionFlags & DOWN) == 0) {
mDy = Math.min(0, 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 >= 0) {
//计算实际要移动的距离
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 = 0; 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 = -1;
float dx = 0, dy = 0;
if (mSelected != null) {
getSelectedDxDy(mTmpPosition);
dx = mTmpPosition[0];
dy = mTmpPosition[1];
}
//调用 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 >= 21) {
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[0];
final float currentTranslateY = mTmpPosition[1];
//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 <= 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.
//这是支持滑动后松开手处理,在里面调用 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 != 0 && dX == 0) {
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 = 0; 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(0);
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, 0);
}
}
} 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 >= 0) {
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 >= 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) {
//设置标志位为 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 == 0 ? 1 : 0;
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 = 0; 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[2];
child.getLocationOnScreen(location);
Rect rect = new Rect(location[0], location[1], location[0] + child.getWidth(), location[1] + 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 <= 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;
//把当前执行动画的 Item 保存下来
mPreOpened = prevSelected;
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);
}
}
};
...
}
对 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(), 0f);
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() > 1) {
ViewGroup viewGroup = (ViewGroup) viewHolder.itemView;
return viewGroup.getChildAt(viewGroup.getChildCount() - 1);
} 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 = 0f;
mActivePointerId = motionEvent.getPointerId(0);
select(vh, ACTION_STATE_SWIPE);
//重新选择滑动的 Item 时候,清空前面选择的 Item 动画
if (mPreOpened != null && mPreOpened != vh && mPreOpened != null) {
closeOpenedPreItem();
}
return true;
}
效果: