高端操作!实现RecyclerView的上下拖拽

4,369 阅读7分钟

写在前面

最近工作强度好大,一天能敲10小时以上的代码,敲的我头疼。代码写多了,突然想起来,好像真的很久没发技术文了,原因有很多,就不说了。。都是借口,今天分享内容也是工作时遇上的一个小需求,觉得挺有意思,那就写篇文章吧!   需求描述大概是这样,一个页面有一个列表,列表里有很多item,需要支持用户拖拽其中item到不同的位置,来对列表项进行重新排序。

要实现的效果大概如下:

1_实现效果演示

除去与业务相关的部分,我们只需关注如何让列表item支持上下拖拽就行,这也是这个需求的关键。

我们组安卓岗在半年前已经全部用kotlin进行开发了,所以后续我的文章也会以kotlin为主进行demo的编写。一些还没学过kotlin的朋友也不用担心,kotlin和java很像,只要你熟悉java,相信你也是可以看得懂的。

那么应该如何实现呢?我们需要写个接口去监听每个item的当前状态(是否被拖动)以及其当前所在的位置吗?不需要

得益于RecyclerView优秀的封装,系统内部默认提供了这样的接口给我们去调用。

ItemTouchHelper

简单介绍下这个类,系统将这些接口封装到了这个类里,看看这个类的描述,它继承自RecyclerView.ItemDecoration,实现了RecyclerView.OnChildAttachStateChangeListener接口。

public class ItemTouchHelper extends RecyclerView.ItemDecoration
        implements RecyclerView.OnChildAttachStateChangeListener {}

ItemDecoration这个类比较熟悉,它可以用来让不同的子View的四周拥有不同宽度/高度的offset,换句话说,可以控制子View显示的位置。

而OnChildAttachStateChangeListener这个接口,则是用来回调当子View Attach或Detach到RecyclerView时的事件。

那怎么使用这个ItemTouchHelper呢?

val callback = object : Callback {...}
val itemTouchHelper = ItemTouchHelperImpl(callback)
itemTouchHelper.attachToRecyclerView(mRecyclerView)

首先定义一个callback,然后传给ItemTouchHelper生成实例,最后将实例与recyclerView进行绑定。

ItemTouchHelper只负责与recyclerView的绑定,剩下的操作都代理给了callback处理。

callback内部实现了许多方法,我们只需要关注里面几个比较重要的方法

getMovementFlags()

callback内部帮我们管理了item的两种状态,一个是用户长按后的拖拽状态,另一个是用户手指左右滑动的滑动状态(以竖向列表为例),这个方法返回允许用户拖拽或滑动时的方向。

override fun getMovementFlags(
    recyclerView: RecyclerView,
    viewHolder: RecyclerView.ViewHolder
): Int = makeMovementFlags(dragFlags, swipeFlags)

比如我们希望,竖向列表时,禁止用户的滑动操作,仅支持用户上、下方向的拖拽操作

因此我们可以这样定义:

val dragFlags = (ItemTouchHelper.UP or ItemTouchHelper.DOWN)
val swipeFlags = 0 // 0 表禁止用户各个方向的操作,即禁止用户滑动

然后传入makeMovementFlags(),这个方法是callback默认提供的,我们不需要关注它的内部实现。

onMove()

当用户正在拖动子View时调用,可以在这里进行子View位置的替换操作

onSwiped()

当用户正在滑动子View时调用,可以在这里进行子View的删除操作。

isItemViewSwipeEnabled(): Boolean

返回值表是否支持滑动

isLongPressDragEnabled(): Boolean

返回值表是否支持拖动

onSelectedChanged(ViewHolder viewHolder, int actionState)

当被拖动或者被滑动的ViewHolder改变时调用,actionState会返回当前viewHolder的状态,有三个值:

  • ACTION_STATE_SWIPE:当View刚被滑动时返回

  • ACTION_STATE_DRAG:当View刚被拖动时返回

  • ACTION_STATE_IDLE:当View即没被拖动也没被滑动时或者拖动、滑动状态还没被触发时,返回这个状态

在这个方法我们可以对View进行一些UI的更新操作,例如当用户拖动时,让View高亮显示等。

clearView()

当View被拖动或滑动完后并且已经结束了运动动画时调用,我们可以在这里进行UI的复原,例如当View固定位置后,让View的背景取消高亮。

真正的开始

简单介绍完这个Callback,接下来写我们的代码

首先准备好我们的列表,列表不需要复杂,够演示就行,就放一行文字,代码我就不贴了,RecyclerVIew、Adapter、ViewHolder相信大家都很熟悉了,我们直接进入主题。

新建一个ItemTouchImpl类,继承自ItemTouchHelper

class ItemTouchHelperImpl(private val callback: Callback): ItemTouchHelper(callback)

不需要实现任何方法,ItemTouchHelper将工作代理给了Callback,所以我们接下来要实现这个Callback。

新建一个ItemTouchHelperCallback,继承自ItemTouchHelper.Callback,默认情况下,我们需要至少实现getMovementFlags()onMove()onSwiped() 三个方法。

在这个需求中,我们不需要滑动的效果,所以onSwiped()空实现就好了,同时让getMovementFlags()返回只允许上下拖拽的标志位就行。

如果我们直接在ItemTouchHelperCallback中实现相关逻辑,那么相当于这个Callback只会被用来处理上下拖拽的情况,是一个定制的Callback。下次遇上点别的场景,我们依然需要重新建个类去实现getMovementFlags(),太麻烦了,也不够通用。

为了方便后面的开发者,我决定把它做成一个通用的组件,对外暴露需要的接口,需要用到的时候只需要按需实现需要的接口就行了。

新建个ItemTouchDelegate接口,分别空实现onMove(),onSwiped(),uiOnSwiping(),uiOnDragging(),uiOnClearView(),其中getMovementFlags()我们默认实现,让ItemTouchHelper进支持上下方向的拖动、其他行为禁止,也即能满足我们的需求。

interface ItemTouchDelegate {
    fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Array<Int> {
        val layoutManager = recyclerView.layoutManager
        var swipeFlag = 0
        var dragFlag = 0
        if (layoutManager is LinearLayoutManager) {
            if (layoutManager.orientation == LinearLayoutManager.VERTICAL) {
                swipeFlag = 0   // 不允许滑动
                dragFlag = (UP or DOWN)     // 允许上下拖拽
            } else {
                swipeFlag = 0
                dragFlag = (LEFT or RIGHT)  // 允许左右滑动
            }
        }

        return arrayOf(dragFlag, swipeFlag)
    }

    fun onMove(srcPosition: Int, targetPosition:Int): Boolean = true

    fun onSwiped(position: Int, direction: Int) {}

    // 刚开始滑动时,需要进行的UI操作
    fun uiOnSwiping(viewHolder: RecyclerView.ViewHolder?) {}

    // 刚开始拖动时,需要进行的UI操作
    fun uiOnDragging(viewHolder: RecyclerView.ViewHolder?) {}

    // 用户释放与当前itemView的交互时,可在此方法进行UI的复原
    fun uiOnClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {}
}

然后,新建一个ItemTouchHelperCallback,把ItemTouchDelegate作为参数传进构造方法内,具体看代码:

class ItemTouchHelperCallback(@NotNull val helperDelegate: ItemTouchDelegate): ItemTouchHelper.Callback() {
    private var canDrag: Boolean? = null
    private var canSwipe: Boolean? = null

    fun setDragEnable(enable: Boolean) {
        canDrag = enable
    }

    fun setSwipeEnable(enable: Boolean) {
        canSwipe = enable
    }

    override fun getMovementFlags(
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder
    ): Int {
        val flags = helperDelegate.getMovementFlags(recyclerView, viewHolder)
        return if (flags != null && flags.size >= 2) {
            makeMovementFlags(flags[0], flags[1])
        } else makeMovementFlags(0, 0)
    }

    override fun onMove(
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder,
        target: RecyclerView.ViewHolder
    ): Boolean {
        return helperDelegate.onMove(viewHolder.bindingAdapterPosition, target.bindingAdapterPosition)
    }

    override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
        helperDelegate.onSwiped(viewHolder.bindingAdapterPosition, direction)
    }

    override fun isItemViewSwipeEnabled(): Boolean {
        return canSwipe == true
    }

    override fun isLongPressDragEnabled(): Boolean {
        return canDrag == true
    }

    /**
     * 更新UI
     */
    override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
        super.onSelectedChanged(viewHolder, actionState)
        when(actionState) {
            ACTION_STATE_SWIPE -> {
                helperDelegate.uiOnSwiping(viewHolder)
            }
            ACTION_STATE_DRAG -> {
                helperDelegate.uiOnDragging(viewHolder)
            }
        }
    }

    /**
     * 更新UI
     */
    override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
        super.clearView(recyclerView, viewHolder)
        helperDelegate.uiOnClearView(recyclerView, viewHolder)
    }
}

看代码应该就一目了然了,在onSelectedChanged()里根据actionState,将具体的事件分发给uiOnSwiping()和uiOnDragging(),同时让它默认不支持拖动和滑动,按业务需要打开。

最后修改下ItemTouchHelperImpl,将ItemTouchHelperCallback传进去。

class ItemTouchHelperImpl(private val callback: ItemTouchHelperCallback): ItemTouchHelper(callback) {

}

怎么使用

只需在recyclerView初始化后加这样一段代码

// 实现拖拽
val itemTouchCallback = ItemTouchHelperCallback(object : ItemTouchDelegate{

    override fun onMove(srcPosition: Int, targetPosition: Int): Boolean {
        if (mData.size > 1 && srcPosition < mData.size && targetPosition < mData.size) {
            // 更换数据源中的数据Item的位置
            Collections.swap(mData, srcPosition, targetPosition);
            // 更新UI中的Item的位置
            mAdapter.notifyItemMoved(srcPosition, targetPosition);
            return true
        }
        return false
    }

    override fun uiOnDragging(viewHolder: RecyclerView.ViewHolder?) {
        viewHolder?.itemView?.setBackgroundColor(Color.parseColor("#22000000"))
    }

    override fun uiOnClearView(
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder
    ) {
        viewHolder.itemView.setBackgroundColor(Color.parseColor("#FFFFFF"))
    }

})

val itemTouchHelper = ItemTouchHelperImpl(itemTouchCallback)
itemTouchHelper.attachToRecyclerView(mRecycler)

我们只需要实现onMove(),在onMove()主要是更新数据源的位置,以及UI界面的位置,在uiOnDragging()和uiOnClearView()里对item进行高亮显示和复原。剩下的onSwiped()滑动那些不在需求范围内,不需要实现。

但还是不能用,还记得我们的helper是默认不支持滑动和滚动的吗,我们要使用的话,还需要打开开关,就可以实现本文开头那样的效果了

itemTouchCallback.setDragEnable(true) 

如果你需要支持滑动,只需要修改下重新实现getMovementFlags(),onSwiped(),同时设置setSwipeEnable() = true即可。

源码在这里,有需要的朋友麻烦自取哈  

兄dei,如果觉得我写的还不错,麻烦帮个忙呗 :-)

  1. 给俺点个赞被,激励激励我,同时也能让这篇文章让更多人看见,(#^.^#)
  2. 不用点收藏,诶别点啊,你怎么点了?这多不好意思!
  3. 噢!还有,我维护了一个路由库。。没别的意思,就是提一下,我维护了一个路由库 =.= !!

拜托拜托,谢谢各位同学!