原生鸿蒙像素画板实战 14:工具状态管理
编辑器工具一多,最怕的不是按钮不够,而是状态互相污染。用户刚把橡皮擦调成 6px,不应该切回铅笔后发现笔刷也变成 6px;填充透明度的调整也不该影响橡皮擦。形状工具还多了 activeShape,选区又有“是否已选中”和尺寸提示。若这些判断散落在每个组件里,页面很快会出现同一状态被不同面板解释成不同结果。bitArt 把工具标签、当前尺寸、透明度和写入规则集中到 EditorToolStateService。

工具状态的核心是区分共享字段和专属字段
常见的实现方式是一个全局 size 和一个全局 opacity。短期看很省事,实际会让不同工具失去独立配置,用户每次切换都得重新调参数。另一个常见问题是 UI 直接写 activeTool 后,历史面板、顶部状态条、工具栏又各自写一遍“当前工具名称”,形状或选区状态一变,就有一个区域忘了更新。
工具配置由谁保存,谁只负责显示
工具状态不是把几个字段塞进页面就结束了。尺寸、透明度和标签都需要根据当前工具推导,才能让面板切换后仍然说同一种状态语言。

同一个滑杆如何服务不同工具
当前服务明确区分 brushSize 与 eraserSize,透明度则分为 brushOpacity、eraserOpacity、fillOpacity。getActivePaintSize 和 getActiveToolOpacity 根据 activeTool 取值,setActivePaintSize 与 setActiveToolOpacity 则只写回真正属于当前工具的字段。页面因此只需绑定“当前可调参数”,而不必知道当前是铅笔还是橡皮擦。
形状名称也通过 normalizeShape 和 getShapeLabel 统一处理。无效的持久化值回退到 line,顶部状态则可根据 activeTool、activeShape 和 selectionActive 推导为“形状 · 椭圆”或“选区 8x6”。这让显示文本成为状态的函数,而不是额外保存的一份易过期数据。
工具服务的价值体现在:页面只问“当前参数是什么”,不必自己维护一套工具分支。
static getActivePaintSize(activeTool: string, brushSize: number, eraserSize: number): number {
return activeTool === 'eraser' ? eraserSize : brushSize;
}
static getActiveToolOpacity(activeTool: string, brushOpacity: number, eraserOpacity: number,
fillOpacity: number): number {
if (activeTool === 'eraser') return eraserOpacity;
if (activeTool === 'fill') return fillOpacity;
return brushOpacity;
}
状态标签要从事实推导,而不是手工拼接
工具状态服务不直接接触 Canvas,也不直接安排保存。它只返回 changed、下一组尺寸或透明度。工作流收到 changed 后再做 requestUiRefresh 与 markProjectDirty。这个分层很朴素,却能避免一个滑杆拖动触发多条保存和多次历史记录。参数规范化也集中在服务里:size 经过 normalizePaintSize,透明度四舍五入并夹在 0 到 100 之间。
static setActivePaintSize(activeTool: string, brushSize: number, eraserSize: number, size: number) {
const activePaintSize = EditorToolStateService.getActivePaintSize(activeTool, brushSize, eraserSize);
const normalizedSize = normalizePaintSize(size, activePaintSize);
return activeTool === 'eraser' ?
{ changed: eraserSize !== normalizedSize, brushSize, eraserSize: normalizedSize } :
{ changed: brushSize !== normalizedSize, brushSize: normalizedSize, eraserSize };
}

参数属于工具,显示文本属于推导结果
当前工具改变时,旧工具的参数仍留在自己的字段中;当前工具读取时才切换到正确值。选择形状只影响 activeShape,不会覆盖画笔或填充配置。对于选区,selectionActive 和宽高来自画布状态,而不是工具服务凭空维护。这样“工具配置”和“当前编辑对象”保持了两个独立来源。
尺寸和透明度的字段归属固定后,切换工具只是换读取视图,不会把旧工具的配置覆盖掉。
static getActiveToolStatusLabel(activeTool: string, activeShape: string, selectionActive: boolean,
selectionWidth: number, selectionHeight: number): string {
if (activeTool === 'shape') return '形状 · ' + EditorToolStateService.getShapeLabel(activeShape);
if (activeTool === 'selection' && selectionActive) {
return '选区 ' + selectionWidth.toString() + 'x' + selectionHeight.toString();
}
return EditorToolStateService.getToolLabel(activeTool);
}

工具切换时常见的状态串扰
- 不要把铅笔、橡皮擦和填充共用一个可写的尺寸或透明度字段。
- 不要把“当前状态文本”持久化为独立字符串,应根据 activeTool 和实际选区大小计算。
- 参数变化前先规范化,避免恢复项目后出现负数尺寸、超过 100 的透明度或未知形状。
这种状态拆分还有一个实际收益:功能新增时可预测。增加喷枪或镜像笔刷时,只需决定它复用哪一种参数模型,再在服务里添加映射,而不是在每个控制面板加一串 if。对复杂编辑器而言,规则集中比一开始少写几个字段更重要。
从面板行为反查工具状态
工具状态可以用一张切换表来回归:先为铅笔设置尺寸和透明度,再为橡皮擦设置另一组值,为填充设置第三组透明度,然后按照“铅笔—填充—橡皮擦—形状—选区—铅笔”的顺序切换。每到一个工具都读取顶部标签、滑杆值和实际绘制结果。若其中一个值被上一个工具覆盖,问题通常不在控件,而在参数归属或归一化服务。
另一个值得检查的路径是项目恢复。把当前工具、形状、三类透明度和两种尺寸保存后重开项目,确认无效值被规范化、合法值保持原样。这样能避免只在实时操作正常、恢复后却出现未知形状或 0px 笔刷。对于编辑器而言,状态切换和状态恢复同样重要,二者都应该由同一个服务规则解释。
新工具加入时如何避免状态爆炸
工具服务还为后续扩展留出了一个实用检查点:新增工具前先问它属于哪一类参数模型。若它与铅笔一样使用大小和透明度,可以复用现有映射;若它只读数据,例如取色器,就不应在尺寸滑杆上制造一个无意义的值;若它拥有新的专属参数,则应新建清楚的字段和规范化函数,而不是借用 brushSize 再给它附加隐藏语义。
这种分类也能改善界面可读性。工具栏只展示当前工具真正可用的控制项,形状工具显示形状选择,选区工具显示范围信息,取色器不显示无效的画笔尺寸。用户不需要猜“这个滑杆会不会影响当前操作”,代码也不需要维护一堆仅为隐藏控件而存在的临时条件。

怎样确认工具参数能独立保留
工具状态检查以切换前后参数是否各自保留为主,不把 UI 标签正常误当作配置正确。
- 分别设置铅笔和橡皮擦尺寸,来回切换工具,确认两个值各自保留。
- 设置填充透明度后切换到画笔,确认画笔透明度不变;再切回填充,确认原值仍在。
- 把 activeShape 恢复为非法字符串,确认界面和绘制逻辑回退到直线。
- 激活选区后观察顶部标签,确认宽高随拖动变化,清除选区后不再显示旧尺寸。
工具状态检查记录
铅笔与橡皮尺寸独立保存
三类透明度互不覆盖
无效形状回退为直线
选区标签来自实时边界,不保存旧文本

工具状态并不是完整命令系统
当前工具状态不是完整的命令系统,也没有为每个参数建立独立的撤销步骤。笔刷尺寸和透明度属于项目配置,用户通常不希望撤销一笔时顺带回退工具滑杆;像素修改才进入历史栈。
这一篇的重点是让每个工具保存自己的配置,并让显示状态由事实推导。工具能稳定切换后,颜色输入就是下一块容易失控的状态:调色板、RGB、HEX 和背景色要如何对齐。
更多推荐




所有评论(0)