从源码来看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;                }            }        }

这里主要重写了两个方法onInterceptTouchEventonTouchEvent,先来看看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的基础上来改进,后面再说吧。

1 0
原创粉丝点击