“把这一小段加粗”听起来像一个按钮功能,真正决定结果的却是两个数字。它们究竟是屏幕上的字序、跨度内部的位置,还是整个编辑文档中的范围?如果这些坐标被混用,按钮可能正确改变颜色,却作用到相邻文字。本篇沿用排版便笺,把样式操作压缩成一条可解释的路径:从控制器读取选区,验证范围,再更新现有跨度的样式。

选区坐标与局部样式的技术封面

三种位置不要共用一个名字

第一篇的 NoteTypeset 工程已经提供 common/NoteSeed.ets,它建立标题和正文。本篇保留该文件与模板入口,只替换 pages/Index.ets,编译基线仍为 API 12,无需新增权限。初始文本依然是“本周记录”换行后接正文,因此读者可以把当前结果与前一篇的跨度查询互相对照。

文档范围描述内容在整个编辑器中的位置;跨度范围描述某个排版片段覆盖的全局区间;跨度内偏移则描述本次返回的有效内容在这个片段内部的位置。它们都可能由两个数字组成,但使用对象不同。把选区起点直接传给某个跨度字符串的截取函数,会在选区不是从文档开头开始时暴露错误。

当前固定样例只含基本汉字、标点和换行。标题加换行占据前五个位置,所以恢复样例后选择区间五到九,对应正文中的“完成草稿”。这组数字只服务于这个固定样例,不是根据标题字体大小算出来的,也不是通用的选词算法。用户修改标题后,原有五到九不再具有同样的业务含义。

可见字符数量也不能直接替代编辑位置。一个表情可能涉及多个编码单元,一个带组合符号的字形也可能包含不止一个码点;混排中的占位内容还有自己的长度规则。因此本篇不根据屏幕上看到几个字符计算实际选区,实际修改始终使用控制器返回的范围。示范按钮使用固定位置之前,会先恢复那份已知文本。

选区样式页面的完整竖屏效果

图 2:初始页面尚无选区;恢复并选词和强调选区是两个独立操作,底部提示不会代替控制器中的真实选择。

跨度内偏移还决定读取时应该截取哪部分字符串。查询范围只穿过某个跨度中间时,返回对象可以包含这个跨度的内容,同时通过内部偏移标明有效片段。直接把整个 value 拼入结果,可能把选择之外的文字一起带走。第六篇会按这一边界生成纯文本快照;现在先把三个坐标的名称区分清楚,避免同一个变量在不同函数里变换含义。

样式更新的入口必须先收紧

局部格式命令采用 updateSpanStyle,不先删除文字,也不重新添加一份带颜色的副本。删除再插入可能影响光标、选区、段落和后续输入位置,所付出的代价远超过改变两个样式属性。对已经存在的内容,只修改命令真正负责的样式字段,才能保留其他格式设置。

这一命令不做“第二次点击恢复普通”的猜测。选区可能同时覆盖粗体与普通文本,仅检查第一个跨度并取反,不能代表整个选择的状态。本篇把按钮定义为明确的“应用强调”:统一加粗并设置棕色。将来若需要切换式工具栏,应先给混合状态独立的表示,再决定点击后进入哪一种目标格式。

范围检查也不交给接口的默认行为。官方参考指出,部分无效范围可能被调整成覆盖更大内容的范围。对于用户请求的局部编辑,这种宽容不一定符合产品预期。业务入口先拒绝负起点、非整数以及没有正长度的范围,宁可提示重新选择,也不把一个失效选择扩展为整篇样式修改。

代码在点击当时读取 getSelection,而不是拿上一次 onSelect 回调留下的数字直接执行。用户可能在选择后继续输入、移动光标或者恢复样例,缓存坐标与当前文档之间就不再有自动保证。显示层可以保留选区说明,但真正修改前仍重新读取。状态文本是提示,不是编辑命令的授权凭证。

选择区间也有方向与顺序的问题。界面手势可以从右向左拖动,但写操作需要使用组件给出的标准范围,而不是自行记下手指按下和抬起时的屏幕横坐标。跨行、不同字号和双向文字都会使屏幕坐标与逻辑位置失去简单对应关系。把定位交给编辑组件,业务代码才不需要复制一套排版命中算法。

完整页面让读取和执行相邻

下面页面保留第一篇的种子函数。恢复并选词按钮会明确替换当前内容,随后选中固定词组;普通使用时也可以直接长按选择任意文本,再应用强调。示例使用 API 12 的 FocusController 在当前帧请求焦点,然后设置选择;焦点请求失败就给出手动操作提示。没有用定时器猜测选择时机,也不在异步回调中持有旧坐标。

import { fillSeed } from '../common/NoteSeed';

@Entry
@Component
struct Index {
  private controller: RichEditorController = new RichEditorController();
  private ready: boolean = false;
  @State private status: string = '等待编辑器';
  @State private rangeText: string = '当前选区:无';

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

  private emphasize(): void {
    if (!this.ready) { return; }
    const selection = this.controller.getSelection().selection;
    const start = selection[0];
    const end = selection[1];
    if (!Number.isInteger(start) || !Number.isInteger(end) ||
      start < 0 || end <= start) {
      this.status = '请先选择有效文字';
      return;
    }
    this.controller.updateSpanStyle({
      start: start, end: end,
      textStyle: { fontWeight: FontWeight.Bold, fontColor: '#B45309' }
    });
    this.status = `已强调区间 [${start}, ${end})`;
  }

  private chooseSample(): void {
    if (!this.ready) { return; }
    fillSeed(this.controller);
    try {
      this.getUIContext().getFocusController().requestFocus('note-editor');
      this.controller.setSelection(5, 9);
      this.rangeText = '当前选区:[5, 9)';
      this.status = '已选择样例中的完成草稿';
    } catch (error) {
      this.rangeText = '当前选区:无';
      this.status = '无法取得焦点,请直接长按文字';
    }
  }

  build() {
    Column({ space: 14 }) {
      Text('选区样式').fontSize(26).fontWeight(FontWeight.Bold)
        .fontColor('#263246')
      Text('先确认范围,再修改样式').fontSize(14).fontColor('#68758A')
      RichEditor({ controller: this.controller })
        .id('note-editor')
        .width('100%').height(240).padding(16)
        .backgroundColor(Color.White).borderRadius(16)
        .onReady(() => {
          this.ready = true;
          fillSeed(this.controller);
          this.status = '长按选择一段文字';
        })
        .onSelect((value: RichEditorSelection) => {
          if (!this.ready) { return; }
          const start = value.selection[0];
          const end = value.selection[1];
          this.rangeText = end > start && start >= 0
            ? `当前选区:[${start}, ${end})` : '当前选区:无';
        })
      Row({ space: 12 }) {
        Button('恢复并选词').onClick(() => { this.chooseSample(); })
        Button('强调选区').onClick(() => { this.emphasize(); })
      }
      Text(this.rangeText).fontSize(16).fontColor('#4F5E8C')
      Text(this.status).fontSize(14).fontColor('#68758A')
      Text('样式命令不替换选中的文字').fontSize(13).fontColor('#68758A')
    }.width('100%').height('100%').padding({ left: 24, right: 24, top: 40 })
      .backgroundColor('#F6F4EF')
  }
}

onSelect 更新的是便于观察的范围文字。回调得到无效选择时,页面显示当前无选区;强调按钮则仍以点击时的读取为准。两处检查看似相近,承担的责任却不同:一处避免显示误导,一处限制写操作。不要因为已经在显示层检查过,就删掉命令入口的验证。

恢复并选词按钮的名称把替换行为写了出来。它适合帮助读者复现固定坐标,不适合悄悄放在生产格式工具栏中。若产品需要“强调当前词”,应依据当前选区或明确的分词规则定位,不能先重置整篇便笺再找到熟悉的词。测试辅助动作与正式编辑动作必须保持可辨认的语义。

样式更新可能拆分原来的跨度。例如一段普通正文中只有四个字变成粗体,编辑器需要分别表达前段、目标段和后段的格式。此时跨度数量增加并不意味着多了一份文字。第一篇提供的读取工具可以帮助观察这种变化,但验证内容完整性时应该比较连续文本,而不是强求跨度数量保持不变。

全局范围与跨度内部偏移的关系

图 3:固定样例的正文全局范围为 [5,17),选词范围为 [5,9),对应正文内部的 [0,4);三者处于不同参照系。

这里的整数检查不是为了代替组件文档,而是让命令入口在以后被其他调用方复用时仍保持明确行为。如果将来快捷键、工具面板或批量格式入口也调用同一个函数,它们不能绕过范围约束。接口能够容忍什么,与产品允许用户做什么,是两个层次的问题;防止误改比替用户猜测失效参数更容易建立信任。

用边界输入检查命令,而非只看颜色

第一组操作是不作选择就点击强调。页面应给出选择提示,不能把整篇内容全部变色。这一步专门验证空选择边界,和按钮有没有响应不是一回事。第二组使用恢复并选词,再应用强调,目标词组应变粗且呈棕色,标题与后半句保持原有定义,文档文本本身不应增删。

第三组通过手势跨过标题与正文的边界,再应用强调。命令对范围内文本统一施加两个目标属性,但不会把所有字号改成一样,因为代码没有写字号字段。这能证明更新操作是局部属性补丁,而不是拿一个新的完整样式对象覆盖所有排版。若字号意外统一,应检查命令是否增加了不必要的字段。

第四组先选择,再点击其他位置使选择消失,随后应用强调。期望结果取决于点击时控制器报告的有效范围,不能根据上次界面提示判定。若产品需要在工具栏夺取焦点后保留选区,必须设计额外的选区会话,并在内容变化时使它失效;本篇选择即时读取,不偷偷增加这层缓存。

还可以在正文中插入表情和组合字符,使用手势选中后观察控制器返回的范围,再执行同一命令。这里检查的是范围经过输入与选择后是否仍由组件维护,不要求读者先手算所有字符的编码长度。若截取选中文本用于其他业务,应使用返回的跨度内偏移,并对混排内容建立独立处理策略。

局部样式命令的红笔代码标注

图 4:写操作前校验范围;只提交加粗与颜色两个属性,避免用失效选区触发大范围更新。

为了区分视觉变化与内容变化,可以在操作前后分别记录所选文字和跨度范围。文字应保持相同,跨度切分可能变化,颜色和字重则按命令更新。三类观察不要合成一个简单的“成功”布尔值。尤其在回归检查中,颜色正确但少了一个换行,与跨度数量增加但文字完整,属于完全不同的问题。

选中范围跨越换行时,还需要观察下一行的排版是否保持原样。本篇只设置字符样式,没有修改对齐、段首缩进或段落间隔。若按钮顺手重建整段内容,换行附近的布局常常成为最先暴露问题的位置。保留现有文档并只更新选定属性,也让异常定位可以缩小到一条格式命令,而不必同时检查文本重建过程。

让样式变化留在它的责任范围内

现有文字的样式与随后输入文字的样式,是两个需要分别验证的问题。本篇更新的是当前选中范围,不承诺光标移开后所有新输入都继承棕色粗体。需要控制输入格式时,可以研究控制器的输入样式接口,并明确它与选择格式工具之间的优先关系。把两种行为混在一个按钮中,会让用户难以预测下一次输入。

同样,文字颜色不能成为内容是否重要的唯一业务标记。若便笺未来支持待办、批注或审核状态,应在业务模型中保存对应含义,再选择合适的显示方式。否则导出纯文本后颜色消失,业务状态也会随之丢失。排版表达帮助阅读,但它并不天然承担完整的数据语义。

页面没有保存控制器返回的对象到跨页单例,也不会在销毁后继续写状态。读取、验证与更新完成后,操作只留下可见样式和提示文字。这使下一篇能够在相同文档上讨论段落格式:字符范围用来找到涉及的内容,但居中与缩进最终作用到段落,不能把字符级补丁的直觉直接套过去。

焦点是示范选择能够显示的前提。编辑器尚未获得焦点时,调用设置选区不等于用户一定能看到高亮。页面为编辑区分配固定组件标识,并使用当前 UIContext 的焦点控制器;这个标识只定位当前页面节点,不是文档业务编号。设备软键盘是否出现仍遵循实际输入状态,不应拿一张初始无键盘界面推断所有交互阶段。

官方资料

Logo

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

更多推荐