(二十八)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