库 flutter_sortable_wrap 主要用到了这三个组件 Wrap, Draggable, DragTarget 来实现
核心思路:
- 静止状态时,所有 children 都是
Draggable,因此此时每个 child 都能拖动 - 拖动状态时,所有 children 都是
DragTarget,因此此时每个 child 都能被击中
先看效果:
主要原理:
1. Draggable 组件提供了 onDragStarted、onDragEnd 等事件回调能力。
因此,静止状态 变到 拖动状态 的时机就选在 onDragStarted 里,需要把所有 Wrap的 children 都变了,那么就需要做一次 setState:
/// A. Return the widget in dragging mode [拖动状态]
if (isDragging) {
if (isDraggingMe(element)) {
return IgnorePointer(ignoring: true, child: Opacity(opacity: 0.2, child: element.view));
} else {
return SortableItem(key: ValueKey(index), element: element, onEventHit: eventDoRollingInDragging);
}
}
/// B. Return the widget in idle mode [静止状态]
return Draggable<SortableElement>(
...
onDragStarted: onDragStarted,
...
);
// 做一次 setState 换掉所有 [idle mode] 的 children,进入 [dragging mode]
void onDragStarted() {
setState(() => draggingElement = element);
}
2. DragTarget 组件提供了 onWillAccept、onAccept 等被手指移到其上方被击中的事件回调能力。
因此,我们就可以在 onWillAccept 里处理动画过渡、修正位置等逻辑。
谨记,动画是花里胡哨点缀用的,数据和逻辑都不要跟动画耦合,不要依赖动画的开始或结束来更新数据和逻辑
源码分析:
库 flutter_sortable_wrap Github 的代码量不多,大家对照着源码,这里简单讲一下:
SortableWrap是Wrap组件的封装,此组件是对外使用的SortableItem是DragTarget组件的封装,用来被击中及做动画的SortableElement是数据封装类,每个SortableItem绑定一个SortableElement数据,它包含了位置索引originalIndex,preservedIndex等数据
那么,在拖动过程中,当 SortableElement 组件被击中,动画及数据就在 onWillAccept -> onEventHit -> eventDoRollingInDragging 方法里处理了,也即处理被击中的 Widget 的动画是向左还是向右移动,若是跨行拖动时就克隆多一个 幽灵Ghost Widget 来左入/右入,核心代码:
// 当 DragTarget 组件被碰中的事件回调,处理位置数据并做动画效果
/// Events
void eventDoRollingInDragging(SortableItemState beHitItemState, SortableElement holdingElement) {
assert(draggingElement != null, 'Dragging status is a mess now, please check it out.');
assert(draggingElement == holdingElement, 'Got a different dragging view, please check it out.');
SortableElement dragging = draggingElement!;
SortableElement element = beHitItemState.widget.element;
int toIndex = animationElements.indexOf(element);
int draggingIndex = animationElements.indexOf(dragging);
// 是否在同一行拖动,如果不是,就是跨行拖动了
bool isDraggingInSameRow = toIndex ~/ elementCountPerRow == draggingIndex ~/ elementCountPerRow;
// 判断是拖动是向左还是向右: 若拖动向右,底下被碰中的 DragTarget 组件就要做向左动画,反之亦然
/// To lower index means user dragging to left, user dragging to left or top, the hit target should animate to right
bool isDraggingToLowerIndex = toIndex < draggingIndex;
int i = isDraggingToLowerIndex ? draggingIndex - 1 : draggingIndex + 1;
for (; isDraggingToLowerIndex ? i >= toIndex : i <= toIndex; isDraggingToLowerIndex ? i-- : i++) {
SortableElement e = animationElements[i];
/// Swap the index in cached data
int sourceIndex = i;
int destinationIndex = isDraggingToLowerIndex ? i + 1 : i - 1;
// 这里已经是更新数据,别等动画的结束,也即就算没有动画,数据也是正确的
animationElements.swap(sourceIndex, destinationIndex);
// 开始做动画
/// Handle animation by corresponding item's state
SortableItemState itemState = e.state;
itemState.sourceIndex = sourceIndex;
itemState.destinationIndex = destinationIndex;
itemState.startAnimation(isDraggingInSameRow);
}
// 既然数据变了,当然就要 setState 了,不要等动画结束才做
/// Make sure you see the right thing on the right position
setState(() {});
}
OK,有什么不明白的欢迎评论~