从源码来看ItemTouchHelper实现RecyclerView列表的拖拽和侧滑
来源:互联网 发布:怎么下载网络倾听者app 编辑:程序博客网 时间:2024/05/16 18:42
RecyclerView是一个用来替换之前的ListView和GridView的控件,使用的时候,虽然比以前的ListView看起来麻烦,但是其实作为一个高度解耦的控件,复杂一点点换来极大的灵活性,丰富的可操作性,何乐而不为呢。不过今天主要说说它的一个辅助类ItemTouchHelper来实现列表的拖动和滑动删除。
RecyclerView用法(ListView)
1.导入控件包
compile 'com.android.support:support-v13:25.+'
2.布局文件加入控件
<android.support.v7.widget.RecyclerView android:id="@+id/rv_test" android:layout_width="match_parent" android:layout_height="match_parent"></android.support.v7.widget.RecyclerView>
3.定义Adapter
public class TestAdapter extends RecyclerView.Adapter implements TouchCallbackListener { /** * 数据源列表 */ private List<String> mData; /** * 构造方法传入数据 * @param mData */ public TestAdapter(List<String> mData) { this.mData = mData; } /** * 创建用于复用的ViewHolder * @param parent * @param viewType * @return */ @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { ViewHolder vh = new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item,parent,false)); return vh; } /** * 对ViewHolder的控件进行操作 * @param holder * @param position */ @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { if(holder instanceof ViewHolder){ ViewHolder holder1 = (ViewHolder) holder; holder1.tv_test.setText(mData.get(position)); } } /** * * @return 数据的总数 */ @Override public int getItemCount() { return mData.size(); } /** * 长按拖拽时的回调 * @param fromPosition 拖拽前的位置 * @param toPosition 拖拽后的位置 */ @Override public void onItemMove(int fromPosition, int toPosition) { Collections.swap(mData, fromPosition, toPosition); notifyItemMoved(fromPosition, toPosition);//通知Adapter更新 } /** * 滑动时的回调 * @param position 滑动的位置 */ @Override public void onItemSwipe(int position) { mData.remove(position); notifyItemRemoved(position);////通知Adapter更新 } /** * 自定义的ViewHolder内部类,必须继承RecyclerView.ViewHolder(这里用不用static存在争议,没有专门的测试, * 从内存占用来看微乎其微,但是不知道有没有内存泄露的问题) */ public class ViewHolder extends RecyclerView.ViewHolder{ private TextView tv_test; public ViewHolder(View itemView) { super(itemView); tv_test = (TextView) itemView.findViewById(R.id.tv_test); } }}
这里定义RecyclerView的Adapter适配器,必须继承自RecyclerView.Adapter,而且需要在内部定义ViewHolder类,这个跟我们之前使用ListView是一样的,不过在RecyclerView里面这个是必须实现的。还有就是这里我并没有用static,不影响复用,但是内存会不会泄漏呢?
然后里面还有两个在拖拽和滑动时的回调,这里是我们自己定义的一个接口TouchCallbackListener
TouchCallbackListener
public interface TouchCallbackListener { /** * 长按拖拽时的回调 * @param fromPosition 拖拽前的位置 * @param toPosition 拖拽后的位置 */ void onItemMove(int fromPosition, int toPosition); /** * 滑动时的回调 * @param position 滑动的位置 */ void onItemSwipe(int position);}
4.使用ItemTouchHelper实现上下拖拽和滑动删除功能
ItemTouchHelper的构造方法需要传入ItemTouchHelper.Callback来自己定义各种动作时的处理,我们自定义的类如下:
TouchCallback
public class TouchCallback extends ItemTouchHelper.Callback { /** * 自定义的监听接口 */ private TouchCallbackListener mListener; public TouchCallback(TouchCallbackListener listener) { this.mListener = listener; } /** * 定义列表可以怎么滑动(上下左右) * @param recyclerView * @param viewHolder * @return */ @Override public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { //上下滑动 int dragFlag = ItemTouchHelper.UP | ItemTouchHelper.DOWN; //左右滑动 int swipeFlag = ItemTouchHelper.LEFT| ItemTouchHelper.RIGHT; //使用此方法生成标志返回 return makeMovementFlags(dragFlag, swipeFlag); } /** * 拖拽移动时调用的方法 * @param recyclerView 控件 * @param viewHolder 移动之前的条目 * @param target 移动之后的条目 * @return */ @Override public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { mListener.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition()); return true; } /** * 滑动时调用的方法 * @param viewHolder 滑动的条目 * @param direction 方向 */ @Override public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { mListener.onItemSwipe(viewHolder.getAdapterPosition()); } /** * 是否允许长按拖拽 * @return true or false */ @Override public boolean isLongPressDragEnabled() { return true; } /** * 是否允许滑动 * @return true or false */ @Override public boolean isItemViewSwipeEnabled() { return true; }}
5.使用RecyclerView绑定Adapter和ItemTouchHelper
最后在Activity中来使用RecyclerView
public class MainActivity extends AppCompatActivity{ private RecyclerView mRecyclerView; private TestAdapter mTestAdapter; private List<String> mData; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initData(); mRecyclerView = (RecyclerView) findViewById(R.id.rv_test); mRecyclerView.setAdapter(mTestAdapter); //定义布局管理器,这里是ListView。GridLayoutManager对应GridView LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this); //ListView的方向,纵向 linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL); mRecyclerView.setLayoutManager(linearLayoutManager); //添加每一行的分割线// mRecyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST)); ItemTouchHelper helper = new ItemTouchHelper(new TouchCallback(mTestAdapter)); helper.attachToRecyclerView(mRecyclerView); } /** * 初始化模拟数据 */ private void initData() { mData = new ArrayList<>(); String temp; for(int i = 0; i < 99; ++i){ temp = i + "*"; mData.add(temp); } mTestAdapter = new TestAdapter(mData); }
6.添加分割线
RecyclerView默认每一行是没有分割线的,如果需要分割线的话要自己去定义ItemDecoration,这个类可以为每个条目添加额外的视图与效果,我们自己定义的代码如下:
DividerItemDecoration
public class DividerItemDecoration extends RecyclerView.ItemDecoration{ private static final int[] ATTRS = new int[]{ android.R.attr.listDivider//Android默认的分割线效果 }; 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 oritation) { final TypedArray a = context.obtainStyledAttributes(ATTRS); mDivider = a.getDrawable(0); a.recycle(); setOrientation(oritation); } public void setOrientation(int orientation) { if(orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST){ throw new IllegalArgumentException("invalid orientation"); } this.mOrientation = orientation; } @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { if(mOrientation == VERTICAL_LIST){ drawVertical(c, parent); }else { drawHorizontal(c,parent); } } /** * 纵向的列表 * @param c * @param 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); RecyclerView v = new 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); } } /** * 横向的列表 * @param c * @param parent */ 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, View view, RecyclerView parent, RecyclerView.State state) { if(mOrientation == VERTICAL_LIST){ outRect.set(0,0,0,mDivider.getIntrinsicHeight()); }else { outRect.set(0,0,mDivider.getIntrinsicWidth(), 0); } }}
到此就实现了一个支持长按拖拽和滑动删除的列表,很简单,效果就不截图了。
ItemTouchHelper原理
实现拖拽和滑动删除的过程的很简单,并且还有非常流畅的动画。只需要给ItemTouchHelper传入一个我们自己定义的回调即可,但是它的内部是怎么实现的呢?来一步一步看看代码。
首先看看它的类定义:
public class ItemTouchHelper extends RecyclerView.ItemDecoration implements RecyclerView.OnChildAttachStateChangeListener
继承自RecyclerView.ItemDecoration,跟分割线一样,也是通过继承这个类来给每个条目添加效果
然后从它的在外层的使用开始:
ItemTouchHelper helper = new ItemTouchHelper(new TouchCallback(mTestAdapter));helper.attachToRecyclerView(mRecyclerView);
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(); } }
首先判断传入的RecyclerView是否跟已经绑定的相等,如果相等,就直接返回,不过不相等,销毁之前的回调,然后将传入的RecyclerView赋值给全局变量,设置速率,最后调用setupCallbacks初始化
ItemTouchHelper.setupCallbacks
private void setupCallbacks() { ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext()); mSlop = vc.getScaledTouchSlop(); mRecyclerView.addItemDecoration(this); mRecyclerView.addOnItemTouchListener(mOnItemTouchListener); mRecyclerView.addOnChildAttachStateChangeListener(this); initGestureDetector(); }
前两句是获取TouchSlop的值,这个值用于判断是滑动还是点击,然后给RecyclerView添加ItemDecoration(也就是自己),条目的触摸监听,条目的关联状态监听。这里最主要的就是看看mOnItemTouchListener的实现:
ItemTouchHelper.mOnItemTouchListener
private final OnItemTouchListener mOnItemTouchListener = new OnItemTouchListener() { @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 = MotionEventCompat.getActionMasked(event); if (action == MotionEvent.ACTION_DOWN) { mActivePointerId = event.getPointerId(0); mInitialTouchX = event.getX(); mInitialTouchY = event.getY(); 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) { 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 = MotionEventCompat.getActionMasked(event); 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); 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: select(null, ACTION_STATE_IDLE); mActivePointerId = ACTIVE_POINTER_ID_NONE; break; case MotionEvent.ACTION_POINTER_UP: { final int pointerIndex = MotionEventCompat.getActionIndex(event); 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; } } }
这里主要重写了两个方法onInterceptTouchEvent和onTouchEvent,先来看看onInterceptTouchEvent,拦截屏幕事触控的事件,首先是判断单点按下
if (action == MotionEvent.ACTION_DOWN) { //现在追踪的触摸事件 mActivePointerId = event.getPointerId(0); //获取最开始按下的坐标值 mInitialTouchX = event.getX(); mInitialTouchY = event.getY(); //获取速度追踪器(此方法避免重复创建) 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); //更新移动距离x,y的值 updateDxDy(event, mSelectedFlags, 0); } } }
然后是判断取消和单点抬起:
else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { mActivePointerId = ACTIVE_POINTER_ID_NONE; select(null, ACTION_STATE_IDLE);//清除动画
最后执行下面判断点击状态为空:
else if (mActivePointerId != ACTIVE_POINTER_ID_NONE) { // 移动距离超过了临界值,判断是否滑动选择的条目 final int index = event.findPointerIndex(mActivePointerId); if (DEBUG) { Log.d(TAG, "pointer index " + index); } if (index >= 0) { //判断是否滑选择的条目 checkSelectForSwipe(action, event, index); } }
最后如果选择的条目不等于null,返回true,表示拦截触摸事件,接下来执行onTouchEvent方法,只看对触摸动作的判断:
1.按下移动手指:
case MotionEvent.ACTION_MOVE: { // 如果点击序号大于0,表示有点击事件 if (activePointerIndex >= 0) { //更新移动距离 updateDxDy(event, mSelectedFlags, activePointerIndex); //移动ViewHolder moveIfNecessary(viewHolder); //先移除动画 mRecyclerView.removeCallbacks(mScrollRunnable); //执行动画 mScrollRunnable.run(); //重绘RecyclerView mRecyclerView.invalidate(); } break; }
这里来看看mScrollRunnable.run():
final Runnable mScrollRunnable = new Runnable() { @Override public void run() { if (mSelected != null && scrollIfNecessary()) { if (mSelected != null) { //it might be lost during scrolling moveIfNecessary(mSelected); } mRecyclerView.removeCallbacks(mScrollRunnable); //递归调用 ViewCompat.postOnAnimation(mRecyclerView, this); } } };
这里的run方法相当于是一个死循环,在里面又不断调用自己,不断的执行动画,因为选中的条目需要不停的跟随手指的移动,直到判断条件返回FALSE停止执行,然后回到onTouchEvent继续判断
2.当用户保持按下操作,并从你的控件转移到外层控件时,会触发ACTION_CANCEL:
case MotionEvent.ACTION_CANCEL: if (mVelocityTracker != null) { //清除速度追踪器 mVelocityTracker.clear(); }
3.抬起手指
case MotionEvent.ACTION_UP: //清理选择动画 select(null, ACTION_STATE_IDLE); //手指状态置空 mActivePointerId = ACTIVE_POINTER_ID_NONE; break;
4.多点触控抬起
case MotionEvent.ACTION_POINTER_UP: { final int pointerIndex = MotionEventCompat.getActionIndex(event); final int pointerId = event.getPointerId(pointerIndex); if (pointerId == mActivePointerId) { //选择一个新的手指活动点,并且更新x,y的距离 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; mActivePointerId = event.getPointerId(newPointerIndex); updateDxDy(event, mSelectedFlags, pointerIndex); } break; }
根据对OnItemTouchListener的源码分析,我们知道了跟随手指的动画是怎么来实现的,简单来说,就是检测手指的动作,然后不断的重绘,最终就展现在我们面前,在长按上下拖拽时,按住的条目随着手指移动,左右滑动时,条目“飞”出屏幕。不过在实际的项目中,这种侧滑删除的操作肯定不是直接侧滑就执行删除,需要右边有一个删除的按钮来确认,这个也可以在ItemTouchHelper的基础上来改进,后面再说吧。
- 从源码来看ItemTouchHelper实现RecyclerView列表的拖拽和侧滑
- RecyclerView使用ItemTouchHelper实现拖拽和侧滑删除
- 使用ItemTouchHelper类轻松实现RecyclerView的拖拽和侧滑
- RecyclerView ItemTouchHelper实现拖拽,侧滑删除
- RecyclerView进阶:使用ItemTouchHelper实现拖拽和侧滑删除
- RecyclerView之使用ItemTouchHelper和ItemTouchHelper.Callback实现条目拖拽排序
- 使用ItemTouchHelper和RecyclerView实现拖拽移动效果
- Android实现RecyclerView侧滑删除和长按拖拽-ItemTouchHelper
- Android实现RecyclerView侧滑删除和长按拖拽-ItemTouchHelper
- Android实现RecyclerView侧滑删除和长按拖拽-ItemTouchHelper
- Android实现RecyclerView侧滑删除和长按拖拽-ItemTouchHelper
- 利用 ItemTouchHelper 实现 RecyclerView 的侧滑删除
- RecyclerView+ItemTouchHelper实现拖拽滑动
- ItemTouchHelper实现拖拽和侧滑删除
- 使用ItemTouchHelper轻松实现RecyclerView拖拽排序和滑动删除
- 使用ItemTouchHelper轻松实现RecyclerView拖拽排序和滑动删除
- ItemTouchHelper实现RecyclerView条目拖拽移动、滑动删除
- ItemTouchHelper类及RecyclerView的item滑动删除和拖拽案例
- 5/12 cnn 手写字体识别代码学习笔记
- Spark学习—统计文件单词出现次数
- Playrix Codescapes Cup (Codeforces Round #413, rated, Div. 1 + Div. 2) C Fountains 乱搞
- Android应用第一次安装成功点击“打开”后Home键切出应用后再点击桌面图标返回导致应用重启问题
- computed与methods的异同
- 从源码来看ItemTouchHelper实现RecyclerView列表的拖拽和侧滑
- 调度脚本
- unsigned和signed、const和define的区别
- java将数据库结果集封装成Map, java获取数据库字段
- Mysql5.7 的一些特点
- shell-test、[]用法
- [李景山php] 深入理解PHP内核[读书笔记]--第四章:函数的实现 --简介
- JS中setInterval和setTimeout的区别
- 数值的整数次方