learn-tech/专栏/Flutter入门教程/22撤销功能与画板优化.md
2024-10-16 00:01:16 +08:00

210 lines
6.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
22 撤销功能与画板优化
1. 回退与撤销功能需求分析
如下效果,在头部标题栏的左侧添加两个按钮,分别用于 向前回退 和 撤销回退
向前回退: 移除当前线列表中的最后一条线。
撤销回退: 向当前线列表中添加上次回退的线。
向前回退
撤销回退
由于需要 "后悔",所以需要引入一个线列表作为 "后悔药",也就是收集向前回退过程中被抛弃的线。这里在状态类中添加 _historyLines 列表来维护:
List<Line> _historyLines = [];
另外,在界面构建逻辑中,需要注意按钮可操作性的限制,比如当线列表为空时,无法向前回退:
当界面上有内容时,才允许点击左侧按钮回退。撤销按钮同理,只有回退历史中有元素,才可以操作。
2. 回退与撤销界面构建
首先看一下两个按钮的构建逻辑,这里封装一个 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,
),
3. 状态数据的维护
最后一步就是在 _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
5. 本章小结
到这里,白板绘制的基础功能就已经完成了,当前代码位置 paper。还有些值得优化和改进的地方比如
现在的线是通过点进行连接的折线,可以通过贝塞尔曲线进行拟合,让点之间的连接更加圆滑;
可以提供一些基础图形的绘制操作,让绘制更加丰富。
现在每次添加点都会将所有的内容绘制一边,随着绘制内容的增加,会带来频繁的复杂绘制。
如何存储绘制的信息到本地,这样即使在退出应用后,也可以在下次开启时恢复绘制的内容。
对这些问题的改进,大家可以在今后的路途中通过自己思考和理解,来尝试解决。接下来,我们将对这三个小项目进行整合,放入到一个项目中。