Flutter 组件集录 | 后悔药 UndoHistory
创始人
2025-01-07 22:03:01
0次

theme: cyanosis

MenuAnchor.png

在现实世界中,没有后悔药可以吃。但在对于计算机世界来说,撤销、恢复是非常常见的功能。小屁孩在键盘上啪啪一顿输出,把你正在写的重要文档搅得面目全非,Ctrl + Z 轻松救场。编程开发的过程中,我们在不断输入和试错,这颗后悔药是我们敢于前行的底气,大不了重新来过。


1. 简单认识 UndoHistory

UndoHistory 是一个 StatefulWidget 组件,在源码中它主要为输入组件服务,只在 EditeableText 源码中打工。可编辑的文字确实是 Undo 使用的最佳场所。

image.png

首先来通过一个小案例体验一下 UndoHistory 的价值。现在有个小需求:

在输入面板上添加两个按钮,分别用于 回退一步 和 撤销回退一步

undo<em>history</em>01.gif

传统的方法处理这个需求,要自己维护列表,在输入变化时进行收集字符串的工作。另外,并非每个字符变化都需要记录,需要进行节流 throttle 的处理,否则历史列表中将会记录大量字符信息,而绝大多数是没有必要的。这些逻辑交给开发者自己处理,就会比较麻烦。为了简化对输入框回退和撤销的操作,Flutter 通过了 UndoHistory 组件。


2. 案例代码实现

界面布局非常简单,上下结构通过 Column 竖向排列:

  • 上方是两个操作按钮,需要根据是否可回退、可撤销展示是否激活的状态。
  • 下方是普通的 TextFiled 组件,延展高度区域并填充白色。

image.png

TextField 组件中有一个 undoController 的参数,可以传入 UndoHistoryController 对象,用于控制 UndoHistory 的内容。它是一个 ValueNotifier 可监听对象,也就是说是否标题栏可以监听它,来感知是否可回退、可撤销的状态数据。

image.png

```dart final UndoHistoryController _undoController = UndoHistoryController();

@override void dispose() { _undoController.dispose(); super.dispose(); }

Widget _buildInputArea() { return TextField( undoController: _undoController, expands: true, maxLines: null, minLines: null, decoration: InputDecoration( filled: true, fillColor: Colors.white, hoverColor: Colors.transparent, border: InputBorder.none, ), ); } ```


如下所示,这里封了 _IconAction 组件处理图标按钮的展示效果,包括悬浮时的背景圆角,已经激活状态的 处理。封装完后标题栏的两个按钮就可以轻松复用 _IconAction 实现展示功能。当onTap 事件为null时,表示非激活状态,无法触发交互。

image.png

```dart class _IconAction extends StatefulWidget { final IconData icon; final VoidCallback? onTap;

const _IconAction({super.key, required this.icon, this.onTap});

@override State<_iconaction> createState() => _IconActionState(); }

class _IconActionState extends State<_iconaction> { bool _hover = false;

bool get enable => widget.onTap != null; Color? get color => (_hover && enable) ? Colors.grey.withOpacity(0.2) : null;

@override Widget build(BuildContext context) { return MouseRegion( cursor: (hover && enable) ? SystemMouseCursors.click : SystemMouseCursors.basic, onExit: () => setState(() => hover = false), onEnter: () => setState(() => _hover = true), child: GestureDetector( onTap: widget.onTap, child: Container( decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(4)), padding: const EdgeInsets.all(4.0), child: Icon( widget.icon, size: 20, color: enable ? null : Colors.grey, )), ), ); } } ```


UndoHistoryController 中维护了两个历史记录,一个是输入的历史列表,用于处理回退;另一个是回退的历史列表,用于处理撤销上一次回退,分别对应左右按钮。 UndoHistoryController#undo 和UndoHistoryController#redo 方法实现回退和撤销回退的功能。

此时,构建顶部栏,可以通过 ValueListenableBuilder 来监听 _undoController 可监听对象。是否可以回退和撤销回退的状态,已经记录在了控制器中。回调构建时取用即可。按钮的事件触发,执行控制器的 undo 和 redo 方法即可。

dart Widget _buildToolBar() { return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4), child: ValueListenableBuilder( valueListenable: _undoController, builder: (BuildContext context, UndoHistoryValue value, Widget? child) { return Wrap( spacing: 4, children: [ _IconAction(icon: Icons.undo, onTap: value.canUndo ? _undoController.undo : null), _IconAction(icon: Icons.redo, onTap: value.canRedo ? _undoController.redo : null), ], ); }, ), ); }


3. UndoHistoryController 承担的角色

仔细思考一下,UndoHistoryController 在功能需求实现过程中。它连接着和 TextFiled 顶部的按钮事件,其既持有状态数据,又具有修改数据的能力,还能触发通知更新。这样的对象在状态管理中,一般称之为视图模型 ViewModel 或业务逻辑层 BLoc 。


可以从回退按钮的点击事件来体会一下,在交互之后,数据的流向。如下绿色箭头所示:
触发 undo 之后,列表数据变化,触发通知更新。此时顶栏和输入框都监听了 UndoHistoryController ,所以两者的视图都会发生变化。顶栏会根据是否可撤销展示激活与否;输入框中展示的文字会发生变化。
同理,输入框的底层在输入过程中,也一定修改了 UndoHistoryController 的内部数据,并触发通知更新。大家可以自己想想此时的数据流向。

image.png


4. UndoHistory 源码简看

下面是 EditableTextState 构建逻辑内 UndoHistory 组件的使用场景,其中我们传入的 undoController 将会为作为构造参数传入。其中 onTriggered 回调时触发 undo 和 redo 的时机,会触发 userUpdateTextEditingValue 方法更新输入的信息:

image.png

UndoHistoryState 中维护了一个 _UndoStack 的栈,

image.png

这个栈是通过列表 List 实现的,输入框中 UndoHistory 组件使用的泛型是 TextEditingValue。所以本质来看 UndoHistoryState 状态类中,维护了一个 TextEditingValue 列表来容纳输入框的编辑内容。

dart class _UndoStack { _UndoStack(); final List _list = [];

在 initState 中可以看到,UndoHistoryState 会监听输入控制器触发 _push 方法; 监听 UndoHistoryContorller 的变化,触发 onTriggered 来更新输入框内容。

image.png


另外,其中定义了节流相关的计时器,时长为 500 ms , 输入变化时的 _push 方法中,会先校验更新的条件。然后将新值放入节流器 _throttledPush 中。

```dart late final _Throttled _throttledPush; Timer? _throttleTimer; bool _duringTrigger = false;

static const Duration _kThrottleDuration = Duration(milliseconds: 500); ```

image.png

_throttledPush在 initState 中被初始化,触发的函数是为 _stack 添加元素,并更新状态。

image.png

在 _updateState 中会更新 UndoHistoryController 控制器的值,触发通知更新。外界就可以因此感知是否可以回退或取消回退。

image.png


到这里,UndoHistory 的基本运转方式就简单了解了一下。虽然 UndoHistory 只在源码中的输入框里发光发热,但是它的价值远不止此。所有需要回退或取消回退的场景,都可以使用它。比如绘制、图片编辑等。后面会结合具体的其他场景,来介绍 UndoHistory 组件自身的使用方式。那本文就到这里,谢谢观看~

相关内容

热门资讯

实测交流透视挂!宝玛大厅ai智... 实测交流透视挂!宝玛大厅ai智能透视挂教程,宝玛大厅破解软件经验(本来有挂辅助)1、每一步都需要思考...
总算了解辅助器!朱雀吴江三缺一... 总算了解辅助器!朱雀吴江三缺一ai智能透视辅助挂技巧,朱雀吴江三缺一好像是有挂(有辅助挂细节)朱雀吴...
今日公布透视!九五至尊大厅ai... 今日公布透视!九五至尊大厅ai智能透视插件,九五至尊大厅有没有透视挂窍门(切实有挂技术)1、在九五至...
玩家必看科普开挂!聚闲麻将ai... 玩家必看科普开挂!聚闲麻将ai智能透视辅助挂软件,聚闲麻将总是真的有挂(有辅助挂方式)聚闲麻将能透视...
据报道开挂!旅游娱乐ai智能开... 据报道开挂!旅游娱乐ai智能开挂工具,旅游娱乐辅助工具演示(一直有挂教学)1、全新机制【旅游娱乐ai...
一分钟揭秘开挂!决胜朋来麻将a... 一分钟揭秘开挂!决胜朋来麻将ai智能透视辅助挂教程,决胜朋来麻将真是真的是有挂(有辅助挂总结)1、首...
如何分辨真伪透视!红牛ai智能... 如何分辨真伪透视!红牛ai智能透视攻略,红牛是不是有挂经验(真是有挂技术)1、操作简单,无需红牛手机...
2分钟细说辅助器!江湖大庆麻将... 2分钟细说辅助器!江湖大庆麻将ai智能透视辅助挂攻略,江湖大庆麻将竟然是真的有挂(有辅助挂头条)1、...
总算清楚透视挂!老夫子厅ai智... 总算清楚透视挂!老夫子厅ai智能透视挂技巧,老夫子厅怎么开挂课程(好像发现有挂)老夫子厅破解侠是真的...
解密关于辅助!菜鸟麻将ai智能... 解密关于辅助!菜鸟麻将ai智能透视辅助挂脚本,菜鸟麻将总是存在有挂(有辅助挂存在)1、用户打开应用后...