# HarmonyOS 像素画编辑器 bitArt 架构拆解:从 ArkUI 工作台到服务层

最近整理 bitArt 这个 HarmonyOS 像素画编辑器项目时,我发现它很适合作为复杂 ArkTS 应用的拆解案例。它不是只有按钮和列表的 Demo,而是一个完整的移动端创作工具:中间是像素画布,周围有工具栏、图层面板、调色板、动画帧时间轴、项目管理、导入导出和设置。

图 1:bitArt 主界面由画布、工具栏、图层、调色板和时间轴组成,交互密度接近真实生产工具。
图 2:架构上将 UI 编排、领域模型、画布渲染、图层合成、历史记录和持久化拆成多个服务模块。

## 1. 为什么编辑器不能只写在页面里

像素画编辑器看起来是 UI 密集型应用,但真正麻烦的地方不是按钮多,而是状态多。用户在画布上移动手指时,应用至少要处理工具类型、触控坐标、当前图层锁定状态、像素矩阵修改、图层合成、画布刷新、撤销快照、项目脏状态和当前动画帧同步。

如果这些逻辑全部塞进 Index.ets,页面会很快变成几千行的状态泥潭。bitArt 的处理方式是让页面只负责状态编排和组件组织,把领域逻辑放进 services 目录。

核心结构可以概括为:

Index.ets:页面入口,负责状态绑定、工具栏、弹窗、面板和服务调用。
EditorModels.ets:定义项目、图层、动画帧、工具、画布配置等数据结构。
PixelCanvasService:处理像素绘制、取色、填充、橡皮、形状等工具逻辑。
PixelCanvasRendererService:负责把像素矩阵绘制到 Canvas,并处理网格、缩放、预览。
LayerCanvasService / LayerStackService:负责图层数据、可见性、锁定、透明度和合成顺序。
EditorFrameService:维护动画帧,每一帧拥有独立的图层和持续时间。
EditorPersistenceService:负责项目保存、恢复、自动保存和脏状态管理。

## 2. 工作流拆成一条清晰流水线

一次绘制动作在 bitArt 中不是“点一下画布”这么简单,它更像一条小型流水线:

触控事件 -> 视口坐标换算 -> 工具分发 -> 修改当前图层像素 -> 标记脏区域 -> 合成可见图层 -> 刷新画布 -> 写入撤销栈 -> 标记项目待保存

这条链路带来的好处是,每个模块只回答自己的问题。坐标换算不关心图层怎么合成,图层服务不关心 ArkUI 如何布局,持久化服务也不关心用户当前用的是铅笔还是橡皮。

## 3. 数据模型是编辑器稳定性的地基

像素编辑器最怕“看起来画上去了,切换图层或切换帧就丢了”。因此 bitArt 的模型设计里,项目不是一张单独的图片,而是一组可恢复的编辑状态。

一个项目至少包含:画布尺寸、项目名、图层列表、每层像素矩阵、当前激活图层、动画帧列表、当前帧、调色板、绘制设置、撤销重做栈和自动保存配置。动画帧也不是简单截图,每一帧都有自己的 layers、layersPixels、activeLayer 和 durationMs。

这个设计让“项目恢复”“动画编辑”“图层合并”“导出 GIF”这些功能可以共享同一套数据,而不是各自维护一份不一致的副本。

## 4. 图层系统采用 Photoshop 式心智模型

bitArt 的图层不是装饰功能,而是编辑器能力的核心。每个图层都有可见性、锁定状态、透明度、名称和像素内容。渲染时从底到顶合成可见图层,用户操作只修改当前激活图层。

这样做的价值很直接:

第一,用户可以把草稿、线稿、上色、阴影拆在不同图层。
第二,锁定图层后可以防止误操作。
第三,导出 PNG 时能保留透明背景,导出 JPG 时可以按背景色展平。
第四,动画帧可以复用图层结构,而不是每次都重新发明一套状态。

## 5. 画布渲染要避免全量重绘

像素画布的性能瓶颈通常不是单个像素,而是连续拖动时的高频更新。bitArt 的优化方向是尽量把“编辑动作”和“最终合成”分开:绘制时更新当前图层和脏区域,随后由渲染服务合成可见内容并刷新 Canvas。

对于 64x64、128x128 这类常见像素画尺寸,全量合成可以接受;但一旦支持更大画布,就要谨慎控制刷新范围、缓存合成结果,并避免每个触点都触发完整项目保存。

## 6. 撤销栈保存的是编辑语义,不是用户手势

撤销功能如果按每个触点保存,会让内存迅速膨胀,也会让一次笔画需要撤销几十次。bitArt 更合理的做法是把一次连续绘制当作一个编辑动作:开始绘制前记录快照,手指抬起后提交历史记录。

这样用户的心智是稳定的:画一笔,撤销一笔;填充一次,撤销一次;合并图层一次,撤销一次。

## 7. 对 HarmonyOS / ArkTS 项目的启发

bitArt 的核心经验不是“像素画怎么画”,而是复杂应用如何拆模块:

页面负责用户体验,服务负责领域逻辑。
模型先稳定,功能才能持续叠加。
高频交互要控制刷新和保存成本。
动画、图层、导出这些功能要共享同一套真实数据。
移动端编辑器要把安全区、横竖屏、滚动面板和弹窗尺寸一起考虑。

如果你正在做 HarmonyOS 上的绘图、设计、音乐、文档或其他创作型工具,bitArt 这套拆法很值得参考。它把一个复杂编辑器拆成了可测试、可维护、可继续扩展的服务集合,让 ArkUI 页面不用承担所有复杂度。


图 1:bitArt 在 HarmonyOS 模拟器中的像素画编辑工作台。

图 2:bitArt 的 HarmonyOS 架构分层,包含 ArkUI 页面层、ArkTS 服务层、领域模型层和 HarmonyOS 系统能力。

## 关键代码片段:页面状态如何落到服务层

下面这两段来自 bitArt 的 HarmonyOS / ArkTS 源码。可以看到,Index.ets 并不直接把页面状态随手写进本地文件,而是先组装成 EditorPersistentProject,再交给持久化服务统一规范化。

源码位置:entry/src/main/ets/pages/Index.ets

```ts
private createPersistentProject(): EditorPersistentProject {
  return EditorPersistenceService.createProject({
    schemaVersion: EditorPersistenceService.SCHEMA_VERSION,
    projectId: this.currentProjectId,
    projectName: this.currentProjectName,
    updatedAt: Date.now(),
    canvasWidth: this.canvasWidth,
    canvasHeight: this.canvasHeight,
    activeTool: this.activeTool,
    activeLayer: this.activeLayer,
    activeColor: this.activeColor,
    activeFrame: this.activeFrame,
    frames: this.getSyncedFrames(),
    gridVisible: this.gridVisible,
    pixelBleedEnabled: this.pixelBleedEnabled,
    layers: this.editorLayers,
    layersPixels: this.layersPixels,
    undoStack: this.undoStack,
    redoStack: this.redoStack
  });
}
```

绘图时,页面层只负责把工具结果写回状态;真正的图层差异判断、局部合成和完整合成都交给 LayerCanvasService。这里的 dirtyBounds 就是前文提到的“只刷新受影响区域”的落点。

源码位置:entry/src/main/ets/pages/Index.ets

```ts
private setLayersPixels(layersPixels: string[][], dirtyBounds?: CanvasDirtyBounds): void {
  if (dirtyBounds) {
    if (!LayerCanvasService.hasLayersChangedInBounds(
      this.layersPixels, layersPixels, this.canvasWidth, this.canvasHeight,
      dirtyBounds.minRow, dirtyBounds.minCol, dirtyBounds.maxRow, dirtyBounds.maxCol
    )) {
      return;
    }
    this.layersPixels = layersPixels;
    this.canvasDirtyVersion++;
    this.compositePixels = LayerCanvasService.updateCompositePixels(
      this.compositePixels, layersPixels, this.editorLayers,
      this.canvasWidth, this.canvasHeight,
      dirtyBounds.minRow, dirtyBounds.minCol, dirtyBounds.maxRow, dirtyBounds.maxCol
    );
    return;
  }

  if (!LayerCanvasService.hasLayersChanged(this.layersPixels, layersPixels)) {
    return;
  }
  this.layersPixels = layersPixels;
  this.compositePixels = LayerCanvasService.createCompositePixels(
    layersPixels, this.editorLayers, this.canvasWidth, this.canvasHeight
  );
}
```

这也是 bitArt 架构拆分的核心:ArkUI 页面只编排状态,像素矩阵、图层合成、帧同步和持久化规则放到 service 层,避免 Index.ets 变成所有业务逻辑的“大杂烩”。
 

Logo

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

更多推荐