同一枚活字可以被取用多次,但印板上每次出现的文字不一定需要相同大小。上一阶段,TypeFoundry已经把四行六列印板拆成二十四个稳定格位,字库有“山、海、云、风、月、光”六枚字模,第一行六格已排好,后面十八格为空。本篇把“山”和“海”再次放进第二行前两列,形成八个已占用位置,同时引入小、标准、大三个字号级别。

关键问题不是怎样把文字放大,而是避免调整第二行的“山”时,第一行的“山”也跟着变化。字模记录只保存字符身份和字形来源,字号属于这一次排版的放置属性。字模复用应增加引用关系,不应复制一份新的字库记录。如此一来,后续如果统一替换字形来源,可以从字库级别管理;若调整局部强调,则只修改某个格位的排版参数。

同字字模跨格复用与字号映射

把共享字模与位置属性拆开

初版的PrintSlot中已经有glyphId,第二阶段需要增加sizeLevel,但不能把字号写回GlyphDef,否则一枚字模被多个格位引用时会发生相互覆盖。三种显示级别约定为小24vp、标准32vp、大40vp;这属于应用内版面设计参数,不需要改变格位间距。单个字被放大时,外层仍按统一单元高度排版,文字在格位内居中,避免把其他字符挤到相邻位置。

第二行第一列引用已有的shan字模,第二行第二列引用已有的hai。第一行六格的字号保持32vp,第二行“山”选择40vp,第二行“海”选择24vp。如此已排字是八格、空格十六格,但独立字模数仍为六。应用同时展示“字模六枚、引用八处”,能够直接说明复用与复制的区别。

type SizeLevel = 'small' | 'regular' | 'large';
interface GlyphDef {
  id: string;
  text: string;
}
interface PrintSlot {
  key: string;
  row: number;
  col: number;
  glyphId: string;
  sizeLevel: SizeLevel;
}
const GLYPHS: GlyphDef[] = [
  { id: 'shan', text: '山' }, { id: 'hai', text: '海' },
  { id: 'yun', text: '云' }, { id: 'feng', text: '风' },
  { id: 'yue', text: '月' }, { id: 'guang', text: '光' }
];
function fontVp(level: SizeLevel): number {
  if (level === 'small') return 24;
  if (level === 'large') return 40;
  return 32;
}
function createBoard(): PrintSlot[] {
  const slots: PrintSlot[] = [];
  for (let row = 1; row <= 4; row++) {
    for (let col = 1; col <= 6; col++) {
      const i: number = col - 1;
      let glyphId: string = row === 1 ? GLYPHS[i].id : '';
      if (row === 2 && col === 1) glyphId = 'shan';
      if (row === 2 && col === 2) glyphId = 'hai';
      const level: SizeLevel = row === 2 && col === 1 ? 'large' :
        row === 2 && col === 2 ? 'small' : 'regular';
      slots.push({ key: `${row}:${col}`, row: row, col: col,
        glyphId: glyphId, sizeLevel: level });
    }
  }
  return slots;
}

业务数据没有因为引用两次“山”就生成两个shan对象。每个格位只有一个字模ID,版面能据此找到文字;相同ID出现在多个位置是合法情况。保存时要保留每个格位的字号级别,即使它现在为空,也可统一重置为标准字号。读取旧版记录如果缺少sizeLevel,转换时可补为regular,而不是让界面遇到空属性后自行猜测显示大小。

字号切换用不可变更新保持界面一致

字号选择通常在点击“24”“32”“40”三个按钮后触发,不能靠修改共享的字库对象。应使用稳定格位键查找目标,然后返回新数组;被替换的格位保留行列及字模ID,仅改变级别字段。这样从第二行第一列的大号“山”改为标准时,第一行第一列的“山”不受影响,已排字数量也不变化。

还需要检查三个边界:空格是否允许设置字号、无效键是否造成写入、未知字号是否进入存储。这里规定空格也能设置预选字号,但不计入已排字统计;不存在的键返回原数据;字号类型通过有限集合约束。预选字号对接下一次落字时有价值,不必为了避免空格设置而增加额外分支。

function updateSize(slots: PrintSlot[], key: string,
  level: SizeLevel): PrintSlot[] {
  if (!slots.some((slot: PrintSlot) => slot.key === key)) {
    return slots;
  }
  return slots.map((slot: PrintSlot) => slot.key === key ? {
    key: slot.key, row: slot.row, col: slot.col,
    glyphId: slot.glyphId, sizeLevel: level
  } : slot);
}
function occupied(slots: PrintSlot[]): number {
  return slots.filter((slot: PrintSlot) => slot.glyphId !== '').length;
}
function uniqueGlyphCount(slots: PrintSlot[]): number {
  const seen: Set<string> = new Set<string>();
  slots.forEach((slot: PrintSlot) => {
    if (slot.glyphId !== '') seen.add(slot.glyphId);
  });
  return seen.size;
}

更新函数使用格位身份,而不是文字内容;如果按照“山”批量寻找并修改,就把局部排版变成了字库统一设置。用于统计的occupied计算八个非空格位,uniqueGlyphCount计算六种被引用的字模,两种数量有不同含义。当用户删除第二行两格时,已排字回到六,独立字模仍为六;若只清除第一行某个字,该字是否仍被使用应根据全印板重新计算。

让ArkUI的格位与字号表达彼此独立

Grid依旧用四行六列模板,采用格位键作为ForEach的稳定标识。内部Text从字库解析字符,再从格位的sizeLevel换算字号。选中焦点使用selectedKey维护,不参与字号转换;字号控制区显示当前格位对应的级别。页面无需对每个字号建立一套网格,切换时只更新对应位置的数据并让Text重新读取。

@Entry
@Component
struct GlyphReusePage {
  @State private slots: PrintSlot[] = createBoard();
  @State private selectedKey: string = '2:1';

  private textOf(id: string): string {
    const glyph: GlyphDef | undefined = GLYPHS.find(
      (item: GlyphDef) => item.id === id);
    return glyph ? glyph.text : '';
  }
  private selectSize(level: SizeLevel): void {
    this.slots = updateSize(this.slots, this.selectedKey, level);
  }
  build() {
    Column({ space: 12 }) {
      Text('活字字盘 · 字模复用').fontSize(23)
      Text(`已排 ${occupied(this.slots)}/24 · 字模 ${uniqueGlyphCount(this.slots)} 枚`)
      Grid() {
        ForEach(this.slots, (slot: PrintSlot) => {
          GridItem() {
            Text(this.textOf(slot.glyphId))
              .fontSize(fontVp(slot.sizeLevel))
              .width('100%').height('100%')
              .textAlign(TextAlign.Center)
          }
          .backgroundColor(this.selectedKey === slot.key ? '#DDEEFE' : '#F8F1DF')
          .onClick(() => { this.selectedKey = slot.key; })
        }, (slot: PrintSlot) => slot.key)
      }
      .rowsTemplate('1fr 1fr 1fr 1fr')
      .columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr')
      .width(336).height(224)
      Row({ space: 10 }) {
        Button('24 小').onClick(() => this.selectSize('small'))
        Button('32 标准').onClick(() => this.selectSize('regular'))
        Button('40 大').onClick(() => this.selectSize('large'))
      }
    }.padding(14).width('100%')
  }
}

三个按钮只决定当前被选格位的字号,并不需要额外维护一个与格位重复的“当前字号”状态。点击第二行第一列以后,页面从该格位读取large,按钮高亮也应该从相同数据导出,才能保证换位时显示正确。界面里的尺寸用vp表达,不是字体文件的点数;它是组件的显示值,与实际印刷时采用的字号体系应通过单独的版面导出规则转换。

字模引用与字号切换的ArkTS开发环境

字号变化后的计数、可读性与兼容处理

尺寸变化只影响某一格的视觉层,不改变二十四个格位的顺序,也不会增加字模存储数量。在手机宽度较小的情况下,可以把字盘容器按比例收缩,而不是直接将字号值替换成随机的小数;否则同一字号级别在不同设备上可能得出不同业务值。对于大字号触碰边框的情形,优先调整单元高度与上下留白,并确保文字位于单元内部。

本篇的核对样本应包含两个“山”同时存在,其中第一行“山”为标准32vp、第二行“山”为大40vp;“海”也出现两次,第一行32vp、第二行24vp。四个位置引用的仍是两枚原有字模,字库总量维持六。点击字号按钮以后,应检查被选中格位以外的字号是否稳定,以及空位是否仍在原始位置。

四行六列字盘中的重复字模与不同字号

多次引用的数据核对办法

可以把验证过程拆成三个独立观察点。第一,检查位置数始终为二十四,第一行六字与第二行前两字以外仍是空位;第二,检查字库ID总数仍是六,没有因复用而增加“山副本”或“海副本”;第三,切换第二行字号以后重新读取第一行的同字记录,确认两个格位引用同一字模但拥有不同的sizeLevel。这样既覆盖结构正确性,也覆盖局部属性隔离。

为了便于后续多端适配,字号级别应持久化为small、regular、large这样的稳定键,而不是直接写入某一台设备上测得的像素宽度。读取端将键解释成当前版面的vp值,导出印刷文件时再通过对应的排版规则转换。若记录中出现未知级别,可将它恢复为标准档,并保留明确的异常记录供编辑页提示;不能让未知字段一路传入Text的fontSize导致不可预期的排版表现。

需要注意,一些字体可能缺失个别字符,或者在不同字体回退路径下产生字宽差异。当前字库只使用常见汉字,依然建议用实际Text布局的可用空间约束字号,并为后续自定义字形资源保留回退策略。重复引用是数据结构上的复用,不要求底层每一个字符绘制一定共享纹理缓存;性能评估应看页面子组件数量及状态更新范围。

华为开发者的Grid/GridItem使用说明提供网格模板与重复子项的具体规则,Text/Span文本展示可参考字号及文本内容的呈现方式,状态变化通知则为后续观察版面字段提供依据。页面采用独立格位赋值,避免由字号副作用写入字库。

这一步把字模身份与排版属性真正拆开:六枚字模能被八个位置引用,不同位置既能共享同一字符,也能保持各自字号。下一篇会继续保留这套四行六列业务数据,转向大屏分栏的字盘、字库和详情焦点协调,解决容器变宽后选中状态与页面结构分离的问题。

Logo

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

更多推荐