(二十八)RecyclerView ItemTouchHelper 源码分析以及拓展
来源:互联网 发布:华康字体淘宝可以用吗 编辑:程序博客网 时间:2024/06/05 06:20
版权声明:本文为博主原创文章,未经博主允许不得转载。
本文纯个人学习笔记,由于水平有限,难免有所出错,有发现的可以交流一下。
一、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; }
效果:
四、附
代码链接:http://download.csdn.net/download/qq_18983205/10104020
- (二十八)RecyclerView ItemTouchHelper 源码分析以及拓展
- RecyclerView ItemTouchHelper源码分析扩展
- ItemTouchHelper源码分析(上)
- ItemTouchHelper源码分析(中)
- RecyclerView源码详解(第一篇ItemTouchHelper源码详解)
- Android recyclerview源码分析(二)
- RecyclerView ItemTouchHelper
- ItemTouchHelper源码分析 手势分析OnTouchEvent
- recyclerview万能适配器用法以及源码分析
- LDA理解以及源码分析(二)
- RecyclerView爱恨情仇之ItemTouchHelper
- RecyclerView使用ItemTouchHelper
- Redis源码分析(二十八)--- object创建和释放redisObject对象
- (十八)事件分发-源码分析
- ItemTouchHelper源码分析 拖拽到屏幕边缘的处理
- 源码分析 ItemTouchHelper手势的入口 (OnInterceptTouchEvent onLongPress等)
- 从源码来看ItemTouchHelper实现RecyclerView列表的拖拽和侧滑
- Android recyclerview源码分析(一)
- P2592 [ZJOI2008]生日聚会
- <C++> const总结
- 坚持#第230天~零基础自学云计算基础语言应用之python第6节
- HDU5236 Article(期望dp)
- XJOI泡泡糖
- (二十八)RecyclerView ItemTouchHelper 源码分析以及拓展
- [BZOJ4870][SHOI2017]组合数问题 DP+矩阵快速幂
- xUtils的4个用法
- 【模板】线段树区间修改、区间求和、查询最值
- Balanced Binary Tree:判断一棵树是否为平衡二叉树
- 归并排序
- Python菜鸟教程(一)-爬b站数据
- 实战命令
- [BZOJ4488][JSOI2015]最大公约数 DP+STL