最近整理一个 HarmonyOS 像素画编辑器项目 bitArt发现它很适合作为复杂 ArkTS 应用的拆解案例。它不是一个只有按钮和列表的 Demo而是一个完整的移动端创作工具中间是像素画布周围有工具栏、图层面板、调色板、动画帧时间轴、项目管理、导入导出和设置。这篇先不急着讲某一个算法而是从整体架构看它如何把复杂编辑器拆开。源码路径是D:\HM\bitArt核心代码集中在entry/src/main/ets。1. 为什么编辑器应用不能只写在页面里像素画编辑器看起来是 UI 密集型应用但真正麻烦的地方不是按钮多而是状态多。一个用户在画布上移动手指时应用至少要处理这些事判断当前工具是铅笔、橡皮、填充、取色、形状还是移动。把触控坐标转换成像素坐标。判断当前图层是否锁定。修改当前图层的像素矩阵。重新合成可见图层。更新画布渲染。记录撤销快照。标记项目已修改等待自动保存。如果时间轴打开还要同步当前帧。如果这些逻辑全部塞进一个页面的 UI 构建代码里页面会很快变成一个几千行的状态泥潭。bitArt 当前把入口、工作区组件和领域服务分开Index.ets只处理页面可见性并挂载工作区BitArtEditorWorkspace.ets负责状态编排具体行为继续下沉到services/。项目核心目录大致是entry/src/main/ets/ pages/ Index.ets components/editor/ BitArtEditorWorkspace.ets PixelCanvasView.ets models/ EditorModels.ets services/ PixelCanvasService.ets PixelCanvasRendererService.ets LayerCanvasService.ets LayerStackService.ets EditorCanvasDrawingWorkflowService.ets EditorHistoryWorkflowService.ets EditorFrameService.ets EditorPersistenceService.ets EditorExportService.ets ImageImportService.ets这个拆分的好处是非常直接的画布算法、图层算法、帧算法、保存算法都可以独立阅读和测试。页面不需要知道“一个 GIF 怎么编码”也不需要知道“source-over 混合公式怎么写”它只需要调用服务层拿到新的状态。2. 工作区组件BitArtEditorWorkspace 是状态编排器Index.ets当前是一个很薄的页面壳页面出现或隐藏时更新editorPageVisiblebuild()中只挂载BitArtEditorWorkspace()。真正持有当前工具、颜色、图层、帧、画布尺寸、面板开合、撤销栈和项目列表的是BitArtEditorWorkspace.ets。例如工作区组件初始化图层和帧时并不手工拼像素数组const INITIAL_LAYERS_PIXELS: string[][] LayerCanvasService.createInitialLayers(LAYERS, DEFAULT_CANVAS_SIZE, DEFAULT_CANVAS_SIZE); const INITIAL_FRAMES: EditorFrame[] EditorFrameService.createInitialFrames(LAYERS, INITIAL_LAYERS_PIXELS, layer-1);这段代码背后的设计点是页面不直接制造像素矩阵而是把“初始图层应该是什么样”交给LayerCanvasService把“第一帧应该如何复制图层数据”交给EditorFrameService。换句话说BitArtEditorWorkspace更像一个工作台调度器它决定当前展示哪些面板。它把用户操作转换成服务层调用。它接收服务返回的新状态。它触发 UI 刷新、保存和导出。这种拆分对编辑器应用很重要。因为编辑器的功能会持续增长如果页面从一开始就承担所有业务细节后面新增图层合并、动画帧、GIF 导出时代码会很难维护。3. 组件层PixelCanvasView 专心处理画布交互bitArt 把真正的 Canvas 组件放到了components/editor/PixelCanvasView.ets。这个组件关注的是“画布如何显示”和“触摸如何转成像素坐标”。它接收像素数据、画布宽高、缩放比例、脏区域边界等属性Prop Watch(handlePixelsChange) pixels: string[] []; Prop Watch(redraw) gridVisible: boolean true; Prop Watch(handleCanvasSizeChange) canvasWidth: number 24; Prop Watch(handleCanvasSizeChange) canvasHeight: number 24; Prop Watch(handleDirtyVersionChange) dirtyVersion: number 0; Prop dirtyMinRow: number 0; Prop dirtyMinCol: number 0; Prop dirtyMaxRow: number 0; Prop dirtyMaxCol: number 0;这里有一个值得借鉴的点组件没有自己去管理项目、图层、帧它只认最终要画出来的pixels。这样 Canvas 组件就可以保持纯粹复用成本也更低。用户触摸画布时组件把屏幕坐标转换成像素行列再通过回调通知页面private getTouchCell(event: TouchEvent): CanvasCell { const x event.touches[0].x - this.getDrawLeft(); const y event.touches[0].y - this.getDrawTop(); return { row: EditorViewportService.positionToCell(y, drawHeight, this.canvasHeight), col: EditorViewportService.positionToCell(x, drawWidth, this.canvasWidth), valid: true }; }组件层只负责把“用户点了哪里”说清楚至于点下去以后是画笔、橡皮、填充还是取色交给上层状态和服务层决定。4. 服务层复杂逻辑按领域拆开bitArt 的services/是项目最值得看的一层。几个核心服务的职责如下服务职责PixelCanvasService单层像素矩阵操作负责画笔、橡皮、填充、形状、取色PixelCanvasRendererServiceCanvas 渲染负责网格、脏区域、像素柔化和同色连续段批绘LayerCanvasService多图层像素合成负责透明度混合和局部合成LayerStackService图层栈操作负责新增、删除、复制、锁定、合并EditorFrameService动画帧操作负责帧复制、删除、时长归一化EditorPersistenceService项目持久化负责 manifest 和项目 JSON自动保存时机由会话工作流控制EditorExportService图片导出负责 PNG/JPG/GIF 和相册写入ImageImportService图片导入负责相册选择、解码、缩放和转像素矩阵这个服务划分的关键是“按领域而不是按按钮拆”。比如图层面板上可能有删除、复制、锁定、透明度调整等多个按钮但它们都属于图层栈行为所以统一放在LayerStackService。5. 模型层让状态可以保存、恢复和撤销bitArt 的模型集中在EditorModels.ets。核心模型包括export interface EditorFrame { id: string; name: string; durationMs: number; layers: LayerItem[]; layersPixels: string[][]; activeLayer: string; } export interface EditorPersistentProject { schemaVersion: number; projectId: string; projectName: string; canvasWidth?: number; canvasHeight?: number; activeTool: string; activeLayer: string; activeFrame: string; frames: EditorFrame[]; layers: LayerItem[]; layersPixels: string[][]; undoStack: EditorSnapshot[]; redoStack: EditorSnapshot[]; }这里可以看到一个重要选择帧不是只保存一张预览图而是保存完整的layers和layersPixels。这意味着每一帧都有独立图层结构动画编辑不会和当前画布状态混在一起。对于编辑器来说这种数据完整性比省一点内存更重要。否则切换帧、复制帧、导出 GIF 时很容易出现“当前画面和帧数据不同步”的问题。6. 系统能力层HarmonyOS API 的落点bitArt 用到的 HarmonyOS 能力主要集中在几个地方kit.AbilityKit拿到UIAbilityContext用于文件路径和应用上下文。kit.CoreFileKit读写项目 JSON、缓存导出图片。ohos.multimedia.image创建 PixelMap、编码 PNG/JPG。ohos.file.photoAccessHelper选择图片、写入系统相册。AppStorage同步安全区、持久化触发信号等跨层状态。这种系统 API 不直接散落在页面里的做法也很稳。比如保存项目时只需要看EditorPersistenceService导出图片时只需要看EditorExportService。后续如果 HarmonyOS API 有变化修改范围也更明确。7. 怎么判断分层没有停留在目录表面文件分得多不等于架构清晰。验证 bitArt 的分层可以沿一条真实操作向下追踪用户在画布落笔后PixelCanvasView只上报触控位置绘制工作流决定当前工具和脏区域像素服务修改活动层图层服务更新合成结果工作区负责替换状态、记录历史并标记项目已修改。每一层都有输入和输出没有让 Canvas 组件直接写项目文件也没有让持久化服务反过来操作 UI。再检查几个跨模块动作切换动画帧前是否同步工作区图层导出前是否结束笔触并强制保存恢复项目后是否重新建立活动帧和活动图层锁定图层后绘制工作流是否在修改像素前拦截。只要这些路径能顺着服务边界走完分层才真正参与了行为控制而不只是把长文件拆成多个短文件。8. 这个架构最值得借鉴的点我认为 bitArt 的整体设计有三个经验可以复用到其他 HarmonyOS 应用。第一页面层不要吞掉业务层。复杂应用早期看起来可以靠一个页面撑住但状态一多维护成本会急剧上升。第二编辑器应用要先定义数据所有权。bitArt 让项目、帧、图层、像素矩阵都有明确归属之后撤销、保存、导出才能顺畅接上。第三高频交互要单独设计渲染路径。像素画布不是普通列表它有连续触摸、缩放、平移、局部刷新和网格渲染。如果直接依赖全量刷新体验很快会掉下来。这一篇主要把项目全貌铺开。下一篇会继续拆EditorModels.ets和EditorCanvasService.ets看一个像素画编辑器的数据模型如何设计尤其是为什么 bitArt 选择LayerItem[] string[][]这种结构来承载图层像素。