鸿蒙纪·梦始卷#14 | 画板绘制 - 撤销功能与优化

439 阅读5分钟

《鸿蒙纪元》张风捷特烈 计划打造的一套 HarmonyOS 开发系列教程合集。致力于创作优质的鸿蒙原生学习资源,帮助开发者进入纯血鸿蒙的开发之中。本系列的所有代码将开源在 HarmonyUnit 项目中:

github: github.com/toly1994328…
gitee: gitee.com/toly1994328…

鸿蒙纪元 系列文章列表可在《文章总集》 或 【github 项目首页】 查看。


上一篇,我们完成了线条粗细颜色 的选择。本篇将继续完善画板需求,如下效果,在头部标题栏的左侧添加两个按钮,分别用于 回退上一步撤销回退

  • 向前回退: 移除当前线列表中的最后一条线。
  • 撤销回退: 向当前线列表中添加上次回退的线。
向前回退撤销回退

1. 增加需求支持的数据

每项功能需求的增加,其背后都需要对应的数据支持。由于需要 "后悔",所以需要引入一个线列表作为 "后悔药",也就是收集向前回退过程中被抛弃的线。这里在状态类中添加 _historyLines 列表来维护:

---->[pages/painter/bloc/PainterBloc.ets]----
@Track historyLines: Line[] = []; //回退历史线列表

另外,在界面构建逻辑中,需要注意按钮可操作性的限制,比如当线列表为空时,无法向前回退;当界面上有内容时,才允许点击左侧按钮回退。撤销按钮同理,只有回退历史中有元素,才可以操作。

无绘制时有绘制时

为了追踪两个列表线的个数,这里在业务逻辑层提供了两个计数器;并通过 updateCount 方法更新这两个数值:

---->[pages/painter/bloc/PainterBloc.ets]----
 @Track lineCount = 0;
 @Track historyLineCount = 0;
  
private updateCount(){
  this.lineCount = this.lines.length;
  this.historyLineCount = this.historyLines.length;
}

2. 回退与撤销界面构建

首先看一下两个按钮的构建逻辑,这里封装在 actionsButton 组件构建2方法中。其中 enabled 表示按钮的可用情况,校验状态中对应的计数即可。比如 lineCount 大于 0 时才允许回退,将它作为左侧按钮的 enable 值:
两个按钮的事件,分别触发 PainterBloc#backPainterBloc#revocation 方法,处理具体的数据变化逻辑:

@Builder
actionsButton() {
  Row() {
    Button() {
      SymbolGlyph($r('sys.symbol.undo')).fontSize(24)
    }
    .width(36).height(36)
    .backgroundColor(Color.Transparent)
    .enabled(this.model.lineCount > 0)
    .onClick(() => this.model.back())
    
    Button() {
      SymbolGlyph($r('sys.symbol.redo')).fontSize(24)
    }
    .width(36).height(36)
    .backgroundColor(Color.Transparent)
    .enabled(this.model.historyLineCount > 0)
    .onClick(() => this.model.revocation())
  }
}

3. 状态数据的维护

最后一步就是在 back 方法中处理回退的逻辑;在 revocation 中处理撤销的逻辑。在回退方法中移除 lines 的最后一个元素,然后让 historyLines 列表添加移除的线,再更新界面即可:

back(): void {
  let line = this.lines.pop()
  if (line) {
    this.historyLines.push(line);
    this.updateCount();
  }
  this.paint(this.context);
}

在撤销回退方法中移除 historyLines 的最后一个元素,然后让 lines 列表添加移除的线,再更新界面即可:

revocation(): void {
  let line = this.historyLines.pop()
  if (line) {
    this.lines.push(line);
    this.updateCount();
  }
  this.paint(this.context);
}

到这里, 回退上一步撤销回退 的功能就已经实现了,提交一个小里程碑 v20-回退与撤销回退


4.拖拽更新的频繁触发

虽然现在画板操作时看起开没什么问题,但内部却危机四伏。如果你缓慢滑动手指,画了很多东西。最后会发现非常卡顿。在拖拽更新的回调 _onPanUpdate 中打印一下日志,会发现它的触发非常频繁。而每触发一次都会像线中添加一个点,就会导致点非常多。

特别是缓慢移动的过程中,会加入很多相近的无用点,不仅占据内存,也会造成绘制的负担。我们可以优化一下收集点的逻辑,根据与前一点的距离决定加不加入该点,这样可以有效降低点的数量,减缓绘制压力。处理逻辑并不复杂,如下所示,只要校验当前点和线的最后一点的距离,是否超过阈值即可。

// 平移更新时,为新线添加点
updateLine(x: number, y: number) {
  let lastLine = this.lines[this.lines.length-1];
  let lastPoint = lastLine.points[lastLine.points.length-1]
  let distance = Math.sqrt(Math.pow((x - lastPoint.x), 2) + Math.pow((y - lastPoint.y), 2))
  if (distance < 5) return;
  console.log(`==onPanUpdate:(${x},${y})========`);
  this.lines[this.lines.length-1].points.push(new Point(x, y));
  this.paint(this.context);
}

阈值越大,忽略的点就越多,线条越不精细,相对来说绘制压力也就越低,需要酌情处理。这里用 5 个逻辑像素,在操作体验上没什么影响,也能达到一定的优化效果。


5. 尾声

到这里,白板绘制的基础功能就已经完成了,当前代码位置提交一个小里程碑 v21-简单画板完成。还有些值得优化和改进的地方,比如:

  • 现在的线是通过点进行连接的折线,可以通过贝塞尔曲线进行拟合,让点之间的连接更加圆滑;
  • 可以提供一些基础图形的绘制操作,让绘制更加丰富。
  • 现在每次添加点都会将所有的内容绘制一边,随着绘制内容的增加,会带来频繁的复杂绘制。
  • 如何存储绘制的信息到本地,这样即使在退出应用后,也可以在下次开启时恢复绘制的内容。

对这些问题的改进,大家可以在今后的路途中通过自己思考和理解,来尝试解决。接下来,我们将对之前几个小项目进行整合,放入到一个项目中。


更多文章和视频知识资讯,大家可以关注我的公众号、掘金和 B 站 。关注 公众号 并回复 鸿蒙纪元 可领取最新的 xmind 脑图电子版,让我们一起成长,变得更强。我们下次再见~