• 怎么简单实现菜单拖拽排序的功能


    1、效果

    在这里插入图片描述

    2、简介

    本文主角是ItemTouchHelper。
    它是RecyclerView对于item交互处理的一个「辅助类」,主要用于拖拽以及滑动处理。
    以接口实现的方式,达到配置简单、逻辑解耦、职责分明的效果,并且支持所有的布局方式。

    3、功能拆解

    在这里插入图片描述

    4、功能实现

    4.1、实现接口

    自定义一个类,实现ItemTouchHelper.Callback接口,然后在实现方法中根据需求简单配置即可。

    class DragCallBack(adapter: DragAdapter, data: MutableList<String>) : ItemTouchHelper.Callback() {
    }
    复制代码
    
    • 1
    • 2
    • 3

    ItemTouchHelper.Callback必须实现的3个方法:

    • getMovementFlags
    • onMove
    • onSwiped

    其他方法还有onSelectedChanged、clearView等

    4.1.1、getMovementFlags

    用于创建交互方式,交互方式分为两种:

    1.拖拽,网格布局支持上下左右,列表只支持上下(LEFT、UP、RIGHT、DOWN)
    2.滑动,只支持前后(START、END)

    最后,通过makeMovementFlags把结果返回回去,makeMovementFlags接收两个参数,dragFlags和swipeFlags,即上面拖拽和滑动组合的标志位。

     override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
            var dragFlags = 0
            var swipeFlags = 0
            when (recyclerView.layoutManager) {
                is GridLayoutManager -> {
                    // 网格布局
                    dragFlags = ItemTouchHelper.LEFT or ItemTouchHelper.UP or ItemTouchHelper.RIGHT or ItemTouchHelper.DOWN
                    return makeMovementFlags(dragFlags, swipeFlags)
                }
                is LinearLayoutManager -> {
                    // 线性布局
                    dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
                    swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END
                    return makeMovementFlags(dragFlags, swipeFlags)
                }
                else -> {
                    // 其他情况可自行处理
                    return 0
                }
            }
        }
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    4.1.2、onMove

    拖拽时回调,这里我们主要对起始位置和目标位置的item做一个数据交换,然后刷新视图显示。

       override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
            // 起始位置
            val fromPosition = viewHolder.adapterPosition
            // 结束位置
            val toPosition = target.adapterPosition
            // 固定位置
            if (fromPosition == mAdapter.fixedPosition || toPosition == mAdapter.fixedPosition) {
                return false
            }
            // 根据滑动方向 交换数据
            if (fromPosition < toPosition) {
                // 含头不含尾
                for (index in fromPosition until toPosition) {
                    Collections.swap(mData, index, index + 1)
                }
            } else {
                // 含头不含尾
                for (index in fromPosition downTo toPosition + 1) {
                    Collections.swap(mData, index, index - 1)
                }
            }
            // 刷新布局
            mAdapter.notifyItemMoved(fromPosition, toPosition)
            return true
        }
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    4.1.3、onSwiped

    滑动时回调,这个回调方法里主要是做数据和视图的更新操作。

       override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
            if (direction == ItemTouchHelper.START) {
                Log.i(TAG, "START--->向左滑")
            } else {
                Log.i(TAG, "END--->向右滑")
            }
            val position = viewHolder.adapterPosition
            mData.removeAt(position)
            mAdapter.notifyItemRemoved(position)
        }
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    4.2、绑定RecyclerView

    上面接口实现部分我们已经简单写好了,逻辑也挺简单,总共不超过100行代码。
    接下来就是把这个辅助类绑定到RecyclerView。
    RecyclerView显示的实现就是基础的样式,就不展开了,可以查看源码。

           val dragCallBack = DragCallBack(mAdapter, list)
            val itemTouchHelper = ItemTouchHelper(dragCallBack)
            itemTouchHelper.attachToRecyclerView(mBinding.recycleView)
    复制代码
    
    • 1
    • 2
    • 3
    • 4

    绑定只需要调用attachToRecyclerView就好了。
    至此,简单的效果就已经实现了。下面开始优化和进阶的部分。

    4.3、设置分割线

    RecyclerView网格布局实现等分,我们一般先是自定义ItemDecoration,然后调用addItemDecoration来实现的。
    但是我在实现效果的时候遇到一个问题,因为我加了布局切换的功能,在每次切换的时候,针对不同的布局分别设置layoutManager和ItemDecoration,这就导致随着切换次数的增加,item的间隔就越大。
    addItemDecoration,顾名思义是添加,通过查看源码发现RecyclerView内部是有一个ArrayList来维护的,所以当我们重复调用addItemDecoration方法时,分割线是以递增的方式在增加的,并且在绘制的时候会从集合中遍历所有的分割线绘制。
    部分源码:

      @Override
        public void draw(Canvas c) {
            super.draw(c);
    
            final int count = mItemDecorations.size();
            for (int i = 0; i < count; i++) {
                mItemDecorations.get(i).onDrawOver(c, this, mState);
            }
            //...
        }
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    既然知道了问题所在,也大概想到了3种解决办法:

    1.调用addItemDecoration前,先调用removeItemDecoration方法remove掉之前所有的分割线
    2.调用addItemDecoration(@NonNull ItemDecoration decor, int index),通过index来维护
    3.add时通过一个标示来判断,添加过就不添加了

    好像可行,实际上并不太行…因为始终都有两个分割线实例。
    我们再来梳理一下:

    • 两种不同的布局
    • 都有分割线
    • 分割线只需设置一次

    我想到另外一个办法,不对RecyclerView做处理了,既然两种布局都有分割线,是不是可以把分割线合二为一了,然后根据LayoutManager去绘制不同的分割线?
    理论上是可行的,事实上也确实可以…
    自定义分割线:

    class GridSpaceItemDecoration(private val spanCount: Int, private val spacing: Int = 20, private var includeEdge: Boolean = false) :
        RecyclerView.ItemDecoration() {
    
        override fun getItemOffsets(outRect: Rect, view: View, recyclerView: RecyclerView, state: RecyclerView.State) {
            recyclerView.layoutManager?.let {
                when (recyclerView.layoutManager) {
                    is GridLayoutManager -> {
                        val position = recyclerView.getChildAdapterPosition(view) // 获取item在adapter中的位置
                        val column = position % spanCount // item所在的列
                        if (includeEdge) {
                            outRect.left = spacing - column * spacing / spanCount
                            outRect.right = (column + 1) * spacing / spanCount
                            if (position < spanCount) {
                                outRect.top = spacing
                            }
                            outRect.bottom = spacing
                        } else {
                            outRect.left = column * spacing / spanCount
                            outRect.right = spacing - (column + 1) * spacing / spanCount
                            if (position >= spanCount) {
                                outRect.top = spanCount
                            }
                            outRect.bottom = spacing
                        }
                    }
                    is LinearLayoutManager -> {
                        outRect.top = spanCount
                        outRect.bottom = spacing
                    }
                }
            }
        }
    
    }
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    4.4、选中放大/背景变色

    为了提升用户体验,可以在拖拽的时候告诉用户当前拖拽的是哪个item,比如选中的item放大、背景高亮等。

    • 网格布局,选中变大
    • 列表布局,背景变色

    这里用到ItemTouchHelper.Callback中的两个方法,onSelectedChanged和clearView,我们需要在选中时改变视图显示,结束时再恢复。

    4.4.1、onSelectedChanged

    拖拽或滑动 发生改变时回调,这时我们可以修改item的视图

      override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
            if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
                viewHolder?.let {
                    // 因为拿不到recyclerView,无法通过recyclerView.layoutManager来判断是什么布局,所以用item的宽度来判断
                    // itemView.width > 500 用这个来判断是否是线性布局,实际取值自己看情况
                    if (it.itemView.width > 500) {
                        // 线性布局 设置背景颜色
                        val drawable = it.itemView.background as GradientDrawable
                        drawable.color = ContextCompat.getColorStateList(it.itemView.context, R.color.greenDark)
                    } else {
                        // 网格布局 设置选中放大
                        ViewCompat.animate(it.itemView).setDuration(200).scaleX(1.3F).scaleY(1.3F).start()
                    }
                }
            }
            super.onSelectedChanged(viewHolder, actionState)
        }
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    actionState:

    • ACTION_STATE_IDLE 空闲状态
    • ACTION_STATE_SWIPE 滑动状态
    • ACTION_STATE_DRAG 拖拽状态

    4.4.2、clearView

    拖拽或滑动 结束时回调,这时我们要把改变后的item视图恢复到初始状态

      override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
            // 恢复显示
            // 这里不能用if判断,因为GridLayoutManager是LinearLayoutManager的子类,改用when,类型推导有区别
            when (recyclerView.layoutManager) {
                is GridLayoutManager -> {
                    // 网格布局 设置选中大小
                    ViewCompat.animate(viewHolder.itemView).setDuration(200).scaleX(1F).scaleY(1F).start()
                }
                is LinearLayoutManager -> {
                    // 线性布局 设置背景颜色
                    val drawable = viewHolder.itemView.background as GradientDrawable
                    drawable.color = ContextCompat.getColorStateList(viewHolder.itemView.context, R.color.greenPrimary)
                }
            }
            super.clearView(recyclerView, viewHolder)
        }
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    4.5、固定位置

    在实际需求中,交互可能要求我们第一个菜单不可以变更顺序,只能固定,比如效果中的第一个菜单「推荐」固定在首位这种情况。

    4.5.1、修改adapter

    定义一个固定值,并设置不同的背景色和其他菜单区分开。

    class DragAdapter(private val mContext: Context, private val mList: List<String>) : RecyclerView.Adapter<DragAdapter.ViewHolder>() {
    
        val fixedPosition = 0 // 固定菜单
    
        override fun onBindViewHolder(holder: ViewHolder, position: Int) {
            holder.mItemTextView.text = mList[position]
    
            // 第一个固定菜单
            val drawable = holder.mItemTextView.background as GradientDrawable
            if (holder.adapterPosition == 0) {
                drawable.color = ContextCompat.getColorStateList(mContext, R.color.greenAccent)
            }else{
                drawable.color = ContextCompat.getColorStateList(mContext, R.color.greenPrimary)
            }
        }
        //...
    }
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    4.5.1、修改onMove回调

    在onMove方法中判断,只要是固定位置就直接返回false。

    class DragCallBack(adapter: DragAdapter, data: MutableList<String>) : ItemTouchHelper.Callback() {
        /**
         * 拖动时回调
         */
        override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
            // 起始位置
            val fromPosition = viewHolder.adapterPosition
            // 结束位置
            val toPosition = target.adapterPosition
    
    		// 固定位置
            if (fromPosition == mAdapter.fixedPosition || toPosition == mAdapter.fixedPosition) {
                return false
            }
            // ...
            return true
        }
    }
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    虽然第一个菜单无法交换位置了,但是它还是可以拖拽的。
    效果实现了吗,好像也实现了,可是又好像哪里不对,就好像填写完表单点击提交时你告诉我格式不正确一样,你不能一开始就告诉我吗?
    为了进一步提升用户体验,可以让固定位置不可以拖拽吗?
    可以,ItemTouchHelper.Callback中有两个方法:

    • isLongPressDragEnabled 是否可以长按拖拽
    • isItemViewSwipeEnabled 是否可以滑动

    这俩方法默认都是true,所以即使不能交换位置,但默认也是支持操作的。

    4.5.3、重写isLongPressDragEnabled

    以拖拽举例,我们需要重写isLongPressDragEnabled方法把它禁掉,然后再非固定位置的时候去手动开启。

     override fun isLongPressDragEnabled(): Boolean {
        	//return super.isLongPressDragEnabled()
            return false
        }
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5

    禁掉之后什么时候再触发呢?
    因为我们现在的交互是长按进入编辑,那就需要在长按事件中再调用startDrag手动开启

          mAdapter.setOnItemClickListener(object : DragAdapter.OnItemClickListener {
    			//...
                override fun onItemLongClick(holder: DragAdapter.ViewHolder) {
                    if (holder.adapterPosition != mAdapter.fixedPosition) {
                        itemTouchHelper.startDrag(holder)
                    }
                }
            })
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    ok,这样就完美实现了。

    4.6、其他

    4.6.1、position

    因为有拖拽操作,下标其实是变化的,在做相应的操作时,要取实时位置

    holder.adapterPosition
    复制代码
    
    • 1
    • 2

    4.6.2、重置

    不管是拖拽还是滑动,其实本质都是对Adapter内已填充的数据进行操作,实时数据通过Adapter获取即可。
    如果想要实现重置功能,直接拿最开始的原始数据重新塞给Adapter即可。
    Author:yechaoa

    5、源码探索

    看源码时,找对一个切入点,往往能达到事半功倍的效果。
    这里就从绑定RecyclerView开始吧

        val dragCallBack = DragCallBack(mAdapter, list)
            val itemTouchHelper = ItemTouchHelper(dragCallBack)
            itemTouchHelper.attachToRecyclerView(mBinding.recycleView)
    复制代码
    
    • 1
    • 2
    • 3
    • 4

    实例化ItemTouchHelper,然后调用其attachToRecyclerView方法绑定到RecyclerView。

    5.1、attachToRecyclerView

     public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
            if (mRecyclerView == recyclerView) {
                return; // nothing to do
            }
            if (mRecyclerView != null) {
                destroyCallbacks();
            }
            mRecyclerView = recyclerView;
            if (recyclerView != 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();
            }
        }
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    这段代码其实有点意思的,解读一下:

    1.第一个if判断,避免重复操作,直接return
    2.第二个if判断,调用了destroyCallbacks,在destroyCallbacks里面做了一些移除和回收操作,说明只能绑定到一个RecyclerView;同时,注意这里判断的主体是mRecyclerView,不是我们传进来的recyclerView,而且我们传进来的recyclerView是支持Nullable的,所以我们可以传个空值走到destroyCallbacks里来做解绑操作
    3.第三个if判断,当我们传的recyclerView不为空时,调用setupCallbacks

    5.2、setupCallbacks

     private void setupCallbacks() {
            ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());
            mSlop = vc.getScaledTouchSlop();
            mRecyclerView.addItemDecoration(this);
            mRecyclerView.addOnItemTouchListener(mOnItemTouchListener);
            mRecyclerView.addOnChildAttachStateChangeListener(this);
            startGestureDetection();
        }
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这个方法里已经大概可以看出内部实现原理了。
    两个关键点:

    • addOnItemTouchListener
    • startGestureDetection

    通过触摸和手势识别来处理交互显示。

    5.3、mOnItemTouchListener

     private final OnItemTouchListener mOnItemTouchListener = new OnItemTouchListener() {
            @Override
            public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent event) {
                mGestureDetector.onTouchEvent(event);
                if (action == MotionEvent.ACTION_DOWN) {
                    //...
                    if (mSelected == null) {
                        if (animation != null) {
                            //...
                            select(animation.mViewHolder, animation.mActionState);
                        }
                    }
                } else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
                    select(null, ACTION_STATE_IDLE);
                } else if (mActivePointerId != ACTIVE_POINTER_ID_NONE) {
                    //...
                    if (index >= 0) {
                        checkSelectForSwipe(action, event, index);
                    }
                }
                return mSelected != null;
            }
    
            @Override
            public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent event) {
                mGestureDetector.onTouchEvent(event);
                //...
                if (activePointerIndex >= 0) {
                    checkSelectForSwipe(action, event, activePointerIndex);
                }
                switch (action) {
                    case MotionEvent.ACTION_MOVE: {
                        if (activePointerIndex >= 0) {
                            moveIfNecessary(viewHolder);
                        }
                        break;
                    }
                    //...
                }
            }
    
            @Override
            public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
                select(null, ACTION_STATE_IDLE);
            }
        };
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    这段代码删减之后还是有点多,不过没关系,提炼一下,核心通过判断MotionEvent调用了几个方法:

    • select
    • checkSelectForSwipe
    • moveIfNecessary

    5.3.1、select

      void select(@Nullable ViewHolder selected, int actionState) {
            if (selected == mSelected && actionState == mActionState) {
                return;
            }
            //...
            if (mSelected != null) {
                if (prevSelected.itemView.getParent() != null) {
                    final float targetTranslateX, targetTranslateY;
                    switch (swipeDir) {
                        case LEFT:
                        case RIGHT:
                        case START:
                        case END:
                            targetTranslateY = 0;
                            targetTranslateX = Math.signum(mDx) * mRecyclerView.getWidth();
                            break;
                        //...
                    }
                    //...
                } else {
                    removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);
                    mCallback.clearView(mRecyclerView, prevSelected);
                }
            }
            //...
            mCallback.onSelectedChanged(mSelected, mActionState);
            mRecyclerView.invalidate();
        }
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    这里面主要是在拖拽或滑动时对translateX/Y的计算和处理,然后通过mCallback.clearView和mCallback.onSelectedChanged回调给我们,最后调用invalidate()实时刷新。

    5.3.2、checkSelectForSwipe

      void checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) {
            //...
            if (absDx < mSlop && absDy < mSlop) {
                return;
            }
            if (absDx > absDy) {
                if (dx < 0 && (swipeFlags & LEFT) == 0) {
                    return;
                }
                if (dx > 0 && (swipeFlags & RIGHT) == 0) {
                    return;
                }
            } else {
                if (dy < 0 && (swipeFlags & UP) == 0) {
                    return;
                }
                if (dy > 0 && (swipeFlags & DOWN) == 0) {
                    return;
                }
            }
            select(vh, ACTION_STATE_SWIPE);
        }
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    这里是滑动处理的check,最后也是收敛到select()方法统一处理。

    5.3.3、moveIfNecessary

     void moveIfNecessary(ViewHolder viewHolder) {
            if (mRecyclerView.isLayoutRequested()) {
                return;
            }
            if (mActionState != ACTION_STATE_DRAG) {
                return;
            }
            //...
            if (mCallback.onMove(mRecyclerView, viewHolder, target)) {
                // keep target visible
                mCallback.onMoved(mRecyclerView, viewHolder, fromPosition,
                        target, toPosition, x, y);
            }
        }
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    这里检查拖拽时是否需要交换item,通过mCallback.onMoved回调给我们。

    5.4、startGestureDetection

      private void startGestureDetection() {
            mItemTouchHelperGestureListener = new ItemTouchHelperGestureListener();
            mGestureDetector = new GestureDetectorCompat(mRecyclerView.getContext(),
                    mItemTouchHelperGestureListener);
        }
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    5.4.1、ItemTouchHelperGestureListener

      private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener {
            //...
            @Override
            public void onLongPress(MotionEvent e) {
                //...
                View child = findChildView(e);
                if (child != null) {
                    ViewHolder vh = mRecyclerView.getChildViewHolder(child);
                    if (vh != null) {
                        //...
                        if (pointerId == mActivePointerId) {
                            //...
                            if (mCallback.isLongPressDragEnabled()) {
                                select(vh, ACTION_STATE_DRAG);
                            }
                        }
                    }
                }
            }
        }
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    这里主要是对长按事件的处理,最后也是收敛到select()方法统一处理。

    5.5、源码小结

    1.绑定RecyclerView
    2.注册触摸手势监听
    3.根据手势,先是内部处理各种校验、位置计算、动画处理、刷新等,然后回调给ItemTouchHelper.Callback

    事儿大概就是这么个事儿,主要工作都是源码帮我们做了,我们只需要在回调里根据结果处理业务逻辑即可。

    源码附件已经打包好上传到百度云了,大家自行下载即可~

    链接: https://pan.baidu.com/s/14G-bpVthImHD4eosZUNSFA?pwd=yu27
    提取码: yu27
    百度云链接不稳定,随时可能会失效,大家抓紧保存哈。

    如果百度云链接失效了的话,请留言告诉我,我看到后会及时更新~

    开源地址

    码云地址:
    http://github.crmeb.net/u/defu

    Github 地址:
    http://github.crmeb.net/u/defu

    链接:https://juejin.cn/post/7124354102296838151

  • 相关阅读:
    斯坦福NLP课程 | 第12讲 - NLP子词模型
    JAVA IO流File
    Excel VBA高级编程-微信群发(支持发送文件)
    【云原生】Spring Cloud微服务学习路线汇总
    HALCON: 对象(object)从声明(declaration)到结束(finalization)
    Rabin-Karp字符串哈希算法
    Android字体大小dp,sp,px系统设置字体大小变化表现
    基于R语言APSIM模型进阶应用与参数优化、批量模拟
    【Spring篇 | 补充】三级缓存解决循环依赖
    7.10 操作系统的启动
  • 原文地址:https://blog.csdn.net/qq_39221436/article/details/126013858