最近在研究recyclerview的拖动排序功能,有很多文章都介绍了,直接生成一个ItemTouchHelper实例,绑定在recyclerview上就行,非常轻松。
不过,我想要实现半程拖动功能。拿支付宝的付款顺序举例(下图),初始“余额宝”在第一行,“账户余额”在第二行。当我把“余额宝”往下拖动一半时,“账户余额”就滑动到了第一行。此时我一松手,“余额宝”就会跑到第二行去。
本来我以为实现起来很简单,说不定set一个参数就能做到,然而在找遍社区、问遍ai后,依然不能得到满意的结果。最后还是自己摸索了一个简洁可行的方法。话不多说直接上代码:
@Override
public RecyclerView.ViewHolder chooseDropTarget(@NonNull RecyclerView.ViewHolder selected, @NonNull List<RecyclerView.ViewHolder> dropTargets, int curX, int curY) {
if (dropTargets.size() == 1) return dropTargets.get(0);
return super.chooseDropTarget(selected, dropTargets, curX, curY);
}
如果你想要修改滑动的阈值,比如说想要往下滑动80%才会触发,可以再加一句(默认是50%,就不用加了):
@Override
public float getMoveThreshold(@NonNull RecyclerView.ViewHolder viewHolder) {
return .8f;
}
把这两个函数重写在ItemTouchHelper.Callback里就可以了!不过需要注意的是此方法适用于一行或一列排开的recyclerview,网格状排序脑测适用,但建议还是自己调试下确认。
接下来我就分享一下我是如何得到这个结论的。如果有误,欢迎各位大佬批评指正~
1.无效的方法以及原因
虽然我没有在社区里找到方法,但我通过各种描述从ai那里获得了一些方法,可惜写的有模有样的,一运行压根不行。
第一种方法是在clearView中手动调用adapter.notifyItemMoved(fromPosition, toPosition),实现视觉上的子件移动。然而,通过调试可以发现,当我拖拽子件一半的距离放手后,它会先回到原来的位置,再调用clearView,从原来的位置移动到新的位置。
第二种方法是在onSelectedChanged中检测到拖拽结束时(actionState == ItemTouchHelper.ACTION_STATE_DRAG)手动调用移动。然而,当拖拽结束时,onSelectedChanged里的viewHolder就为null了,根本就不知道目的地的位置。
而且,这两种方法甚至还没有考虑到如何准确地判断是否划过一半,要么没给判断式,要么给的是不准确的。兜了半天圈子后,我终于放弃ai了。我只是想要把条件改的宽松一点,只要滑动一半就可以实现交换,怎么这么费劲呢?我不禁好奇,ItemTouchHelper的拖拽交换原理究竟是什么?
2.拖拽交换原理
在我看了半天源码后,大致总结了ItemTouchHelper的拖拽交换原理。
首先,ItemTouchHelper内定义了一个函数叫moveIfNecessary,这个函数的作用就是在你拖拽子件的时候持续地判断是否达到移动的标准,达到了就移动。这个函数里主要的流程如下:
1.判断滑动量是否达标
2.findSwapTargets得到target列表,target就是移动的目的地
3.chooseDropTarget得到最终的target
2.1 判断滑动量是否达标
final float threshold = mCallback.getMoveThreshold(viewHolder);
final int x = (int) (mSelectedStartX + mDx);
final int y = (int) (mSelectedStartY + mDy);
if (Math.abs(y - viewHolder.itemView.getTop()) < viewHolder.itemView.getHeight() * threshold
&& Math.abs(x - viewHolder.itemView.getLeft())
< viewHolder.itemView.getWidth() * threshold) {
return;
}
这部分的代码是判断滑动量是否达标的。这里threshold就是滑动的阈值,默认为0.5f。下面的代码看起来很长,实际意思就是滑动没超过50%就提前退出函数。也就是说,当滑动超过50%时,就已经通过第一关了。
2.2 得到target列表
List<ViewHolder> swapTargets = findSwapTargets(viewHolder);
if (swapTargets.size() == 0) {
return;
}
findSwapTargets函数很长就不贴出来了,主要的目的就是得到target列表。举个例子,当我把第一行的子件往下拖动时,它会先处于第一行和第二行之间,此时待选的target就是第一行和第二行的viewHolder,不过由于第一行的viewHolder就是自身,所以被剔除,此时swapTargets的成员就只剩下第二行的viewHolder。接着往下拖动,当它处于第二行和第三行之间时,swapTargets的成员就变为第二行和第三行的viewHolder。
2.3 得到最终的target
ViewHolder target = mCallback.chooseDropTarget(viewHolder, swapTargets, x, y);
if (target == null) {
mSwapTargets.clear();
mDistances.clear();
return;
}
chooseDropTarget函数会从输入的findSwapTargets中选择最终的target。还是举上一个例子,当我把第一行的子件往下拖动时,首先处于第一行和第二行之间,swapTargets的成员只有第二行的viewHolder,此时chooseDropTarget返回值为null,因此不会触发交换。当它刚刚进入第三行时,swapTargets的成员就变为第二行和第三行的viewHolder。此时由于被拖拽的子件更靠近第二行,chooseDropTarget的返回值为第二行的viewHolder,触发交换,第二行的数据就移动到第一行了。
那么为什么一开始chooseDropTarget会返回null呢?好吧终于说到重点了。下面给出了chooseDropTarget的一部分代码,这里dY>0代表这是向下移动的场景。可以看到,在if语句中有一句diff < 0,意思就是说目标target必须要比被拖拽的子件高,否则就不会进入后续判定。再次回想刚才的例子,在一开始被拖拽的子件处于第一行和第二行之间,它是高于第二行的,因此此时第二行不会成为它的目的地。直到拖拽的子件来到第二行和第三行之间,它的目的地才从null变为第二行。
if (dy > 0) {
int diff = target.itemView.getBottom() - bottom;
if (diff < 0 && target.itemView.getBottom() > selected.itemView.getBottom()) {
final int score = Math.abs(diff);
if (score > winnerScore) {
winnerScore = score;
winner = target;
}
}
}
知道了原理,想要实现半程交换就很简单了。最保险的做法就是在重写的时候把源代码复制一遍,然后再把diff<0改掉,比如diff<-target.itemView.getHeight()*mCallback.getMoveThreshold(viewHolder)
,或者直接把diff<0删掉。但是重新复制一遍太麻烦了,最简单的改法就是我一开始提到的,直接返回findSwapTargets的第一项就完事了。
最后回顾一下ItemTouchHelper的拖拽交换流程:原先的ItemTouchHelper中,拖拽不超过50%的会卡在第一关,不超过100%的会卡在第三关,比100%大一点就可以触发交换了。我提供的方法相当于把第三关给删了,只要比50%大一点就可以触发交换。
当然,还是要叠个甲,作者写了那么多的判断,肯定有他的道理,我直接丢掉,指不定就埋下了bug的种子。作者只要注意长宽不等或者是网格排列等复杂情况就可以,可是我随便写个最简单的单列排列要考虑的就很多了。
3.代码
贴个代码供参考~
class MyItemTouchHelperCallback extends ItemTouchHelper.Callback {
private final RecycleAdapterLabel recycleAdapterLabel;
private int from = -1, to;
public MyItemTouchHelperCallback(RecycleAdapterLabel recycleAdapterLabel) {
this.recycleAdapterLabel = recycleAdapterLabel;
}
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
return makeMovementFlags(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0);
}
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
if (viewHolder.itemView instanceof TextView) return false;
int fromPosition = viewHolder.getAdapterPosition();
int toPosition = target.getAdapterPosition();
if (toPosition > 0 && toPosition < FileData.getLabelSize() - 1) {
recycleAdapterLabel.notifyItemMoved(fromPosition, toPosition);
if (from == -1) from = fromPosition;
to = toPosition;
return true;
}
return false;
}
@Override
public RecyclerView.ViewHolder chooseDropTarget(@NonNull RecyclerView.ViewHolder selected, @NonNull List<RecyclerView.ViewHolder> dropTargets, int curX, int curY) {
if (dropTargets.size() == 1) return dropTargets.get(0);
return super.chooseDropTarget(selected, dropTargets, curX, curY); //实际上这行不会执行
}
@Override
public float getMoveThreshold(@NonNull RecyclerView.ViewHolder viewHolder) {
return .55f;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
}
@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
if (viewHolder != null) {
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG)
viewHolder.itemView.setAlpha(0.71f);
else if (actionState == ItemTouchHelper.ACTION_STATE_IDLE)
viewHolder.itemView.setAlpha(1.0f); //似乎在拖拽结束后,viewHolder必定为null,因此这条语句不会被执行
}
super.onSelectedChanged(viewHolder, actionState);
}
@Override
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
viewHolder.itemView.setAlpha(1.0f);
if (from != -1 && from != to) {
FileData.changeLabelOrder(from, to);
HomePage.labelChange(HomePage.MOVE_LABEL, from, to);
}
from = -1;
}
@Override
/*
限制拖拽的范围
*/
public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder,
float dX, float dY, int actionState, boolean isCurrentlyActive) {
int position = viewHolder.getAdapterPosition();
int viewHeight = viewHolder.itemView.getHeight(); //假设每一个itemView的高度都是一样的
int min_dY = (int) -((position - 0.5) * viewHeight), max_dY = (int) ((FileData.getLabelSize() - position - 1.5) * viewHeight);
super.onChildDraw(c, recyclerView, viewHolder, dX, min(max(dY, min_dY), max_dY), actionState, isCurrentlyActive);
}
@Override
//设置整个viewHolder不能长按拖动,后续通过setLongClickInterface将长按拖动绑定在某个子件上
public boolean isLongPressDragEnabled() {
return false;
}
}
使用实例:
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new MyItemTouchHelperCallback(recycleAdapterLabel) {});
itemTouchHelper.attachToRecyclerView(rv_label);
recycleAdapterLabel.setLongClickInterface(itemTouchHelper::startDrag); //括号内等价于holder -> itemTouchHelper.startDrag(holder)