ItemTouchHelper实现拖动分组与定制
Android 在 RecyclerView 中使用了 ItemTouchHelper 来支持列表项的移动、横扫功能。
最近在项目中用到了这部分功能,并且有限定拖动触发区域、拖动范围限制的要求,在此做一点记录。
基础使用
要为 RecyclerView
用上 ItemTouchHelper
,需要自己实例化一个 ItemTouchHelper
并传入一个自定义的 ItemTouchHelper.Callback
。
1 | val itemTouchHelper = ItemTouchHelper( |
自己所需要的移动、横扫功能定制就由传入的 Callback
来定制。
在传入的 Callback
中,由3个需要实现的抽象方法:
1 | abstract fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int |
这个方法用来判定一个 ViewHolder
支持什么样的方式移动,一般直接调用makeMovementFlags(int fragFlags, int swipeFlags)
方法,来构造这个 flag int ,如
1 | return makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) |
表示所有的项目都支持往上和往下方向的拖动,不支持横扫手势。
与拖动、横扫手势对应的,有两个事件回调:
1 | abstract fun onMove( |
其中 onMove
在列表项被移动到一个新的位置上时被调用,我们在此处理数据交换的流程,如交换数据集中的位置、通知 adapter
更新;
onSwipe
在触发横扫手势时被调用,用来更新界面以显示横扫后需要出现的操作按钮。
定制
在我的业务中,需要限制拖动触发区域,在手点在拖动锚点上时触发拖动操作,其余区域即使长按也不能拖动;列表在视觉上分为两部分,列表项不能在组间互相交换位置,只能在组内交换,因此需要有更深的定制操作。
修改拖动触发时机
ItemTouchHelper
的默认行为是长按列表项就触发拖动操作,我们需要修改触发时机,首先是禁用长按,只需要重载 Callback
的一个方法:
1 | object : ItemTouchHelper.Callback() { |
然后为列表项中的 view
绑定点击事件,通过主动调用 itemTouchHelper.startDrag(viewHolder)
触发拖动事件:
1 | dragAnchor.setOnTouchListener { _, event -> |
拖动分组
在默认行为里,列表项可以拖动到任意其它列表项上,我们需要重载 Callback.chooseDragTarget
,实现拖动范围的限制。
首先观察这个方法的签名:
1 | public ViewHolder chooseDropTarget(int curX, int curY) ViewHolder selected, List<ViewHolder> dropTargets, |
我们通过参数可以拿到当前被拖动的列表项,
以及可以被选为释放目标的其它列表项 dropTargets
,
而默认实现则是从 dropTargets
这个列表中选择最合适的释放位置。
那么我们就可以通过过滤不合适的释放位置,达到限制拖动的目的:
1 | override fun chooseDropTarget( |
在使用kotlin时,切记要在重载方法时,将返回类型改为可空
RecyclerView.ViewHolder?
,以节约一次编译时间。
Updated
除了 chooseDragTarget
,我们还可以选择另一个方法实现拖动范围的限制:
1 | public boolean canDropOver(RecyclerView recyclerView, ViewHolder current, |
其作用是在拖动到其他项上方时,判断能不能释放在这些其他项的位置上。只有通过 canDropOver
筛选的 ViewHolder
才会出现在 chooseDropTarget
的 dropTargets
列表中。所以这个方法更适合做针对每一项的筛选操作:
1 | override fun canDropOver( |
拖动阴影
在 RecyclerView
中,列表项的布局和绘制顺序一般来说是从上到下的,如果不加任何限制,可能会出现的情况是:
将某一项往下拖动时,会因为下面的视图绘制顺序比被拖动项更后,使得被拖动项被其它视图覆盖,不符合一般直观感受。
ItemTouchHelper
对此的解决方案分为两种。在系统小于 Lolipop 21
的机子上,通过一个 RecyclerView.ChildDrawingOrderCallback
来更改绘制顺序,
保证被拖动项最后绘制,以免被其它视图覆盖。
在大于 21 的机子上,系统支持通过 elevation 来控制视图的z轴前后关系,并实现阴影效果:
1 | class ItemTouchUIUtilImpl implements ItemTouchUIUtil { |
ItemTouchUIUtilImpl
在大于21的机子上,会找到当前 recyclerView
中 除被拖动项以外 elevation
的最大值,
并将最大值 +1 px 设置给被拖动项,以达到被拖动项在所有项目之上的效果,并且在手势完成后恢复为原来的值。这里有两点需要注意的:
- 拖动时的
elevation
的差值固定为 1px,没有提供定制; - 拖动时的
elevation
值与自己的原值无关,因此不能通过拖动开始时修改elevation
来达到定制效果;
因为上述的 2 ,如果我们需要定制拖动时的阴影效果,我们需要另外一种方式来控制z轴的前后关系:
z = elevation + translationZ
我们可以在拖动前后修改视图的 translationZ
值:
1 | override fun onSelectedChanged( |
这样,对拖动效果的定制就完成了。
后记
为了验证上述关于 elevation
的两个结论,我们可以做一点好玩的事情:
elevation 值固定
拖动时的 elevation
的差值固定为 1px,没有提供定制。如果我们尝试修改被拖动项的 elevation
,将没有效果。
1 | override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { |
elevation 值与自己无关
因为被拖动项的 elevation
值与自己的原值无关,而是当前列表的最大值 +1 px,可以通过触发拖动时随意修改一个项的 elevation
来验证:
1 | override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { |
观察到的现象:
- 如果被拖动的是列表第一项,他的z会变成 1px;
- 如果被拖动的不是第一项,那么第一项的z会变成 10px,被拖动的z变成 11px。
本文标题:ItemTouchHelper实现拖动分组与定制
文章作者:Xiao
发布时间:2019-08-28
最后更新:2023-12-04
原始链接:https://blog.xiaozk.site/blog/item-touch-helper-section/