如下效果,在头部标题栏的左侧添加两个按钮,分别用于 向前回退 和 撤销回退 :
向前回退 | 撤销回退 |
---|---|
由于需要 "后悔"
,所以需要引入一个线列表作为 "后悔药"
,也就是收集向前回退过程中被抛弃的线。这里在状态类中添加 _historyLines
列表来维护:
List<Line> _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
中。这里有两种思路:
其实两者本质上没有区别,只是形式上的差异、这里使用前者,这样对于 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。虽然现在画板操作时看起开没什么问题,但内部是危机四伏的。
如下所示,在拖拽更新的回调 _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。还有些值得优化和改进的地方,比如:
对这些问题的改进,大家可以在今后的路途中通过自己思考和理解,来尝试解决。接下来,我们将对这三个小项目进行整合,放入到一个项目中。