我们这篇来了解下撤销、重制的功能,其实也就是 undo 和 redo,在这里我们使用命令模式去设计,若对该模式不了解的话,可以考虑看下 「关于命令模式的误区,你知道了吗」。
其实对于命令模式,我最开始的理解为命令模式只是为了方便数据的管理和记录,不应该和具体的事务或状态进行绑定,后面经过跟同事的“友好”沟通后,感觉命令模式更符合数据的管理+具体事务执行
,这个这样才算是一个命令的独立执行过程,而并非只是对数据进行管理,后续的操作还得自己额外去执行。
好了,正文开始。
首先,新建 ICommand 接口,由于 dart 没有 interface,就用 abstract class
进行代替:
abstract class ICommand{
// 执行
execute();
// 撤销
undo();
}
使用 Invoker 进行命令的管理:
class Invoker {
// 存储命令内容
late List _undoCommands = [];
late List _redoCommands = [];
/// 执行
execute(ICommand command) {
_undoCommands.add(command);
command.execute();
}
/// 撤销
undo() {
if (_undoCommands.isNotEmpty) {
final last = _undoCommands.last;
_redoCommands.add(last);
_undoCommands.remove(last);
last.undo();
}
}
/// 重制
redo() {
if (_redoCommands.isNotEmpty) {
final last = _redoCommands.last;
_undoCommands.add(last);
_redoCommands.remove(last);
last.execute();
}
}
}
关于 Invoker 的设计也是很简单,使用两个 List 去存储命令,分别是 undo 命令列表 和 redo 命令列表。然后执行 undo 和 redo 操作时,其实就是对两个命令列表的数据进行增删操作,同时调用该命令的执行或撤销。
至于具体的命令设计上,每一种操作都应该拥有一种具体的命令实现,由于我这里仅存储画笔的操作,所以我只封装一种命令:
class PaintedCommand extends ICommand {
late Stroke _stroke;
late PaintedBoardProvider _paintedBoardProvider;
PaintedCommand(PaintedBoardProvider paintedBoardProvider, Stroke stroke) {
_paintedBoardProvider = paintedBoardProvider;
_stroke= stroke;
}
@override
execute() {
if (!_paintedBoardProvider.strokes.contains(_stroke)) {
_paintedBoardProvider.strokes.add(_stroke);
}
_paintedBoardProvider.refreshPaintedBoard();
}
@override
undo() {
if (_paintedBoardProvider.strokes.contains(_stroke)) {
_paintedBoardProvider.strokes.remove(_stroke);
}
_paintedBoardProvider.refreshPaintedBoard();
}
}
PaintedCommand 需要使用到 PaintedBoardProvider 和 Stroke,PaintedBoardProvider 用于数据与状态管理,Stroke 用于存储当前命令的数据,而 execute 和 undo 其实就是该命令运作时所需的操作,也就是数据管理+状态更新。
在这里用到了 PaintedBoardProvider 的 refreshPaintedBoard()
方法,其实就是 notifyListeners()
:
refreshPaintedBoard(){
notifyListeners();
}
到了这里有可能有人会问,你这画笔操作不是有两种吗?一种笔刷模式,一种橡皮擦模式,为什么只设计一种命令?
这是因为笔刷模式和橡皮擦模式的区分仅在于 Stroke 中 isClear 的值,所以为了便于管理,使用一种命令更合适。
下面我们把刚刚设计好的命令模式应用到具体业务中。
首先,我们得新增两个按钮:undo 和 redo,并且新建 Invoker 对象进行操作 :
class _MyHomePageState extends State {
final PaintedBoardProvider _paintedBoardProvider = PaintedBoardProvider();
final Invoker _invoker = Invoker(); // <- 重点在这里
Expanded(
child: GestureDetector(
onTap: () {
print("点击了 undo");
_invoker.undo(); // <- 新增
},
child: const Center(
child: Text("undo"),
),
),
),
Expanded(
child: GestureDetector(
onTap: () {
print("点击了redo");
_invoker.redo(); // <- 新增
},
child: const Center(
child: Text("redo"),
),
),
),
剩下的就是要确认命令执行时机。
正常来说,一旦画布进行更改了,就是一次命令,但是由于在手绘期间,画布是一直在不断刷新的,若是这样继续记录,那命令数量就过于庞大了,所以这里我该改为一次手势完整流程就是一次命令,这样我们可以在 onPanEnd
进行记录即可。
所以,我们先把 _invoker 传递给 HandPaintedBoard:
Expanded(child: HandPaintedBoard(_paintedBoardProvider, _invoker)), // <- 重点在这里
class HandPaintedBoard extends StatefulWidget {
const HandPaintedBoard(
this._paintedBoardProvider, this._invoker, { // <- 更改
Key? key,
}) : super(key: key);
final PaintedBoardProvider _paintedBoardProvider;
final Invoker _invoker; // <- 更改
然后在 onPanEnd
进行命令的执行:
onPanEnd: (details) {
print("onPanDown:移动结束");
widget._invoker.execute(PaintedCommand( // <- 命令执行
_paintedBoardProvider, _paintedBoardProvider.strokes.last));
},
由此,整个效果就完成了。