手绘板的制作——命令模式与撤销、重制(3)

·  阅读 99

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第18天,点击查看活动详情

前言

我们这篇来了解下撤销、重制的功能,其实也就是 undo 和 redo,在这里我们使用命令模式去设计,若对该模式不了解的话,可以考虑看下 「关于命令模式的误区,你知道了吗」

其实对于命令模式,我最开始的理解为命令模式只是为了方便数据的管理和记录,不应该和具体的事务或状态进行绑定,后面经过跟同事的“友好”沟通后,感觉命令模式更符合数据的管理+具体事务执行,这个这样才算是一个命令的独立执行过程,而并非只是对数据进行管理,后续的操作还得自己额外去执行。

好了,正文开始。

命令模式实践

首先,新建 ICommand 接口,由于 dart 没有 interface,就用 abstract class 进行代替:

abstract class ICommand{
   // 执行
   execute();
   // 撤销
   undo();
}
复制代码

使用 Invoker 进行命令的管理:

class Invoker {
  // 存储命令内容
  late List<ICommand> _undoCommands = [];
  late List<ICommand> _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<MyHomePage> {
  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));
      },
复制代码

由此,整个效果就完成了。

橡皮擦.png

分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改