天天看點

(二十八)RecyclerView ItemTouchHelper 源碼分析以及拓展一、ItemTouchHelper 的使用二、ItemTouchHelper 源碼分析三、ItemTouchHelper 拓展四、附

版權聲明:本文為部落客原創文章,未經部落客允許不得轉載。

本文純個人學習筆記,由于水準有限,難免有所出錯,有發現的可以交流一下。

一、ItemTouchHelper 的使用

1.效果

RecycleView 通過 ItemTouchHelper 實作上下交換,滑動删除的效果。

(二十八)RecyclerView ItemTouchHelper 源碼分析以及拓展一、ItemTouchHelper 的使用二、ItemTouchHelper 源碼分析三、ItemTouchHelper 拓展四、附

側滑點選删除。

(二十八)RecyclerView ItemTouchHelper 源碼分析以及拓展一、ItemTouchHelper 的使用二、ItemTouchHelper 源碼分析三、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();
        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 = ; 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 = ; 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(, , , mDivider.getIntrinsicHeight());
        } else {
            outRect.set(, , mDivider.getIntrinsicWidth(), );
        }
    }
}
           

MyAdapter:

public class MyAdapter extends Adapter<MyAdapter.MyHolder>{
    private List<String> list;

    public MyAdapter() {
        //建立假資料
        list = new ArrayList<>();
        for (int i = ; i < ; 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);
        }
    }

}
           

效果:

(二十八)RecyclerView ItemTouchHelper 源碼分析以及拓展一、ItemTouchHelper 的使用二、ItemTouchHelper 源碼分析三、ItemTouchHelper 拓展四、附

代碼也比較簡單,布局檔案就不貼出來。

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);
           

效果:

(二十八)RecyclerView ItemTouchHelper 源碼分析以及拓展一、ItemTouchHelper 的使用二、ItemTouchHelper 源碼分析三、ItemTouchHelper 拓展四、附

在這裡已經支援拖拽和滑動的效果,隻是拖拽手指松開後,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);
    }
}
           

效果:

(二十八)RecyclerView ItemTouchHelper 源碼分析以及拓展一、ItemTouchHelper 的使用二、ItemTouchHelper 源碼分析三、ItemTouchHelper 拓展四、附

簡單的幾行代碼就實作了比較酷炫的效果,這是谷歌全幫我們封裝好了工具,是以可以很友善的使用。

二、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 - ; i >= ; i--) {
            final RecoverAnimation recoverAnimation = mRecoverAnimations.get();
            mCallback.clearView(mRecyclerView, recoverAnimation.mViewHolder);
        }
        mRecoverAnimations.clear();
        mOverdrawChild = null;
        mOverdrawChildPosition = -;
        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();
                //記錄觸摸點的坐标
                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, );
                    }
                }
            } 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 >= ) {
                    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 >= ) {
                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 >= ) {
                        //計算實際要移動的距離
                        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 ==  ?  : ;
                        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) == ) {
            mDx = Math.max(, mDx);
        }
        if ((directionFlags & RIGHT) == ) {
            mDx = Math.min(, mDx);
        }
        if ((directionFlags & UP) == ) {
            mDy = Math.max(, mDy);
        }
        if ((directionFlags & DOWN) == ) {
            mDy = Math.min(, 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 >= ) {
                        //計算實際要移動的距離
                        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 移到邊沿滾動效果:

(二十八)RecyclerView ItemTouchHelper 源碼分析以及拓展一、ItemTouchHelper 的使用二、ItemTouchHelper 源碼分析三、ItemTouchHelper 拓展四、附

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 = ; 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 = -;
        float dx = , dy = ;
        if (mSelected != null) {
            getSelectedDxDy(mTmpPosition);
            dx = mTmpPosition[];
            dy = mTmpPosition[];
        }
        //調用 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 >= ) {
                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[];
                final float currentTranslateY = mTmpPosition[];

                //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 <= ) {
                            // 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 > ) {
                                // 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.修改滑動動畫

先來看一下效果:

(二十八)RecyclerView ItemTouchHelper 源碼分析以及拓展一、ItemTouchHelper 的使用二、ItemTouchHelper 源碼分析三、ItemTouchHelper 拓展四、附

直接在上面的代碼基礎上進行修改,新增 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 !=  && dX == ) {
            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 = ; 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();
                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, );
                    }
                }
            } 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 >= ) {
                    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 >= ) {
                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 >= ) {

                        //設定标志位為 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 ==  ?  : ;
                        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 = ; 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[];
        child.getLocationOnScreen(location);
        Rect rect = new Rect(location[], location[], location[] + child.getWidth(), location[] + 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);
    }
}
           

效果:

(二十八)RecyclerView ItemTouchHelper 源碼分析以及拓展一、ItemTouchHelper 的使用二、ItemTouchHelper 源碼分析三、ItemTouchHelper 拓展四、附

3.複用問題

由于 RecycleView 的複用機制,在一個 item 滑動後進行整個 RecycleView 的滾動,會導緻後面複用出現顯示問題。

複用導緻顯示問題:

(二十八)RecyclerView ItemTouchHelper 源碼分析以及拓展一、ItemTouchHelper 的使用二、ItemTouchHelper 源碼分析三、ItemTouchHelper 拓展四、附

我們需要記錄被滑動的 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 <= ) {
                            // 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 > ) {
                                // 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(), f);
        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() > ) {
            ViewGroup viewGroup = (ViewGroup) viewHolder.itemView;
            return viewGroup.getChildAt(viewGroup.getChildCount() - );
        } 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 = f;
        mActivePointerId = motionEvent.getPointerId();
        select(vh, ACTION_STATE_SWIPE);

        //重新選擇滑動的 Item 時候,清空前面選擇的 Item 動畫
        if (mPreOpened != null && mPreOpened != vh && mPreOpened != null) {
            closeOpenedPreItem();
        }

        return true;
    }
           

效果:

(二十八)RecyclerView ItemTouchHelper 源碼分析以及拓展一、ItemTouchHelper 的使用二、ItemTouchHelper 源碼分析三、ItemTouchHelper 拓展四、附

四、附

代碼連結:http://download.csdn.net/download/qq_18983205/10104020