发布时间:2026/9/2 16:09:50
bitArt 系列 01:架构 最近整理一个 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[][]这种结构来承载图层像素。

相关新闻

2026/9/2 13:52:25

专业铸就口碑!细数黑龙江移远科技的核心服务优势

在黑龙江无线通信行业,供应商数不胜数,但能长期稳定服务政企、景区、林场、油田、市政、工程企业的本土品牌,黑龙江移远科技始终凭借专业、靠谱、本土化的服务优势,赢得市场认可与客户信赖。第一大优势:深耕本土&#…

2026/9/1 1:44:24

AI视频生成技术实战:从扩散模型到工程化应用全解析

最近在技术圈里,一个名为"2026红尘CK冲榜宣传片"的项目引起了广泛讨论。很多开发者第一眼看到这个标题时都会感到困惑——这到底是游戏宣传片、影视项目,还是某种新型的技术应用?实际上,这个项目背后涉及的是当前最热门…

2026/8/31 20:41:26

蓝牙5.4音频模块开发实战:低功耗LC3编码与射频设计

1. 项目背景与核心组件选型在无线音频传输领域,蓝牙5.4标准的推出标志着LE Audio技术正式进入实用阶段。我们选择的IDC777-1模块作为射频前端,搭配STM32L152RE主控的方案,能够实现专业级无线音频传输质量。这套组合的核心优势在于&#xff1a…

2026/9/2 16:05:50

Zexor文件管理器:从仓库到工作台,重塑高效文件操作体验

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/2 16:05:50

RustFS 生命周期与分层:让冷数据自动搬家、热数据自己回家

一个训练日志桶,三个月前的调试日志没人看了,却还在占着高性能 NVMe 的贵价空间。我见过不少团队靠手写 cron 脚本定期 mc rm 来清过期数据,结果脚本哪天挂了没人发现,磁盘悄悄被塞满。RustFS 把这类需求做成了原生的生命周期&…

2026/9/2 16:05:50

STM32驱动AD9833实现DDS波形发生器:原理、代码与调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/2 16:05:50

ADB安装与连接全攻略:从环境配置到设备调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/2 16:05:50

AI漫剧制作教程:人机协作的半自动流程,从剧本到成片

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/2 16:00:49

授权环境下的密码恢复:从哈希破解到安全工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/1 16:02:17

vSound小提琴数字处理器实操指南:从接线到演出的完整配置

电小提琴或者原声小提琴插电演出,第一个绕不开的坎就是声音难听。原声琴的共鸣和空气感一旦进了拾音器,出来的往往是一坨干瘪、发尖、带着奇怪塑料味的信号。我当初第一次把琴接上乐队调音台,直接被主唱吐槽"你这声音像在锯钢丝"。…

2026/9/2 9:00:32

传感器接口IC如何攻克生物化学传感的微弱信号难题?

1. 从电极到比特流:为什么生物化学传感必须依赖专用接口IC 做生物化学传感的人都有过类似的经历:明明传感器本身性能很好,信号输出却一塌糊涂——噪声大、漂移明显、重复性差,怎么调都达不到预期。很多时候问题并不在传感器&#…

2026/9/2 8:41:06

STM32F411CEU6多通道ADC采集:扫描模式+DMA实现详解

1. 多通道 ADC 的用武之地把“Multichannel ADC”和“STM32F411CEU6”这两个关键字放在一起,其实就是嵌入式开发里最常遇到的一类需求:用一块不算贵的 MCU,同时采集多路模拟信号。STM32F411CEU6 是 48 引脚的 Cortex-M4F 主控,主频…

2026/9/2 0:03:41

单片机毕业设计-基于单片机与蓝牙通讯的输液状态监测终端设计与开发 基于 STM32 或 51 单片机的液位‑滴速‑温度多参数输液监护装置设计(024005)

博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机,Java、小程序技术领域和毕业项目实战 ✌️…

2026/9/2 0:03:41

DeepSeek字幕翻译实战:从API调用到批量SRT转中文的完整方案

这次我们来看一个很实用的 DeepSeek 落地场景:用 DeepSeek 把英文视频字幕自动翻译成中文。具体案例是《恶魔君》1989 年第 28 集的英转中字幕任务,标题写得很直白,但背后其实是一整套可以复用的技术流程:字幕解析、模型调用、批量…

2026/9/2 0:03:41

用Python搭建搞笑语音助手:从语音识别到语音合成全教程

当你家里摆着一台天猫精灵,却总希望语音助手偶尔“不正经”一点,不用官方腔回答问题,而是张口就接几句搞笑段子,会是什么体验?我最近动手验证了一下这个想法——没有去改装任何市面上现有的智能音箱,而是直…

2026/9/2 1:15:22

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/2 1:15:22

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/2 1:15:20

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…