《海浪角》的八乘八针格已经有落针、水平镜像、用线预算和缩放定位四层能力。把编辑页摆到更宽的平板容器时,一个细节比“能分成两栏”更重要:用户在图谱上选中了第五行第五列,切换到窄布局查看批次信息,再回到宽布局,右侧详情应当指向同一个格子。倘若把选中状态分别写在左右两个视图组件里,一次重排就可能让高亮框还在第五格,批次详情却指向上一格。

本节继续采用既有业务记录:第二行第二、三列是湖蓝B,第三行第四列是珊瑚橙C,第四行第四列是乳白A,完成四针、总格数六十四,完成率6.25%。第五行第五列目前为空格,选中它不会修改针数。用线预算依旧按照每针0.35米及12%余量计算,B的需求0.784米;B-01库存0.60米不足,选用库存1.20米的B-02批次。所有这些都是同一份图谱数据,而不是为平板另外准备一份副本。

平板图谱分栏与选中状态

分栏变动不应该成为业务变更

单列页面往往按“画布、图例、库存详情”的顺序从上向下排列,适合手持时逐段阅读;容器宽度较充足时,可以把画布放左侧、选中格及当前线色批次放右侧,减少用户反复滚动。这里采用内容区域宽度720vp作为本工程分栏阈值:420vp的容器展示单列,840vp展示双列。720不是某款设备的固有属性,而是根据画布最小可读尺寸、详情表格列宽与间距相加得到的设计值。

为了避免误判,阈值应作用在图谱工作区,而不是应用窗口的全部宽度。平板多窗口、悬浮面板或侧栏展开后,应用窗口够宽但真正用于编辑的容器可能已经缩小。HarmonyOS的ContainerReader从API 26提供容器尺寸感知,恰好用于把布局判断放回这块实际内容区域。容器宽度达到阈值时切换Row布局,低于阈值时切换Column布局;业务状态始终由页面父级维护。

单列与双列并非两套编辑器。两种布局调用同一gridPane、detailPane构建逻辑,共享selectedKey和selectedLotId。即使条件渲染会重建视觉子树,选中格键与批次选择仍保留在父级状态中。换句话说,布局是内容的排布方式,不是针位的拥有者。

先固定格子与选中键的数据契约

之前的坐标反算函数统一生成从1开始的行列,现在继续把行列组合为稳定的row-col键。模型存储颜色记录时使用相同键;没有颜色记录的格子依旧可选中。这样可以明确区分“当前聚焦在哪个格子”和“这个格子已经绣了什么颜色”。如果将二者混在同一个数组索引里,空格就会失去可选择性,镜像或过滤展示也会使索引失效。

export interface StitchCell {
  row: number;
  col: number;
  colorId: string;
}
export interface ThreadLot {
  lotId: string;
  colorId: string;
  stockMeters: number;
}
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' }
];
export const LOTS: ThreadLot[] = [
  { lotId: 'A-01', colorId: 'A', stockMeters: 0.90 },
  { lotId: 'B-01', colorId: 'B', stockMeters: 0.60 },
  { lotId: 'B-02', colorId: 'B', stockMeters: 1.20 },
  { lotId: 'C-01', colorId: 'C', stockMeters: 0.80 }
];
export function cellKey(row: number, col: number): string {
  return `${row}-${col}`;
}
export function colorAt(row: number, col: number): string {
  const item: StitchCell | undefined = STITCHES.find(
    (cell: StitchCell) => cell.row === row && cell.col === col
  );
  return item ? item.colorId : '';
}

这段契约并没有重新定义用线规则:A、B、C针数仍是1、2、1。库存字段按颜色批次存储,选定B-02只是当前采购与制作选项,并不会把B-01余量加上去。在UI层使用selectedLotId保存选择结果,在提交备线清单时再核对批次对应颜色和库存,避免用户从湖蓝改选珊瑚橙后仍拿着旧批次继续操作。

进入页面时,如果保存的选中键落在1至8行列以外,应恢复到上次有效键或默认值;如果库存批次已下架,就清除对应选择,提醒重新确认。选中键恢复失败不该清空四针图谱,批次变化也不该自动修改原稿。把异常限定在所属状态,才不会把小问题扩散成整页数据重置。

用纯计算拆开详情数据

右侧详情不需要直接读取GridItem内部临时状态。它只依赖父级的选中格键、当前批次和模型快照。用线长度计算仍沿用前文公式:B两针乘0.35米再乘1.12,得到0.784米;库存B-02为1.20米,差值为0.416米。第五行第五列为空,但旁侧可以保留最近确认的线色批次,供下一次明确落针动作使用。

export interface SelectionDetail {
  key: string;
  stitchedColor: string;
  selectedLot: string;
  requiredMeters: number;
  stockMeters: number;
  stockEnough: boolean;
}
export function selectionDetail(key: string, lotId: string): SelectionDetail {
  const pair: string[] = key.split('-');
  const row: number = Number(pair[0]);
  const col: number = Number(pair[1]);
  const valid: boolean = Number.isInteger(row) && Number.isInteger(col) &&
    row >= 1 && row <= 8 && col >= 1 && col <= 8;
  const safeKey: string = valid ? key : '5-5';
  const lot: ThreadLot | undefined = LOTS.find((v: ThreadLot) => v.lotId === lotId);
  const chosen: ThreadLot = lot ? lot : LOTS[2];
  const count: number = STITCHES.filter(
    (cell: StitchCell) => cell.colorId === chosen.colorId
  ).length;
  const required: number = Math.round(count * 0.35 * 1.12 * 1000) / 1000;
  const xy: string[] = safeKey.split('-');
  return {
    key: safeKey,
    stitchedColor: colorAt(Number(xy[0]), Number(xy[1])),
    selectedLot: chosen.lotId,
    requiredMeters: required,
    stockMeters: chosen.stockMeters,
    stockEnough: chosen.stockMeters >= required
  };
}

这个投影函数把选中位置有效性、库存批次查找和用线规则集中放在一起,界面可以直接读取它的结果。生产工程中若库存批次不存在,应返回专门的缺失状态并阻止备线确认;这里把兜底分支限制在页面展示样本,不把自动选中B-02作为库存修改行为。尤其要区分“选中空格”和“清除已有针位”:前者只是焦点变化,后者才需要修改业务记录与针数。

在父页面放置容器断点

ContainerReader的size使用状态变量和!!建立双向绑定。其位置紧贴可用编辑区,可以拿到该区域的布局尺寸。父级只保存一个selectedKey,不根据布局宽窄改变它。为了压缩主体代码,gridPane与detailPane使用@Builder构建,两种排布共享同一组业务字段,切换时不重新初始化图谱。

import { ContainerReader, Size } from '@kit.ArkUI';

@Entry
@Component
struct PatternComparePage {
  @State private contentSize: Size = { width: 0, height: 0 };
  @State private selectedKey: string = '5-5';
  @State private selectedLotId: string = 'B-02';
  private indexes: number[] = new Array<number>(64).fill(0)
    .map((value: number, index: number): number => index);

  @Builder
  private gridPane() {
    Column({ space: 10 }) {
      Text('海浪角 · 8×8绣格图谱').fontSize(20)
      Grid() {
        ForEach(this.indexes,
          (index: number) => {
            GridItem() {
              Text(colorAt(Math.floor(index / 8) + 1, index % 8 + 1) || '·')
                .width('100%').height(36).textAlign(TextAlign.Center)
                .backgroundColor(this.selectedKey === cellKey(
                  Math.floor(index / 8) + 1, index % 8 + 1) ? '#D9EDFF' : '#FAFAF7')
            }
            .onClick(() => {
              this.selectedKey = cellKey(Math.floor(index / 8) + 1, index % 8 + 1);
            })
          }, (index: number) => index.toString())
      }
      .columnsTemplate('repeat(8, 1fr)').rowsTemplate('repeat(8, 1fr)')
      .height(320)
    }
  }

  @Builder
  private detailPane() {
    Column({ space: 12 }) {
      Text(`选中 ${this.selectedKey} · 尚未落针`)
      Text('已落针 4/64 · 6.25%')
      Text(`当前批次 ${this.selectedLotId}`)
      Text('B 湖蓝 · 需求0.784米 · 库存1.20米')
    }.padding(16).backgroundColor('#F1F8FF')
  }

  build() {
    ContainerReader({ size: this.contentSize!! }) {
      if (this.contentSize.width >= 720) {
        Row({ space: 18 }) {
          Column() { this.gridPane() }.layoutWeight(3)
          Column() { this.detailPane() }.layoutWeight(2)
        }
      } else {
        Column({ space: 18 }) {
          this.gridPane()
          this.detailPane()
        }
      }
    }
    .breakpointConfig({ width: [720] })
    .width('100%').height('100%')
  }
}

ContainerReader是布局读数工具,而非图谱存储工具。双向绑定字段使用@State,如果容器尺寸初始化为零,页面会暂时走单列分支,待尺寸就绪后自然切换。两侧容器通过layoutWeight分配宽度,需要给画布设置实际的最大宽度并适当留出边距;当左侧不足以放下三百二十vp图谱时,应对画布另做缩放,而不是让详情列压到不可阅读。

白色开发环境中的图谱分栏与状态绑定

校对转换路径,而不是反复点击试错

对照页的重点测试数据应是“视图变了,记录没变”:在420vp单列中选中第五行第五列,再把内容区扩到840vp,观察左侧高亮和右侧详情是否指向同一个稳定键;切回420vp,两项仍应一致。此过程不改变已绣针位集合,不影响上一节算出的B-02库存可用性。还需要处理阈值附近的尺寸抖动:容器若在719与721之间连续变化,频繁重排会干扰交互,可为业务设计适当滞回区间或减少工作区的动态边距变化。

触点和缩放也需要保留原先的单位约定。第04篇将屏幕坐标反算为画布局部坐标,再映射到行列;本篇只是把结果写到父级selectedKey,不能因为平板加了右侧详情而把触点的屏幕x坐标直接送进原函数。事件接收层必须使用左侧画布组件的局部坐标,避免额外侧栏宽度污染映射结果。

另一条检查路径是批次恢复。将当前选用批次保持为B-02,收起右栏或改变布局后,需求0.784米、库存1.20米及“足够”仍应一致;如果批次不可用,详情显示需要重选,但图谱继续保持4/64。这样错误提示能准确定位到库存,而不是误报为绘图内容损坏。

单列图谱页面与相同的选中键

让视觉尺寸与工程数据各守其位

开发过程中最容易遗留的问题,是两个分栏里分别计算同一个量。左侧若按四针显示6.25%,右侧却根据当下可见网格重新统计,就会发生镜像预览、缩放以及条件渲染后的计数偏差。应当统一由稳定的业务记录生成统计,再将结果投影到界面,而非从已绘制组件倒查真实针位。

选中框、滚动位置、缩放倍数属于视图层;针位、颜色、批次需求属于业务层;容器宽度只决定视图的排布方式。将三类状态独立管理后,平板上的双栏体验不需要复制一套编辑器,也为下一篇进一步加入草稿恢复与跨页面编辑打下了清楚的数据基础。

相关技术参考:华为开发者:ContainerReader容器断点、华为开发者:创建网格、华为开发者:@Watch状态变化通知。

Logo

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

更多推荐