6.8 KiB
因收到Google相关通知,网站将会择期关闭。相关通知内容
22 撤销功能与画板优化
1. 回退与撤销功能需求分析
如下效果,在头部标题栏的左侧添加两个按钮,分别用于 向前回退 和 撤销回退 :
向前回退: 移除当前线列表中的最后一条线。 撤销回退: 向当前线列表中添加上次回退的线。
向前回退 撤销回退
由于需要 "后悔",所以需要引入一个线列表作为 "后悔药",也就是收集向前回退过程中被抛弃的线。这里在状态类中添加 _historyLines 列表来维护:
List _historyLines = [];
另外,在界面构建逻辑中,需要注意按钮可操作性的限制,比如当线列表为空时,无法向前回退:
当界面上有内容时,才允许点击左侧按钮回退。撤销按钮同理,只有回退历史中有元素,才可以操作。
- 回退与撤销界面构建
首先看一下两个按钮的构建逻辑,这里封装一个 BackUpButtons 组件进行维护。其中有两个可空的回调函数,分别用于处理两个按钮的点击事件。当函数为空时,表示当前按钮不可用,呈灰色状态示意,代码中对应的是 backColor 和 revocationColor 颜色的赋值。
IconButton 默认情况下是 48*48 的尺寸,看起来比较大,可以设置 constraints 参数来修改约束,从而控制尺寸。这里用了 Transform 组件,通过 scale 构造让图标按钮沿 Y 轴镜像,就可以得到与右侧对称的效果。
class BackUpButtons extends StatelessWidget { final VoidCallback? onBack; final VoidCallback? onRevocation;
const BackUpButtons({ Key? key, required this.onBack, required this.onRevocation, }) : super(key: key);
@override Widget build(BuildContext context) { const BoxConstraints cts = BoxConstraints(minHeight: 32, minWidth: 32); Color backColor = onBack == null?Colors.grey:Colors.black; Color revocationColor = onRevocation == null?Colors.grey:Colors.black; return Center( child: Wrap( children: [ Transform.scale( scaleX: -1, child: IconButton( splashRadius: 20, constraints: cts, onPressed: onBack, icon: Icon(Icons.next_plan_outlined,color: backColor), ), ), IconButton( splashRadius: 20, onPressed: onRevocation, constraints: cts, icon: Icon(Icons.next_plan_outlined, color: revocationColor), ) ], ), ); } }
然后将 BackUpButtons 放在恰当的位置即可,由于两个按钮属于头部标题,而头部标题栏的构建逻辑封装在 PaperAppBar 中。这里有两种思路:
将 BackUpButtons 封装在 PaperAppBar 内部,将两个按钮的点击事件继续向上传递。 将左侧组件作为插槽位置,通过构造将组件传入到 PaperAppBar 里进行使用。
其实两者本质上没有区别,只是形式上的差异、这里使用前者,这样对于 PaperAppBar 的使用者而言,只需要在意其中的事件,不需要关注标题中构造逻辑。代码如下:
在使用 PaperAppBar 组件时,为 onBack 和 onRevocation 设置回调处理函数。当 _lines 为空,表示不可回退,onBack 设为 null 即可,这样在 BackUpButtons 组件构建时就会将左侧按钮置位灰色,onRevocation 同理。
appBar: PaperAppBar( onClear: _showClearDialog, onBack: _lines.isEmpty ? null : _back, onRevocation: _historyLines.isEmpty ? null : _revocation, ),
- 状态数据的维护
最后一步就是在 _back 方法中处理回退的逻辑;在 _revocation 中处理撤销的逻辑。在回退方法中移除 _lines 的最后一个元素,然后让 _historyLines 列表添加移除的线,再更新界面即可:
void _back() { Line line = _lines.removeLast(); _historyLines.add(line); setState(() {}); }
在撤销回退方法中移除 _historyLines 的最后一个元素,然后让 _lines 列表添加移除的线,再更新界面即可:
void _revocation() { Line line = _historyLines.removeLast(); _lines.add(line); setState(() {}); }
到这里, 向前回退和撤销回退的功能就已经实现了,当前代码位置 paper.dart。虽然现在画板操作时看起开没什么问题,但内部是危机四伏的。
4.拖拽更新的频繁触发
如下所示,在拖拽更新的回调 _onPanUpdate 中打印一下日志,会发现它的触发非常频繁。而每触发一次都会像线中添加一个点,就会导致点非常多。
特别是缓慢移动的过程中,会加入很多相近的无用点,不仅占据内存,也会造成绘制的负担。如下所示,右图通过 PointMode.points 模式展示点前的点,可以看出虽然中间点线很短,但非常密集:
PointMode.polygon PointMode.points
我们可以在收集点时优化一下逻辑,根据与前一点的距离决定加不加入改点,这样可以有效降低点的数量,减缓绘制压力。处理逻辑并不复杂,如下所示,只要校验当前点和线的最后一点的距离,是否超过阈值即可。
void _onPanUpdate(DragUpdateDetails details) { Offset point = details.localPosition; double distance = (_lines.last.points.last - point).distance; if (distance > 5) { _lines.last.points.add(details.localPosition); setState(() {}); } }
阈值越大,忽略的点就越多,线条越不精细,相对来说绘制压力也就越低,需要酌情处理。这里用 5 个逻辑像素,在操作体验上没什么影响,也能达到一定的优化效果。下面是缓慢移动过程中添加点集的情况,可以看出已经避免了点过于密集的问题:
PointMode.polygon PointMode.points
- 本章小结
到这里,白板绘制的基础功能就已经完成了,当前代码位置 paper。还有些值得优化和改进的地方,比如:
现在的线是通过点进行连接的折线,可以通过贝塞尔曲线进行拟合,让点之间的连接更加圆滑; 可以提供一些基础图形的绘制操作,让绘制更加丰富。 现在每次添加点都会将所有的内容绘制一边,随着绘制内容的增加,会带来频繁的复杂绘制。 如何存储绘制的信息到本地,这样即使在退出应用后,也可以在下次开启时恢复绘制的内容。
对这些问题的改进,大家可以在今后的路途中通过自己思考和理解,来尝试解决。接下来,我们将对这三个小项目进行整合,放入到一个项目中。