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

比较之前先固定产品目标
场景是便笺中嵌入一个紧凑的状态标签。需要的视觉结果很具体:文字“状态:”之后出现绿色圆角标签,再继续普通说明。若只需要文字颜色,前两篇的文本样式已经足够;只有当标签确实需要独立尺寸、背景和布局时,才有理由选择内嵌构建内容。不能因为接口更复杂,就自动认为它更适合所有文本。
本篇仍使用 NoteTypeset 和 API 12。新增的是实验控制器 experiment,不把标签写入前面三篇的正式文本内容。页面只用于比较混排行为,之后第五、六篇会继续使用文本跨度作为正式便笺契约。这样的隔离让能力评估能够完整发生,也避免在尚未设计数据协议时,把不可回读对象混进可导出的正文。
比较标准不只看页面好不好看,还要看能否由控制器读回、能否保留业务语义、复制之后怎样呈现,以及一个删除动作处理多大范围。若产品需要长期保存“待确认”这个状态,业务模型必须存在对应字段。屏幕上的绿色标签只能作为表达,不能成为该状态唯一的存储位置。
在 API 12 版本参考中,BuilderSpan 作为占位对象参与富文本布局,长度按一个占位单元处理。标签里虽然显示三个汉字,它们不是三个可在 RichEditor 中分别选择的文本跨度。这个区别决定了为什么不能用标签文字的字符串长度推进文档光标,也不能把对象内部的文本直接纳入普通字符格式命令。
两种插入方案各自保留什么
如果使用 addTextSpan 插入“待确认”,控制器管理的是文本与样式,字符级选择和样式更新符合前两篇建立的规则。它适合需要被读出、搜索、复制和继续编辑的便笺内容。代价是不能把任意复杂组件结构都当成普通文本样式;需要完整布局容器时,应先检查文本本身是否足够表达目标。
BuilderSpan 接收构建函数,可以在文档流中形成一个明确的可视对象。本文构建一个 Row,内部是 Text,外部设置宽高、背景与圆角。它在页面里看起来和文本相邻,但内部组件属于自己的布局树。外层编辑器负责安放这个对象,业务代码不能由外观相邻推导出它拥有普通文本的全部编辑语义。
这个版本的官方参考明确限制通过 getSpans 等接口取得 BuilderSpan 信息,也不支持把它当作普通文本复制与粘贴。于是“读取所有跨度,再逐个拼成完整便笺”并不是混排文档的完整导出方案。即使数组读取没有报错,也可能遗漏标签所表达的信息。接口成功返回与业务数据完整,是需要分别检查的两件事。
当前实验关闭复制相关操作,避免让一个未定义降级策略的标签进入剪贴板流程。正式产品若需要复制,应先规定是输出纯文字“待确认”、一个可识别占位标记,还是另一种富媒体协议,再选择对应实现。不能先承诺无损复制,再靠用户看到内容少了一块来发现边界。

图 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 插入另一个纯文本实验,再通过跨度查询理解两种表示的差别。比较时保留同一段前后文,不把字体、宽度和内容同时变化,否则很难判断差异究竟来自对象类型还是样式设置。当前图示只列出接口能够支持的行为,不把未实现的能力勾成通过。

图 3:普通文本便于字符编辑与跨度读取;BuilderSpan 提供自定义布局占位,但不能由显示效果推导出完整的文本回读能力。
无障碍与视觉表现也需要单独处理。一个绿色圆角块让视觉用户看到状态,但并不等于所有辅助技术都能获得相同语义。生产版本应给对象清楚的可访问描述,并在目标版本设备上验证朗读与焦点顺序。不要把“有一个 Text 子组件”当作已经完成完整可访问性验收的证据。
布局截图还不能证明序列化行为。即使标签在两种宽度下都正常显示,如果业务要求退出后恢复原标签,仍需持久化它的类型、标识、显示文字及必要参数,再通过明确构建器重建。把组件对象直接塞进通用数据结构,既不能提供稳定格式,也会把页面生命周期带入文档模型。
删除对象以后,业务状态是否也被删除,需要由产品明确回答。如果标签只是某个外部状态的镜像,删掉页面占位不应该自动把外部记录改成已完成;如果标签本身就是文档内容,删除应更新对应的文档模型。两种语义都不能由编辑器的删除事件替业务选择,否则视觉操作会悄悄变成外部数据变更。
内嵌组件也不应长期持有已经销毁页面的回调。当前构建函数只读取本次传入的宽度,标签没有异步工作,页面关闭后不会继续写界面。未来若添加动态状态,应该让业务数据的订阅与当前页面寿命对应,并区分暂停展示与销毁对象;不能因为标签被放进文档流,就忽略它仍然是一个有上下文的界面组件。
图中的占位长度是文档定位概念,尺寸是逻辑布局概念,两者没有固定比例。宽版标签可能多占一行,仍然不应变成两个业务对象。若日志把两者都写成length,排查时很容易把布局变化当成内容增减,因此工程字段应明确使用占位位置、宽度和高度等名称。
正式便笺保留可解释的内容契约
经过比较,本系列正式便笺仍选择文本跨度作为输入范围,BuilderSpan 留在独立实验区。这是由后续纯文本快照目标决定的取舍:先保证可读回的文字与换行完整,再评估混排数据格式。它不否定内嵌组件的价值,而是把显示能力和文档保存能力分别设计,避免一个功能悄悄改变另一个功能的正确性前提。
若真实需求只是给状态加底色,也可以继续评估文本样式、外围组件或只读预览。不同方案应该围绕用户是否要在段落中编辑、是否要复制以及是否要跨页面保存作比较,不应只围绕一张效果图决定。编辑器中的视觉一致性有价值,数据语义的一致性同样需要成本。
第五篇将回到正式文本便笺,为选择内容增加自定义菜单。菜单与底部按钮会调用同一个格式命令,避免同一操作因入口不同而产生不同校验。第四篇留下的边界仍然生效:选择菜单不试图把实验标签当成三个普通汉字,也不假装当前文本导出器已经理解任意内嵌组件。
官方资料
更多推荐

所有评论(0)