这次不做孤立的线条示例,直接做一枚能放进真实页面的 TrailSeal 极光轨迹徽章:天空、山峰、帐篷、营地路线和移动信标全部由 ArkGraphics 2D 绘制;点击主题、拖动线宽、切换线帽/转角,画面会即时重绘;点击“生成成品贴纸”还能把实时画布冻结成一张离屏 PixelMap。

上面的 GIF 是 Mate 60 Pro 连续截取的 12 帧真机画面,信标沿路线移动,画面不是预制动画。

先看最终效果

默认状态是日落主题、8 px、圆头、圆角,徽章有深色玻璃画布、金属外圈、极光路径和右上角样式小面板:

同一套 Path 可以切换为极光和午夜两套颜色:

本篇的视觉方向先用 image2 做了“深海蓝 + 极光青 + 珊瑚橙”的设计参考,再在 Figma 中整理成可编辑稿,再在真机上重现:TrailSeal 极光轨迹徽章视觉重设计。设计参考图只用于确定色彩和构图,文章中的效果图和结论全部来自真机。

实验准备

项目实际环境
设备HUAWEI Mate 60 Pro
系统HarmonyOS 7.0
SDKAPI 26
开发方式ArkTS / ArkUI
图形接口@kit.ArkGraphics2D
图像接口@kit.ImageKit
权限不需要相机、相册、定位、存储或网络权限

打开应用后进入实验20即可看到设计器。这个实验不依赖相机或素材文件,所有图形在 Canvas 中生成。

1. 先把徽章拆成“填充”和“描边”

绘制顺序固定为:工作台背景 → 玻璃面板 → 圆形天空 → 远山和近山 → 帐篷 → 路线 → 动态信标 → 底部色卡。填充区域交给 Brush,轮廓和路线交给 Pen

RenderNode.draw() 只负责选择实时画布或冻结后的 PixelMap:

draw(context: DrawContext): void {
  const width: number = Math.max(1, Math.round(context.sizeInPixel.width));
  const height: number = Math.max(1, Math.round(context.sizeInPixel.height));

  if (this.previewPixelMap) {
    context.canvas.clear(BADGE_THEMES[this.stateValue.themeIndex].workspace);
    context.canvas.drawImage(this.previewPixelMap, 0, 0);
  } else {
    this.drawBadgeScene(context.canvas, width, height, this.stateValue, true);
  }
}

填充时先设置 Brush,再挂载到 Canvas,画完立即解绑:

private fillPath(canvas: drawing.Canvas, path: drawing.Path,
  color: number, antiAlias: boolean): void {
  const brush: drawing.Brush = new drawing.Brush();
  brush.setColor(color);
  brush.setAntiAlias(antiAlias);
  canvas.attachBrush(brush);
  canvas.drawPath(path);
  canvas.detachBrush();
}

2. 用 Pen 画路线和山峰边缘

路线是平滑的三段 Cubic Path,山峰边缘使用折线。Pen 的颜色、宽度、线帽、转角和抗锯齿都在 attachPen() 前设置:

private strokePath(canvas: drawing.Canvas, path: drawing.Path,
  color: number, strokeWidth: number,
  capStyle: drawing.CapStyle,
  joinStyle: drawing.JoinStyle,
  antiAlias: boolean): void {
  const pen: drawing.Pen = new drawing.Pen();
  pen.setColor(color);
  pen.setStrokeWidth(strokeWidth);
  pen.setCapStyle(capStyle);
  pen.setJoinStyle(joinStyle);
  pen.setAntiAlias(antiAlias);
  canvas.attachPen(pen);
  canvas.drawPath(path);
  canvas.detachPen();
}

页面上的三个线帽和三个转角分别映射到 ArkGraphics 2D 枚举:

private capStyle(index: number): drawing.CapStyle {
  if (index === 0) {
    return drawing.CapStyle.FLAT_CAP;
  }
  if (index === 2) {
    return drawing.CapStyle.SQUARE_CAP;
  }
  return drawing.CapStyle.ROUND_CAP;
}

private joinStyle(index: number): drawing.JoinStyle {
  if (index === 0) {
    return drawing.JoinStyle.MITER_JOIN;
  }
  if (index === 2) {
    return drawing.JoinStyle.BEVEL_JOIN;
  }
  return drawing.JoinStyle.ROUND_JOIN;
}

把滑杆拖到18 px,再选择方头和斜角:

切换为平切和尖角,路线端点和山峰转角会立即改变:

这组截图同时证明了三件事:滑杆确实改变了 Pen.setStrokeWidth(),线帽改变了路径端点,转角改变了山峰折线的连接方式。

3. 三套主题共用同一套几何数据

主题只保存颜色,不复制 Path。点击主题按钮后更新 themeIndex,RenderNode 调用 invalidate() 重绘:

updateStyle(state: BadgeStyleState): void {
  this.stateValue = {
    themeIndex: state.themeIndex,
    strokeWidth: state.strokeWidth,
    capIndex: state.capIndex,
    joinIndex: state.joinIndex,
    antiAlias: state.antiAlias,
    beaconProgress: state.beaconProgress
  };
  if (!this.previewPixelMap) {
    this.invalidate();
  }
}

日落使用珊瑚橙,极光使用青绿色,午夜使用紫色;山体轮廓和路线位置保持不变。这样切换主题只改颜色,不增加组件和 Path 数量。

4. 让信标沿路线移动

信标的位置由 0—1 的进度计算。进度小于0.5时沿第一段二次曲线移动,大于0.5时沿第二段曲线移动。Animator 每5秒循环一次,请求30 FPS:

this.animator = this.getUIContext().createAnimator({
  duration: 5000,
  easing: 'linear',
  delay: 0,
  fill: 'both',
  direction: 'normal',
  iterations: -1,
  begin: 0,
  end: 1
});

this.animator.setExpectedFrameRateRange({
  min: 30,
  max: 30,
  expected: 30
});

this.animator.onFrame = (progress: number): void => {
  if (this.released || this.previewMode) {
    return;
  }
  this.progress = progress;
  this.controller.setProgress(progress);
};

点击暂停后,按钮变成“继续信标”,当前进度停在35%,画布不再请求新帧:

等待2秒后,裁剪出的画布区域 SHA-256 仍然相同:

10948E0707C21A31F861DEB6099FA6F0830627758F3F6A2F5865A3295E327EA6
10948E0707C21A31F861DEB6099FA6F0830627758F3F6A2F5865A3295E327EA6

5. 生成离屏成品贴纸

点击“生成成品贴纸”后,页面先暂停 Animator,再按实时 Canvas 尺寸创建 RGBA_8888 PixelMap,复用同一个绘制函数,但关闭右上角的编辑样例:

const metrics: BadgeMetrics = this.badgeNode.getMetrics();
const pixelBuffer: ArrayBuffer = new ArrayBuffer(
  metrics.width * metrics.height * 4
);

const pixelMap: image.PixelMap = await image.createPixelMap(pixelBuffer, {
  editable: true,
  pixelFormat: image.PixelMapFormat.RGBA_8888,
  size: { width: metrics.width, height: metrics.height }
});

const offscreenCanvas: drawing.Canvas = new drawing.Canvas(pixelMap);
this.badgeNode.drawBadgeScene(
  offscreenCanvas,
  metrics.width,
  metrics.height,
  this.badgeNode.getState(),
  false
);
this.badgeNode.setPreview(pixelMap);

真机页面显示 OFFSCREEN、预览编号和 1174 × 884 px

回到画布顶部后,状态变成 PREVIEW,信标停止,编辑样例不再出现在成品中:

实操中遇到的情况

线宽滑杆拖动时触发了多次回调

真机拖动滑杆会连续产生 WIDTH 日志,这是正常的交互过程。最终状态以页面右上角的线宽值和最后一条 BADGE_STYLE source=WIDTH 为准;本次最终验证为18 px和8 px两组。

抗锯齿开关肉眼差异不大

高像素密度屏幕上,整屏观看不容易看出差异。测试时保持固定 Path,只切换 Brush 和 Pen 的 setAntiAlias(),再放大圆弧、山峰斜线观察。状态栏和日志均能确认开关已经进入绘制状态。

预览后画布不再动

这是预期结果:预览模式由 PixelMap 提供画面,Animator 被暂停,退出或返回实时编辑时再释放 PixelMap 并恢复 Animator。

本次真机关键日志

下面保留本次重设计后的关键日志,已去掉应用标识:

BADGE_READY themes=3 caps=3 joins=3 durationMs=5000 expectedFps=30 permissionRequired=false
BADGE_STYLE source=THEME theme=1 width=8 cap=ROUND join=ROUND aa=true
BADGE_STYLE source=THEME theme=2 width=8 cap=ROUND join=ROUND aa=true
BADGE_STYLE source=WIDTH theme=1 width=18 cap=FLAT join=MITER aa=true
BADGE_STYLE source=CAP theme=1 width=8 cap=ROUND join=MITER aa=true
BADGE_STYLE source=JOIN theme=1 width=8 cap=ROUND join=ROUND aa=true
BADGE_PAUSE progressPermille=345 drawCount=23573
BADGE_RESUME progressPermille=345 drawCount=23573
BADGE_PREVIEW success=true number=2 width=1174 height=884 progressPermille=599
BADGE_RELEASE action=EXIT mode=PREVIEW drawCount=23613 callbacks=23585 previews=2 progressPermille=599

最终结果

这枚徽章已经把 ArkGraphics 2D 的关键基础能力放进一个完整交互场景:Brush 负责天空、山体和帐篷填充,Pen 负责轮廓和路线,三套主题复用同一套 Path,线宽/线帽/转角/抗锯齿可即时调整,Animator 驱动信标沿路线移动,离屏 Canvas 输出冻结贴纸。

真机确认:新版深色玻璃视觉已落地;日落、极光、午夜三套主题可切换;18 px 描边、三种线帽和三种转角均有截图;暂停后画布区域哈希不变;离屏预览为 1174 × 884 px;退出页面产生资源释放日志。

官方资料:基础绘制效果(ArkTS)

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐