HarmonyOS ArkUI:十字绣符号叠加与色觉友好图例(连载07)【共创稿事节】
《海浪角》的编辑器已经完成落针、镜像预览、缩放触点定位、平板分栏和撤销重做。前一篇在第五行第五列落下湖蓝针后,图谱从四针变成五针,线色统计随编辑命令重新计算。不过,用户把画布缩小到适合手机屏幕的宽度时,乳白针与浅色绣布的区别并不醒目;湖蓝和珊瑚橙虽然有色相差异,在低亮度、灰度显示或颜色识别困难的情况下,也不宜成为辨认针位的唯一依据。
本篇沿用同一份八乘八业务数据,不修改历史命令,也不增加落针。湖蓝B在第二行第二、三列和第五行第五列,共三针;珊瑚橙C在第三行第四列,乳白A在第四行第四列,各一针。新增的是独立于线色的符号层:A对应空心圆,B对应实心菱形,C对应实心三角。用户可切换“仅颜色”与“颜色+符号”,但图谱稳定键、库存需求与撤销栈保持不变。

把图案识别与业务颜色分成两层
以前绘制格子时,可以直接拿colorId选择底色:有针就填色,空格就显示布纹。这样的渲染方式简单,却将业务分类与视觉表达绑定在一起。真正需要长期维护的应当是StitchCell里的行、列和颜色编码;背景色、笔画、文字和符号都是渲染策略。假如将来调整湖蓝的色值,或提供高对比度主题,修改的应是绘制配置,而不应批量改写五条针位记录。
这里沿用A、B、C三个稳定编码,建立明确的符号表。A使用深色轮廓空心圆,配乳白色格底;B用黑色菱形配湖蓝;C用黑色三角配珊瑚橙。每个符号放在单格中心,并为选中格使用另一层虚线边框。这样即便用户关闭颜色提示,仍能凭符号和文字图例区分针位;开启颜色时也有两个彼此独立的识别线索。格子的选中边框不应和B的菱形混为同一概念,前者表示操作焦点,后者表示针线种类。
export type ThreadColor = 'A' | 'B' | 'C';
export interface StitchCell {
row: number;
col: number;
colorId: ThreadColor;
}
export interface ColorLegend {
colorId: ThreadColor;
name: string;
symbol: string;
background: string;
foreground: string;
}
export const LEGEND: ColorLegend[] = [
{ colorId: 'A', name: '乳白', symbol: '○', background: '#F5EBDD', foreground: '#12243B' },
{ colorId: 'B', name: '湖蓝', symbol: '◆', background: '#57BCE0', foreground: '#12243B' },
{ colorId: 'C', name: '珊瑚橙', symbol: '▲', background: '#FF9868', foreground: '#12243B' }
];
export const STITCHES: StitchCell[] = [
{ row: 2, col: 2, colorId: 'B' },
{ row: 2, col: 3, colorId: 'B' },
{ row: 3, col: 4, colorId: 'C' },
{ row: 4, col: 4, colorId: 'A' },
{ row: 5, col: 5, colorId: 'B' }
];
export function legendOf(id: ThreadColor): ColorLegend {
const item: ColorLegend | undefined = LEGEND.find(
(entry: ColorLegend) => entry.colorId === id
);
return item ? item : LEGEND[0];
}
这里采用确定的颜色值与深色前景,避免在页面中各自散落十几处色号。空格不能使用A的乳白底色,否则绣布本来的空白会被误识为已落针。后续若提供用户自定义配色,可以先校验三种颜色是否都具备符号与文字,再替换配置对象。对于来自旧图谱的非法颜色编码,应当停在数据检查阶段,不把它随意映射成A,以免把错误数据画成真实乳白针。
八乘八网格只投影一次
每一个屏幕格子都需要自己的稳定标识。延续前文row-col的键格式,第五行第五列的键仍是5-5。渲染时由五条已落针记录产生一个坐标索引,再按行优先生成六十四个视图格子;空白格子的颜色ID设置为空字符串,标注为“未落针”。不要为了符号模式另建第二份画布数组,因为用户切换开关时,第二份数组可能停留在撤销之前的四针版本。只要底层针位快照改变,就重新生成一份投影,符号开关仅参与显示。
export interface CellView {
key: string;
row: number;
col: number;
colorId: ThreadColor | '';
}
export function buildCells(stitches: StitchCell[]): CellView[] {
const result: CellView[] = [];
for (let row: number = 1; row <= 8; row++) {
for (let col: number = 1; col <= 8; col++) {
const found: StitchCell | undefined = stitches.find(
(item: StitchCell) => item.row === row && item.col === col
);
result.push({
key: `${row}-${col}`, row, col,
colorId: found ? found.colorId : ''
});
}
}
return result;
}
export function countStitches(stitches: StitchCell[], id: ThreadColor): number {
return stitches.filter((item: StitchCell) => item.colorId === id).length;
}
export function describeCell(cell: CellView): string {
if (cell.colorId === '') {
return `第${cell.row}行第${cell.col}列,未落针`;
}
const legend: ColorLegend = legendOf(cell.colorId);
return `第${cell.row}行第${cell.col}列,${legend.name},符号${legend.symbol}`;
}
find的线性搜索在八乘八、五个落针的当前数据规模下很直观;如果图谱扩大到数千针,可以先构建以坐标键为索引的查找表,减少绘制时重复扫描。无论内部查找方式怎样变化,最终仍按六十四个位置的固定顺序输出,这一点关系到键控更新与屏幕阅读顺序。计数同样直接从针位推导,A为一针、B为三针、C为一针,不能按图例出现次数计算库存。
ArkUI中把符号、选中框和文本统一呈现
页面采用Grid承载六十四个GridItem,内部用Stack组合底色与符号文字。切换开关仅改变showSymbols,编辑服务保持独立;选中格继续使用上一轮的selectedKey。建议为格子设置明确的无障碍文字,把行列和实际线色一起表达,而不是让读取结果仅剩一个“菱形”。对于空格则说清“未落针”,避免用户将布纹误听作乳白针。
@Entry
@Component
struct SymbolBoardPage {
@State private showSymbols: boolean = true;
@State private selectedKey: string = '5-5';
@State private cells: CellView[] = buildCells(STITCHES);
private colorFor(cell: CellView): string {
return cell.colorId === '' ? '#FFFFFF' : legendOf(cell.colorId).background;
}
private symbolFor(cell: CellView): string {
return cell.colorId === '' ? '' : legendOf(cell.colorId).symbol;
}
build() {
Column({ space: 12 }) {
Text('海浪角 · 符号图例').fontSize(23).fontWeight(FontWeight.Bold)
Row({ space: 10 }) {
Text('颜色+符号')
Toggle({ type: ToggleType.Switch, isOn: this.showSymbols })
.onChange((value: boolean) => { this.showSymbols = value; })
}
Grid() {
ForEach(this.cells, (cell: CellView) => {
GridItem() {
Stack() {
Rect().width(38).height(38).fill(this.colorFor(cell))
if (this.showSymbols && cell.colorId !== '') {
Text(this.symbolFor(cell))
.fontSize(20).fontColor('#12243B')
}
}
.border({ width: cell.key === this.selectedKey ? 2 : 1,
color: cell.key === this.selectedKey ? '#157DE5' : '#D9E1EA' })
.accessibilityText(describeCell(cell))
.onClick(() => { this.selectedKey = cell.key; })
}
}, (cell: CellView) => cell.key)
}
.columnsTemplate('repeat(8, 1fr)')
.rowsTemplate('repeat(8, 1fr)')
.width(320).height(320)
Text('A 乳白 ○ 1针 · B 湖蓝 ◆ 3针 · C 珊瑚橙 ▲ 1针')
Text(`选中位置 ${this.selectedKey} · 已落针 ${STITCHES.length}/64`)
}.padding(12)
}
}
这段页面以固定宽度表达网格结构,整合到第五篇的容器分栏后,应从可用容器宽度计算单格尺寸,同时保持八列八行和符号居中。不要只按整张画布缩放文字:在较窄的窗口中可以提高单元格的最小可触摸范围,或提供放大模式,避免文字被压成很小的点。accessibilityText是ArkUI组件属性;对外可参考华为开发者的屏幕朗读适配指导及Grid与GridItem布局文档。实际采用的组件属性和版面阈值还应结合目标SDK与窗口尺寸核对。

复查撤销重做与配色开关的组合
界面最值得排查的不是单次开关点击,而是操作顺序交叉。先保持五针、选中5-5,开启符号显示;按撤销时,第五行第五列变为空白,B计数恢复两针,图例文字随快照更新,开关状态仍保持打开。再按重做,B回到三针、第五行第五列重新出现菱形,图例和选中框保持原有含义。关闭符号模式后,画布只隐藏符号,不会重新落针,也不应消耗库存。
此外,若用户在平板双栏中选中第五行第五列,再缩至手机单列,稳定键仍可作为右侧详情的读取依据。颜色和符号显示属于偏好,保存时可以记录布尔值;但已落针数据、选中焦点、编辑历史和线材账目仍分开处理。当前B-02库存1.20米,用线预算为三针乘0.392米,即1.176米,剩余0.024米。开关切换前后所有这些数值完全相同,说明显示策略没有进入业务写入链路。

让图例参与工程验收
可以建立一组确定的检查输入:同一份五针图谱在开启和关闭符号时,计数都应为1、3、1;选中格的行列都是5、5;符号表中三个编码必须一一对应,缺失或重复时拒绝发布该套图例。再把绣布背景改为灰白、将画面转为低饱和度,观察空心圆、菱形与三角是否仍能区分。对文字朗读,还应查看句子是否同时包含坐标和颜色,而不是只有单个符号名称。
还要关注最小视图状态的同步顺序。编辑历史执行撤销或重做之后,应先获取新的针位快照,再一次性刷新格子投影、线色数量和选中项说明。假如符号图例先更新而格子仍引用旧快照,用户可能短暂看到B图例是两针、画面却有三个菱形。把这些派生值收敛在同一次状态提交中,能减少此类视觉闪烁。遇到格子内容不合法时,最好保留当前焦点并在邻近位置给出可理解的提示,不要把错误输入悄悄解释为一针A。
性能方面,六十四个单元使用固定键有利于局部更新,符号层是轻量文本,通常无须引入额外图片资源;真正需要注意的是每次按钮点击是否重建所有业务对象。编辑服务应只产生新的针位快照,UI再做单向投影。通过这层分离,十字绣图谱从依靠颜色识别变成颜色、形状、文字三种信息并列的界面,也为下一篇成品检查与统计归档提供了可复用的图例契约。
更多推荐


所有评论(0)