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


    前言

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

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

    好了,正文开始。

    命令模式实践

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

    abstract class ICommand{
       // 执行
       execute();
       // 撤销
       undo();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    使用 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();
        }
      }
    }
    
    • 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

    关于 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();
      }
    }
    
    • 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

    PaintedCommand 需要使用到 PaintedBoardProvider 和 Stroke,PaintedBoardProvider 用于数据与状态管理,Stroke 用于存储当前命令的数据,而 execute 和 undo 其实就是该命令运作时所需的操作,也就是数据管理+状态更新。

    在这里用到了 PaintedBoardProvider 的 refreshPaintedBoard() 方法,其实就是 notifyListeners()

      refreshPaintedBoard(){
        notifyListeners();
      }
    
    • 1
    • 2
    • 3

    到了这里有可能有人会问,你这画笔操作不是有两种吗?一种笔刷模式,一种橡皮擦模式,为什么只设计一种命令?

    这是因为笔刷模式和橡皮擦模式的区分仅在于 Stroke 中 isClear 的值,所以为了便于管理,使用一种命令更合适。

    场景运用

    下面我们把刚刚设计好的命令模式应用到具体业务中。

    首先,我们得新增两个按钮:undo 和 redo,并且新建 Invoker 对象进行操作 :

    class _MyHomePageState extends State {
      final PaintedBoardProvider _paintedBoardProvider = PaintedBoardProvider();
      final Invoker _invoker = Invoker();   // <- 重点在这里
    
    • 1
    • 2
    • 3
                          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"),
                              ),
                            ),
                          ),
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    剩下的就是要确认命令执行时机。

    正常来说,一旦画布进行更改了,就是一次命令,但是由于在手绘期间,画布是一直在不断刷新的,若是这样继续记录,那命令数量就过于庞大了,所以这里我该改为一次手势完整流程就是一次命令,这样我们可以在 onPanEnd 进行记录即可。

    所以,我们先把 _invoker 传递给 HandPaintedBoard:

                    Expanded(child: HandPaintedBoard(_paintedBoardProvider, _invoker)),  // <- 重点在这里
    
    • 1
    class HandPaintedBoard extends StatefulWidget {
      const HandPaintedBoard(
        this._paintedBoardProvider, this._invoker, {  // <- 更改
        Key? key,
      }) : super(key: key);
      final PaintedBoardProvider _paintedBoardProvider;
      final Invoker _invoker;  // <- 更改
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    然后在 onPanEnd 进行命令的执行:

          onPanEnd: (details) {
            print("onPanDown:移动结束");
            widget._invoker.execute(PaintedCommand(   // <- 命令执行
                _paintedBoardProvider, _paintedBoardProvider.strokes.last));
          },
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

    在这里插入图片描述

  • 相关阅读:
    PHP生成带中文的图片
    iOS 借助定位实现“保活”策略
    不同content-type对应的前端请求参数处理格式
    js垃圾回收机制
    C++知识精讲15 | 三类基于贪心思想的区间覆盖问题【配套资源详解】
    Word控件Spire.Doc 【段落处理】教程(十二):如何在 C# 中管理 word 文档的分页
    怎么让机器认识你的手势?机器学习方向
    大数据学习-Hive
    dialogx,给大家推荐一个开源安卓弹窗组件。
    一台服务器,最大支持的TCP连接数是多少?
  • 原文地址:https://blog.csdn.net/m0_46278918/article/details/125217384