注册

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

写在前面


最近工作强度好大,一天能敲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. 噢!还有,我维护了一个路由库。。没别的意思,就是提一下,我维护了一个路由库 =.= !!

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


作者:古嘉明同学
链接:https://juejin.cn/post/7110408776477310989
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册