同样显示“待确认”,用普通文字插入和用一块带底色的内嵌标签插入,后续能力并不相同。前者可以逐字编辑,后者可以拥有明确的布局尺寸;它们在光标、删除、查询和复制上的行为也不应该被强行统一。本篇给排版便笺增加一个独立混排实验区,比较 TextSpan 与 BuilderSpan 的责任边界,再决定哪一种内容适合进入正式文档。

BuilderSpan内嵌展示边界技术封面

比较之前先固定产品目标

场景是便笺中嵌入一个紧凑的状态标签。需要的视觉结果很具体:文字“状态:”之后出现绿色圆角标签,再继续普通说明。若只需要文字颜色,前两篇的文本样式已经足够;只有当标签确实需要独立尺寸、背景和布局时,才有理由选择内嵌构建内容。不能因为接口更复杂,就自动认为它更适合所有文本。

本篇仍使用 NoteTypeset 和 API 12。新增的是实验控制器 experiment,不把标签写入前面三篇的正式文本内容。页面只用于比较混排行为,之后第五、六篇会继续使用文本跨度作为正式便笺契约。这样的隔离让能力评估能够完整发生,也避免在尚未设计数据协议时,把不可回读对象混进可导出的正文。

比较标准不只看页面好不好看,还要看能否由控制器读回、能否保留业务语义、复制之后怎样呈现,以及一个删除动作处理多大范围。若产品需要长期保存“待确认”这个状态,业务模型必须存在对应字段。屏幕上的绿色标签只能作为表达,不能成为该状态唯一的存储位置。

在 API 12 版本参考中,BuilderSpan 作为占位对象参与富文本布局,长度按一个占位单元处理。标签里虽然显示三个汉字,它们不是三个可在 RichEditor 中分别选择的文本跨度。这个区别决定了为什么不能用标签文字的字符串长度推进文档光标,也不能把对象内部的文本直接纳入普通字符格式命令。

两种插入方案各自保留什么

如果使用 addTextSpan 插入“待确认”,控制器管理的是文本与样式,字符级选择和样式更新符合前两篇建立的规则。它适合需要被读出、搜索、复制和继续编辑的便笺内容。代价是不能把任意复杂组件结构都当成普通文本样式;需要完整布局容器时,应先检查文本本身是否足够表达目标。

BuilderSpan 接收构建函数,可以在文档流中形成一个明确的可视对象。本文构建一个 Row,内部是 Text,外部设置宽高、背景与圆角。它在页面里看起来和文本相邻,但内部组件属于自己的布局树。外层编辑器负责安放这个对象,业务代码不能由外观相邻推导出它拥有普通文本的全部编辑语义。

这个版本的官方参考明确限制通过 getSpans 等接口取得 BuilderSpan 信息,也不支持把它当作普通文本复制与粘贴。于是“读取所有跨度,再逐个拼成完整便笺”并不是混排文档的完整导出方案。即使数组读取没有报错,也可能遗漏标签所表达的信息。接口成功返回与业务数据完整,是需要分别检查的两件事。

当前实验关闭复制相关操作,避免让一个未定义降级策略的标签进入剪贴板流程。正式产品若需要复制,应先规定是输出纯文字“待确认”、一个可识别占位标记,还是另一种富媒体协议,再选择对应实现。不能先承诺无损复制,再靠用户看到内容少了一块来发现边界。

DevEco Studio中查看内嵌标签构建与插入

图 2:代码把标签加入独立 experiment 控制器;右侧默认显示宽度为 76 vp 的紧凑标签,不代表正式便笺已经支持完整混排导出。

给对象明确尺寸,再观察它进入行内布局

将 pages/Index.ets 替换为下面的实验页。本篇不需要额外图片资源,标签完全由公开 ArkUI 组件构建;也不读取网络或设备文件。此前 common/NoteSeed.ets 保留在工程中,但实验页不调用它,从命名与调用路径上区分正文恢复和混排实验。

@Entry
@Component
struct Index {
  private experiment: RichEditorController = new RichEditorController();
  private ready: boolean = false;
  @State private caption: string = '等待实验编辑区';

  aboutToDisappear(): void { this.ready = false; }

  @Builder
  badge(width: number) {
    Row() {
      Text('待确认').fontSize(14).fontColor('#22664C')
    }.width(width).height(32).justifyContent(FlexAlign.Center)
      .backgroundColor('#DCEBE2').borderRadius(8)
  }

  private loadBadge(width: number): void {
    if (!this.ready) { return; }
    this.experiment.deleteSpans();
    this.experiment.addTextSpan('状态:', {
      style: { fontSize: 18, fontColor: '#263246' }
    });
    this.experiment.addBuilderSpan(() => {
      this.badge(width);
    });
    this.experiment.addTextSpan(',继续编辑。', {
      style: { fontSize: 18, fontColor: '#263246' }
    });
    this.caption = `标签宽度:${width} vp`;
  }

  build() {
    Column({ space: 14 }) {
      Text('内嵌标签').fontSize(26).fontWeight(FontWeight.Bold)
        .fontColor('#263246')
      Text('占位长度与布局宽度分开').fontSize(14).fontColor('#68758A')
      RichEditor({ controller: this.experiment })
        .width('100%').height(210).padding(16)
        .backgroundColor(Color.White).borderRadius(16)
        .copyOptions(CopyOptions.None)
        .onReady(() => { this.ready = true; this.loadBadge(76); })
      Row({ space: 12 }) {
        Button('紧凑标签').onClick(() => { this.loadBadge(76); })
        Button('宽版标签').onClick(() => { this.loadBadge(136); })
      }
      Text(this.caption).fontSize(16).fontColor('#3F7666')
      Text('实验内容不进入正文快照').fontSize(14).fontColor('#68758A')
      Text('标签是布局对象,不是三个可编辑字符')
        .fontSize(13).fontColor('#68758A').width('100%').padding(16)
        .backgroundColor('#E8ECF4').borderRadius(12)
    }.width('100%').height('100%').padding({ left: 24, right: 24, top: 40 })
      .backgroundColor('#F6F4EF')
  }
}

标签根节点明确设置宽度与高度。没有显式外层尺寸时,构建内容会受到编辑器传入的布局约束,结果可能远大于一个短标签。对于行内对象,先约束最外层盒子,再处理内部对齐,比只给内部文字设置字号更稳定。本文用居中布局把三个字放进盒子,不在文本前后塞空格构造宽度。

两个按钮分别使用七十六与一百三十六的逻辑宽度。标签的占位长度没有因为宽版增加,变化的是它占据的页面空间。编辑区剩余宽度不足时,后面的普通文本可能被排到下一行;那是布局结果,不应被解释为文档里新增了换行字符。第三篇区分软换行与段落边界的原则,在这里仍然有效。

每次切换按钮都会重建这份短实验内容,所以用户刚输入的实验文字会被替换。页面明确定位为标签实验,不把切换当成保留全文的样式更新。如果需要在真实长文中改变某个内嵌对象尺寸,应建立对象身份和更新通道,而不是每点一次宽度按钮就清空整个编辑器。当前代码没有把测试便利伪装成生产能力。

构建函数只产生静态标签,没有在内部加入网络按钮、表单或新的焦点入口。这使尺寸、占位和读取边界能够单独观察。将交互复杂的组件塞进行内区域,会引入手势竞争、焦点与选择关系,必须逐项验证。一个能显示的组件,并不自动适合成为富文本中的可编辑对象。

用三组观察作出取舍

第一组比较紧凑与宽版标签。记录编辑区宽度、标签尺寸和后续文本的排列即可,不需要编造性能测量。关注点是对象尺寸是否受控,以及宽度变化是否只改变布局。若标签突然占满整块编辑区,先检查根布局有没有明确尺寸,而不是调整编辑器里的字符范围。

第二组把光标移动到标签附近,观察删除时的粒度。占位对象应作为一个整体参与编辑,不能按照内部显示的三个字推算三次删除。这里尤其需要区分对象内容与文档内容:内层 Text 展示三个字,不代表外层 RichEditor 可以逐字编辑这个 Text。若产品要求直接修改标签名称,应提供独立编辑入口。

第三组对照普通文本方案的读取结果。可以把同样文字用 addTextSpan 插入另一个纯文本实验,再通过跨度查询理解两种表示的差别。比较时保留同一段前后文,不把字体、宽度和内容同时变化,否则很难判断差异究竟来自对象类型还是样式设置。当前图示只列出接口能够支持的行为,不把未实现的能力勾成通过。

TextSpan与BuilderSpan能力边界对照

图 3:普通文本便于字符编辑与跨度读取;BuilderSpan 提供自定义布局占位,但不能由显示效果推导出完整的文本回读能力。

无障碍与视觉表现也需要单独处理。一个绿色圆角块让视觉用户看到状态,但并不等于所有辅助技术都能获得相同语义。生产版本应给对象清楚的可访问描述,并在目标版本设备上验证朗读与焦点顺序。不要把“有一个 Text 子组件”当作已经完成完整可访问性验收的证据。

布局截图还不能证明序列化行为。即使标签在两种宽度下都正常显示,如果业务要求退出后恢复原标签,仍需持久化它的类型、标识、显示文字及必要参数,再通过明确构建器重建。把组件对象直接塞进通用数据结构,既不能提供稳定格式,也会把页面生命周期带入文档模型。

删除对象以后,业务状态是否也被删除,需要由产品明确回答。如果标签只是某个外部状态的镜像,删掉页面占位不应该自动把外部记录改成已完成;如果标签本身就是文档内容,删除应更新对应的文档模型。两种语义都不能由编辑器的删除事件替业务选择,否则视觉操作会悄悄变成外部数据变更。

内嵌组件也不应长期持有已经销毁页面的回调。当前构建函数只读取本次传入的宽度,标签没有异步工作,页面关闭后不会继续写界面。未来若添加动态状态,应该让业务数据的订阅与当前页面寿命对应,并区分暂停展示与销毁对象;不能因为标签被放进文档流,就忽略它仍然是一个有上下文的界面组件。

图中的占位长度是文档定位概念,尺寸是逻辑布局概念,两者没有固定比例。宽版标签可能多占一行,仍然不应变成两个业务对象。若日志把两者都写成length,排查时很容易把布局变化当成内容增减,因此工程字段应明确使用占位位置、宽度和高度等名称。

正式便笺保留可解释的内容契约

经过比较,本系列正式便笺仍选择文本跨度作为输入范围,BuilderSpan 留在独立实验区。这是由后续纯文本快照目标决定的取舍:先保证可读回的文字与换行完整,再评估混排数据格式。它不否定内嵌组件的价值,而是把显示能力和文档保存能力分别设计,避免一个功能悄悄改变另一个功能的正确性前提。

若真实需求只是给状态加底色,也可以继续评估文本样式、外围组件或只读预览。不同方案应该围绕用户是否要在段落中编辑、是否要复制以及是否要跨页面保存作比较,不应只围绕一张效果图决定。编辑器中的视觉一致性有价值,数据语义的一致性同样需要成本。

第五篇将回到正式文本便笺,为选择内容增加自定义菜单。菜单与底部按钮会调用同一个格式命令,避免同一操作因入口不同而产生不同校验。第四篇留下的边界仍然生效:选择菜单不试图把实验标签当成三个普通汉字,也不假装当前文本导出器已经理解任意内嵌组件。

官方资料

Logo

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

更多推荐