(二十八)RecyclerView ItemTouchHelper 源码分析以及拓展

版权声明:本文为博主原创文章,未经博主允许不得转载。

本文纯个人学习笔记,由于水平有限,难免有所出错,有发现的可以交流一下。

一、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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值