鸿蒙纪·梦始卷#12 | 画板绘制 - 手势处理

282 阅读5分钟

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

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

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


上一篇,我们简单了解一下鸿蒙中使用 Canvas 自定义的方式,其中涉及到绘制操作在知识树中整理了一下,如下所示。但这只是绘制操作的很少一部分,绘制方面的知识以后有机会再详细介绍。这点


1. 需求分析与数据准备

这一篇将实现手势交互的画板基本功能,效果如下所示:

  • 支持手指在屏幕上绘制线条;
  • 右上角按钮可以弹出清除提示框,点击确认时清空画板。
绘制清空绘制
  • 数据准备

界面上需要呈现多条线,而线是由若干点构成的。另外可以指定线的颜色和粗细。所以这里可以封装一个 Line 类维护这些数据;其中点的坐标通过 Point 类记录:

export class Line {
  points: Point[];
  color: string;
  strokeWidth: number;

  constructor(
    points: Point[],
    color: string = '#000000',
    strokeWidth: number =2
  ) {
    this.points = points;
    this.color = color;
    this.strokeWidth = strokeWidth;
  }
}

export class Point {
  readonly x: number = 0;
  readonly y: number = 0;

  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}

  • 业务逻辑类

我们将数据的维护逻辑统一维护在 PainterBloc 中,目前的核心数据是 Line 数组 记录线列表信息;另外绘制的具体逻辑也可以放在其中,以减轻界面组件中的逻辑处理

import { Line, Point } from '../model/Line';

export class PainterBloc {
  @Track lines: Line[] = []; // 线列表

  private settings: RenderingContextSettings = new RenderingContextSettings(true);
  @Track context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);

}

线列表数据的维护和用户的拖拽事件息息相关:

  • 用户开始拖拽开始时,需要创建 Line 对象,加入线列表。
  • 用户拖拽过程中,将触点添加到线列表最后一条线中。
  • 用户点击清除时,清空线列表。

2. 平移手势事件

鸿蒙开发中,手势事件 gesture 是所有组件的通用方法。平移手势事件通过 PanGesture 设置, 如下所示:

  • onActionStart: 监听平移手势开始的时机。
  • onActionUpdate: 监听平移手势更新的时机。

两个函数都会回调 GestureEvent 对象,通过它可以得到手势的具体信息,比如偏移量、落点等。也是我们数据的来源:

---->[pages/painter/view/Painter.ets]----
Canvas(this.model.context)
  .width('100%')
  .layoutWeight(1)
  .backgroundColor('#fafafa')
  .gesture(
    PanGesture()
      .onActionStart((e) => this.onPanStart(e))
      .onActionUpdate((e) => this.onPanUpdate(e)),
  )
  
onPanUpdate(e: GestureEvent): void {
  // TODO
}

onPanStart(e: GestureEvent): void {
    // TODO
}

在视图层中依赖 PainterBloc 对象,手势事件中回调对象的 fingerList 记录了触点信息,这里取第一个触点的坐标。通过 PainterBloc 维护坐标数据:

  • onPanStart 开始平移时,通过 PainterBloc#newLine 添加一条线;
  • onPanUpdate 平移更新时,通过 PainterBloc#updateLine 更新最后一条线的点集;
@State model: PainterBloc = new PainterBloc();

---->[pages/painter/view/Painter.ets]----
onPanStart(e: GestureEvent): void {
  if (e.fingerList.length < 0) {
    return;
  }
  let x = e.fingerList[0].localX;
  let y = e.fingerList[0].localY;
  this.model.newLine(x, y);
}

onPanUpdate(e: GestureEvent): void {
  if (e.fingerList.length < 0) {
    return;
  }
  let x = e.fingerList[0].localX;
  let y = e.fingerList[0].localY;
  this.model.updateLine(x, y);
}

添加线时创建 Line 对象,添加到 lines 列表中;更新时,将当前的点放到最后一条线中。最后在每次更新时,重新绘制即可,这样就可以完成绘制的功能了:

---->[pages/painter/bloc/PainterBloc.ets]----
// 开始平移时,添加新线
newLine(x: number, y: number) {
  this.lines.push(new Line([new Point(x, y)]));
}

// 平移更新时,为新线添加点
updateLine(x: number, y: number) {
  this.lines[this.lines.length-1].points.push(new Point(x, y));
  this.paint(this.context);
}

绘制逻辑也放在了 PainterBloc 中,根据 Line 数组创建一条条线路径完成线条绘制:

paint(canvas: CanvasRenderingContext2D) {
  canvas.clearRect(0, 0, this.context.width, this.context.height);
  for (let i = 0; i < this.lines.length; i++) {
    let cur = this.lines[i];
    this.drawLine(canvas, cur);
  }
}

///根据点集绘制线
drawLine(canvas: CanvasRenderingContext2D, line: Line) {
  canvas.strokeStyle = line.color;
  canvas.lineWidth = line.strokeWidth;
  canvas.beginPath();
  let count = line.points.length;
  if (count == 1) return;
  let first = line.points[0];
  canvas.moveTo(first.x, first.y);
  for (let i = 1; i < count; i++) {
    let cur = line.points[i];
    canvas.lineTo(cur.x, cur.y);
  }
  canvas.stroke();
}

3. 对话框与清空绘制

通过 AlertDialog.show 方法可以弹出通用形式的对话框,能设置标题、信息、按钮等。点击确定时,通过 PainterBloc#clear 方法清空数据:

showClearDialog(): void {
  AlertDialog.show(
    {
      title: '清空操作提示',
      message: '您的当前操作会清空所有的绘制内容,是否确定继续!',
      autoCancel: true,
      cornerRadius: 8,
      alignment: DialogAlignment.Center,
      gridCount: 4,
      primaryButton: { value: '取消', action: () => {} },
      secondaryButton: {
        enabled: true,
        defaultFocus: true,
        style: DialogButtonStyle.HIGHLIGHT,
        backgroundColor: '#fe5455',
        value: '确定',
        action: () => this.model.clear()
      }
    }
  );
}

清空绘制逻辑非常简单,就是将线数据置为空列表,再绘制即可。这里注意一下,和 html 的绘制一样,鸿蒙每次绘制并不会主动清空画板,如果希望画板内容由数据控制,最好在绘制前通过 canvas.clearRect 清空一下当前绘制内容:

clear(): void {
  this.lines = [];
  this.paint(this.context);
}

这里提交一个小里程碑 v18-手势交互、绘制


4. 尾声

到这里,我们就完成了画板绘制最基础的能力,接下来将继续实现线条粗细和颜色的切换功能。
更多文章和视频知识资讯,大家可以关注我的公众号、掘金和 B 站 。关注 公众号 并回复 鸿蒙纪元 可领取最新的 xmind 脑图电子版,让我们一起成长,变得更强。我们下次再见~