RecyclerView使用ItemTouchHelper实现拖拽排序

RecyclerView使用ItemTouchHelper实现拖拽排序

一、目标

拖拽完成文件夹收藏排序。

通过拖拽右侧拖拽按钮,实现文件夹收藏排序。

二、体验地址

神马笔记最新版本:【神马笔记 版本1.3.0.apk

三、功能设计

收藏文件夹时,总是添加到第一个位置,显示在列表最上方。

当收藏的文件夹数量增加时,需要对收藏进行排序,方便查找。

常见的几种排序方式:

  1. 手动排序
  2. 名称
  3. ……

这里,我们使用手动排序方式。

四、准备工作

1. ItemTouchHelper

ItemTouchHelper有2个功能:

  1. 拖拽——调整列表项顺序
  2. 侧向滑动——删除列表项
1
2
3
4
5
6
public ItemTouchHelper(Callback callback); 

public void attachToRecyclerView(@Nullable RecyclerView recyclerView);

public void startDrag(ViewHolder viewHolder);
public void startSwipe(ViewHolder viewHolder);

ItemTouchHelper提供给外部调用的接口主要有以上4个。

  1. 创建ItemTouchHelper对象
  2. 附加到RecyclerView
  3. 开始拖拽
  4. 开始侧向滑动

ItemTouchHelper的行为由Callback对象来决定,使用ItemTouchHelper的关键在于实现Callback接口。

2. ItemTouchHelper.Callback

Callback的需要实现的接口,分为4个类别。

  • 必须实现的抽象方法
1
2
3
4
5
6
7
8
9
10
11
// 必须通过makeMovementFlags返回值,上、下、左、右四个方向
// SimpleCallback提供了该方法的一个实现版本
public abstract int getMovementFlags(RecyclerView recyclerView,
ViewHolder viewHolder);

// 发生拖拽事件
public abstract boolean onMove(RecyclerView recyclerView,
ViewHolder viewHolder, ViewHolder target);

// 发生侧滑事件
public abstract void onSwiped(ViewHolder viewHolder, int direction);
  • 拖拽行为接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 是否支持长按触发拖拽
public boolean isLongPressDragEnabled() {
return true;
}

// 是否可以放置到target位置
public boolean canDropOver(RecyclerView recyclerView, ViewHolder current,
ViewHolder target) {
return true;
}

// 触发移动的阈值
public float getMoveThreshold(ViewHolder viewHolder) {
return .5f;
}
  • 侧滑行为接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Item是否支持侧向滑动
public boolean isItemViewSwipeEnabled() {
return true;
}

// 侧滑事件的滑动距离触发值
public float getSwipeThreshold(ViewHolder viewHolder) {
return .5f;
}

// 侧滑事件的速度触发值
public float getSwipeEscapeVelocity(float defaultValue) {
return defaultValue;
}

// ???
public float getSwipeVelocityThreshold(float defaultValue) {
return defaultValue;
}
  • UI相关接口(包括拖拽及侧滑)
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
// 选中目标Item时
public void onSelectedChanged(ViewHolder viewHolder, int actionState) {
if (viewHolder != null) {
sUICallback.onSelected(viewHolder.itemView);
}
}

// 动作完成时
public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) {
sUICallback.clearView(viewHolder.itemView);
}

// ItemDecoration#onDraw
public void onChildDraw(Canvas c, RecyclerView recyclerView,
ViewHolder viewHolder,
float dX, float dY, int actionState, boolean isCurrentlyActive) {
sUICallback.onDraw(c, recyclerView, viewHolder.itemView, dX, dY, actionState,
isCurrentlyActive);
}

// ItemDecoration#onDrawOver
public void onChildDrawOver(Canvas c, RecyclerView recyclerView,
ViewHolder viewHolder,
float dX, float dY,
int actionState, boolean isCurrentlyActive) {
sUICallback.onDrawOver(c, recyclerView, viewHolder.itemView, dX, dY, actionState,
isCurrentlyActive);
}

3. ItemTouchUIUtil

拖拽及侧滑发生时,控制Item的UI。

1
2
3
4
5
6
7
8
9
10
11
interface ItemTouchUIUtil {
void onDraw(Canvas c, RecyclerView recyclerView, View view,
float dX, float dY, int actionState, boolean isCurrentlyActive);

void onDrawOver(Canvas c, RecyclerView recyclerView, View view,
float dX, float dY, int actionState, boolean isCurrentlyActive);

void clearView(View view);

void onSelected(View view);
}

4. ItemTouchUIUtilImpl.BaseImpl

BaseImple在事件发生时,通过设置translationX及translationY移动View的位置。

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
static class BaseImpl implements ItemTouchUIUtil {

@Override
public void clearView(View view) {
view.setTranslationX(0f);
view.setTranslationY(0f);
}

@Override
public void onSelected(View view) {

}

@Override
public void onDraw(Canvas c, RecyclerView recyclerView, View view,
float dX, float dY, int actionState, boolean isCurrentlyActive) {
view.setTranslationX(dX);
view.setTranslationY(dY);
}

@Override
public void onDrawOver(Canvas c, RecyclerView recyclerView,
View view, float dX, float dY, int actionState, boolean isCurrentlyActive) {

}
}

5. ItemTouchUIUtilImpl.Api21Impl

Api21ImplBaseImpl基础上设置View的elevation实现了阴影效果。

在RecyclerView最大elevation的基础上+1

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
static class Api21Impl extends BaseImpl {
@Override
public void onDraw(Canvas c, RecyclerView recyclerView, View view,
float dX, float dY, int actionState, boolean isCurrentlyActive) {
if (isCurrentlyActive) {
Object originalElevation = view.getTag(R.id.item_touch_helper_previous_elevation);
if (originalElevation == null) {
originalElevation = ViewCompat.getElevation(view);
float newElevation = 1f + findMaxElevation(recyclerView, view);
ViewCompat.setElevation(view, newElevation);
view.setTag(R.id.item_touch_helper_previous_elevation, originalElevation);
}
}
super.onDraw(c, recyclerView, view, dX, dY, actionState, isCurrentlyActive);
}

private float findMaxElevation(RecyclerView recyclerView, View itemView) {
final int childCount = recyclerView.getChildCount();
float max = 0;
for (int i = 0; i < childCount; i++) {
final View child = recyclerView.getChildAt(i);
if (child == itemView) {
continue;
}
final float elevation = ViewCompat.getElevation(child);
if (elevation > max) {
max = elevation;
}
}
return max;
}

@Override
public void clearView(View view) {
final Object tag = view.getTag(R.id.item_touch_helper_previous_elevation);
if (tag != null && tag instanceof Float) {
ViewCompat.setElevation(view, (Float) tag);
}
view.setTag(R.id.item_touch_helper_previous_elevation, null);
super.clearView(view);
}
}

五、组合起来

1. 触发拖拽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// dragBtn按下时即触发拖拽
dragBtn.setOnTouchListener(this::onDragTouch);

// 必须返回false,ItemTouchHelper会接管
boolean onDragTouch(View view, MotionEvent event) {
parent.requestDrag(this);

return false;
}

// 调用ItemTouchHelper#startDrag开始拖拽
void requestDrag(RecyclerView.ViewHolder viewHolder) {
itemTouchHelper.startDrag(viewHolder);
dividerDecoration.setDragViewHolder(viewHolder);
}

2. SimpleDragCallback

SimpleDragCallback禁用了侧滑功能,为拖拽量身定做。

  • 禁用侧滑功能——isItemViewSwipeEnabled返回false
  • 禁用长按触发拖拽功能——isLongPressDragEnabled返回false
  • 实现onSwiped方法为空方法
  • 将elevation值在增加12,使阴影效果更加明显
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
public abstract class SimpleDragCallback extends ItemTouchHelper.SimpleCallback {

protected float elevation;

public SimpleDragCallback(int dragDirs, int swipeDirs) {
super(dragDirs, swipeDirs);

this.elevation = 12.f;
}

@Override
public boolean isLongPressDragEnabled() {
return false;
}

@Override
public boolean isItemViewSwipeEnabled() {
return false;
}

@Override
public float getMoveThreshold(RecyclerView.ViewHolder viewHolder) {
return super.getMoveThreshold(viewHolder);
}

@CallSuper
@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {

// order attention
{
View view = viewHolder.itemView;

final Object tag = view.getTag(R.id.anc_item_drag_previous_elevation);
if (tag != null && tag instanceof Float) {
ViewCompat.setElevation(view, (Float) tag);
}

view.setTag(R.id.anc_item_drag_previous_elevation, null);
}

{
super.clearView(recyclerView, viewHolder);
}

}

@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
return super.getMovementFlags(recyclerView, viewHolder);
}

@Override
public boolean canDropOver(RecyclerView recyclerView, RecyclerView.ViewHolder current, RecyclerView.ViewHolder target) {
return super.canDropOver(recyclerView, current, target);
}

@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView,
RecyclerView.ViewHolder viewHolder, float dX, float dY,
int actionState, boolean isCurrentlyActive) {

// order attention
{
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}

if (isCurrentlyActive) {
View view = viewHolder.itemView;

Object originalElevation = view.getTag(R.id.anc_item_drag_previous_elevation);
if (originalElevation == null) {
originalElevation = ViewCompat.getElevation(view);

float newElevation = elevation + findMaxElevation(recyclerView, view);
ViewCompat.setElevation(view, newElevation);

view.setTag(R.id.anc_item_drag_previous_elevation, originalElevation);
}
}

}

@Override
public void onChildDrawOver(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
super.onChildDrawOver(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}

@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
super.onSelectedChanged(viewHolder, actionState);
}

@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {

}

private float findMaxElevation(RecyclerView recyclerView, View itemView) {
final int childCount = recyclerView.getChildCount();
float max = 0;
for (int i = 0; i < childCount; i++) {
final View child = recyclerView.getChildAt(i);
if (child == itemView) {
continue;
}
final float elevation = ViewCompat.getElevation(child);
if (elevation > max) {
max = elevation;
}
}
return max;
}
}

3. ItemDragCallback

继承自SimpleDragCallback,实现收藏相关的功能。

  • 实现onMove接口,完成文件夹收藏排序
  • 重载canDropOver方法,限定只在文件夹收藏间移动
  • 重载clearView方法,拖拽完成后,保存数据
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
private static class ItemDragCallback extends SimpleDragCallback {

HashMap<Class, BiConsumer<RecyclerView.ViewHolder, RecyclerView.ViewHolder>> actionMap;

EntranceFragment parent;

public ItemDragCallback(EntranceFragment f) {
super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0);

this.elevation = 12.f;

this.parent = f;

this.actionMap = new HashMap<>();
actionMap.put(FavoriteViewHolder.class, (viewHolder, target) -> {
FavoriteEntity from = ((FavoriteViewHolder)viewHolder).getItem();
FavoriteEntity to = ((FavoriteViewHolder)target).getItem();

FavoriteEntity ds = FavoriteEntity.obtain();
ds.swap(from, to);
});
}

@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {

{
BiConsumer consumer = actionMap.get(viewHolder.getClass());
if (consumer != null) {
consumer.accept(viewHolder, target);
}
}

{
int from = viewHolder.getAdapterPosition();
int to = target.getAdapterPosition();

recyclerView.getAdapter().notifyItemMoved(from, to);
}

return true;
}

@Override
public boolean canDropOver(RecyclerView recyclerView,
RecyclerView.ViewHolder current,
RecyclerView.ViewHolder target) {
return (current.getClass() == target.getClass());
}

@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);

parent.dividerDecoration.setDragViewHolder(null);

{
FavoriteEntity ds = FavoriteEntity.obtain();
ds.save();
}
}
}

六、Finally

~仙人有待乘黄鹤~海客无心随白鸥~