引言

如果你从事移动端开发,你一定遇到过这样的需求:用户需要在一段文字中自由切换粗体、斜体、颜色和字号——甚至在一行之内混合使用多种样式。这不是简单的 TextArea 能解决的。TextArea 是纯文本输入:一个组件、一种字体、一种颜色、一个尺寸。要支持富文本,你需要 RichEditor。

RichEditor 是 HarmonyOS NEXT ArkUI 提供的富文本编辑组件,支持跨 span 的混合样式、图文混排、以及通过 Controller 进行精细的格式控制。它比 TextArea 复杂,但能力强得多——从笔记应用的格式化工具栏,到社交动态的图文编辑,再到文档应用的字号段落控制,RichEditor 都能胜任。

本文将通过构建一个完整的"富文本编辑器"Demo,深入讲解 RichEditor 的核心概念:Span 模型、RichEditorController、setTypingStyle 格式控制、addTextSpan 内容插入、以及格式工具栏的交互设计。

读完本文你将能够:

  • 理解 RichEditor 的 Span 风格模型
  • 使用 RichEditorController 控制文本样式和内容
  • 构建 Bold/Italic/Underline/Color/Size 五维格式工具栏
  • 使用 setTypingStyle 实现"先设置格式,后输入文字"的交互模式
  • 通过 addTextSpan 插入预格式化模板文本
  • 理解 RichEditor 与 TextArea 的本质区别

纯文本 vs 富文本:TextArea 的局限

在讨论 RichEditor 之前,先明确一个问题:为什么需要富文本编辑器?

TextArea(多行文本输入框)处理的是"一段文字一个样式"的场景。你设置一个字体颜色,整个 TextArea 都是这个颜色。如果用户在填写反馈表单时输入"我觉得很好",TextArea 无法让"很好"两个字变粗——因为 TextArea 的样式是全局属性,作用于整个组件的所有文字。

RichEditor 的核心差异在于它的文字模型:不是"一大段文字",而是"一系列 Span 的集合"

一个 Span(文本段)就是一段拥有独立样式的文字。RichEditor 中,用户可以:

  • 在输入前切换样式(“接下来的文字用红色大号”)
  • 选中一段文字后修改样式(“把这几个字改成粗体”)
  • 插入一个图片 Span(“在这里贴一张图”)
  • 不同类型的 Span 共存于同一个编辑器中

这种 Span-Based 的模型是现代富文本编辑器的基础架构——iOS 的 NSAttributedString、Android 的 SpannableStringBuilder、Web 的 contenteditable 都是基于类似的思考。

核心概念:RichEditorController

RichEditor 的操作不通过组件属性(.fontColor() 之类),而是通过一个独立的 Controller 对象:

controller: RichEditorController = new RichEditorController();

RichEditor({ controller: this.controller })

RichEditorController 是编辑器内容的"总控"。所有格式化操作、内容插入、删除操作都通过它完成。这种设计将"视图"(RichEditor)和"控制"(Controller)分离——RichEditor 负责渲染,Controller 负责命令。

本文 Demo 使用了 Controller 的四个核心方法:

方法 功能 调用时机
setTypingStyle(style) 设置"接下来输入的文字"的样式 点击格式按钮后
getTypingStyle() 获取当前输入样式 需要了解当前格式状态时
addTextSpan(content, options) 插入一段带样式的文字 插入模板文本
deleteSpans(range?) 删除指定范围或全部 spans 清空编辑器
getSpans(range?) 获取指定范围或全部 spans 统计字数

其中 setTypingStyle 是最重要的方法——它是格式工具栏的基石。

格式工具栏设计:五维样式控制

本文 Demo 的格式工具栏提供了五个维度的样式控制:

1. 粗体(Bold)

toggleBold(): void {
  this.formatState.bold = !this.formatState.bold;
  this.formatState = new FormatState(); // 不可变更新触发 @State
  this.applyTypingStyle();
}

用户点击 B 按钮后,formatState.bold 翻转,然后 applyTypingStyle() 将新格式应用到 RichEditor:

applyTypingStyle(): void {
  let weight = this.formatState.bold ? FontWeight.Bold : FontWeight.Normal;
  let fStyle = this.formatState.italic ? FontStyle.Italic : FontStyle.Normal;

  let style: RichEditorTextStyle = {
    fontWeight: weight,
    fontStyle: fStyle,
    fontColor: this.formatState.fontColor,
    fontSize: this.formatState.fontSize
  };

  this.controller.setTypingStyle(style);
}

RichEditorTextStyle 是一个配置对象,每个属性都是可选的——只传需要设置的属性即可。本文将五个属性全部传递,确保每次切换时,之前设置的样式不会意外丢失。

2. 斜体(Italic)

斜体的实现与粗体完全一致,只是修改的是 fontStyle 字段:

fStyle = this.formatState.italic ? FontStyle.Italic : FontStyle.Normal;

3. 下划线(Underline)

下划线使用了 decoration 属性而非 fontStyle

let dec: DecorationStyleInterface | undefined = undefined;
if (this.formatState.underline) {
  dec = { type: TextDecorationType.Underline, color: this.formatState.fontColor };
}
// ...
style.decoration = dec;

DecorationStyleInterface 支持 Underline(下划线)、LineThrough(删除线)、Overline(上划线)三种类型。这里设置为当前字体颜色,确保下划线与文字颜色一致。

4. 文字颜色

颜色选择器使用 6 个圆形色块直观展示可选颜色:

private colorOptions: string[] = ['#1a1a2e', '#1677FF', '#D53F8C', '#38A169', '#ED8936', '#722ED1'];

每个色块显示自己的颜色值,被选中时添加深色粗边框:

.border({
  width: this.formatState.fontColor === color ? 2.5 : 1.5,
  color: this.formatState.fontColor === color ? '#1a1a2e' : '#DDDDDD'
})

点击色块 → selectColor(color) → 更新 formatState.fontColorapplyTypingStyle()setTypingStyle()

5. 字号

字号选择器提供了四个预设值:13px(小字)、15px(正文)、18px(大号)、22px(标题):

private sizeOptions: number[] = [13, 15, 18, 22];

选中态使用蓝色圆角按钮样式,与颜色选择器的选中态区分开:

.backgroundColor(this.formatState.fontSize === size ? '#1677FF' : '#F5F5F5')

在这里插入图片描述
在这里插入图片描述

格式工具栏的交互模式:“先调格式,再输入”

这可能是 RichEditor 最容易让初学者困惑的设计:setTypingStyle() 改变的是接下来输入的文字的样式,而不是已经输入的文字。

打个比方:RichEditor 就像一支荧光笔。你可以按 B 按钮把笔调成粗头,然后画出来的字就是粗的;按颜色按钮把笔换成红色,然后画出来的字就是红的。但你没法用这支笔去"染"已经画好的字——要修改已输入文字的样式,你需要选中它再用 updateSpanStyle()

本文 Demo 聚焦于"先调格式再输入"的交互模式:用户在工具栏中设置好样式组合(如 Bold + Blue + 18px),然后直接在编辑器中输入,新文字自动带有这些样式。这种模式更符合"写作"的心智模型——先想好这里要重点强调,调整格式,再写内容。

工具栏底部有一个实时格式状态显示:

Text('当前格式: ' + (this.formatState.bold ? 'B ' : '') +
     (this.formatState.italic ? 'I ' : '') +
     (this.formatState.underline ? 'U ' : '') +
     this.formatState.fontSize.toString() + 'px')

用户可以随时看到当前处于什么格式状态——粗体开了吗?斜体开了吗?字号是多少?这些信息对于"先设格式再输入"的模式至关重要。

为什么不用 updateSpanStyle?

updateSpanStyle() 用于修改已选中文字的样式——这是一个更高级的交互模式,类似于桌面 Word 处理器的"选中→加粗"。本文 Demo 没有使用它,原因有两个:

  1. setTypingStyle 更基础、更易理解:它是富文本编辑的"入门模式",适合作为第一篇文章
  2. updateSpanStyle 需要处理选区逻辑:用户需要先在编辑器中选中一段文字(手指长按拖动选择),然后点击格式按钮。这个交互对于移动端的触摸操作来说比较蛋疼,且 API 的行为边界需要在不同版本间验证

对于实际应用,"先设格式再输入"的模式反而更符合移动端的使用习惯——用户在手机上不太可能像在电脑上那样精确选中一段文字。

插入模板内容:addTextSpan

除了手动输入,Demo 还提供了"插入模板"按钮,一键生成带格式的标题和正文:

insertTemplate(): void {
  // 插入标题(粗体、大号、深色)
  this.controller.addTextSpan('📝 标题\n', {
    style: {
      fontWeight: FontWeight.Bold,
      fontSize: 22,
      fontColor: '#1a1a2e'
    }
  });

  // 插入正文(常规、灰色)
  this.controller.addTextSpan('这是一段正文内容,你可以继续编辑。', {
    style: {
      fontSize: 15,
      fontColor: '#444455'
    }
  });
}

addTextSpan 接收两个参数:

  • content: ResourceStr — 要插入的文字内容
  • options?: RichEditorTextSpanOptions — 可选的样式配置

RichEditorTextSpanOptions 包含两个字段:

  • offset?: number — 插入位置(不传则插入到末尾)
  • style?: RichEditorTextStyle — 该 span 的样式

注意:addTextSpan 可以不在 setTypingStyle 之后调用——它自己携带样式。而用户在编辑器中直接键盘输入的样式,则由最后一次 setTypingStyle 决定。两者互不干扰。

字数统计:getSpans

Demo 底部显示实时字数统计——这不是通过监听键盘事件,而是通过 RichEditor 的 onDidChange 回调和 getSpans 方法:

RichEditor({ controller: this.controller })
  .onDidChange(() => {
    this.updateCharCount();
  })
updateCharCount(): void {
  let spans = this.controller.getSpans();
  let count = 0;
  for (let i = 0; i < spans.length; i++) {
    let span = spans[i];
    if (span !== undefined && span !== null) {
      let textSpan = span as RichEditorTextSpanResult;
      if (textSpan !== null && textSpan.value !== undefined) {
        count += (textSpan.value as string).length;
      }
    }
  }
  this.charCount = count;
}

getSpans() 返回当前编辑器中所有的 Span 结果数组。遍历数组,取出每个 Span 的文字内容,累加长度,即得到总字数。

这里涉及 ArkTS 严格模式下的类型处理:getSpans() 返回的是 Array<RichEditorImageSpanResult | RichEditorTextSpanResult>,需要先使用 as 断言为 RichEditorTextSpanResult 才能访问 value 属性。

清空编辑器:deleteSpans

清空按钮使用 deleteSpans() 无参调用——删除所有 spans:

clearAll(): void {
  this.controller.deleteSpans();
  this.updateCharCount();
}

deleteSpans 也可以接受一个 RichEditorRange 参数来删除指定范围。无参调用等同于删除全部。

完整页面结构

Column(根容器)
├── Header(深色标题栏:"富文本编辑器" + RichEditor 标签)
├── 格式工具栏
│   ├── Row 1:B / I / U 按钮 + 分隔线 + 6 个颜色色块
│   └── Row 2:字号选择(13/15/18/22) + 当前格式状态文字
├── RichEditor(主要编辑区域,占据剩余空间)
│   .onDidChange → 更新字数统计
└── Bottom Bar
    ├── 插入模板按钮(主色文字)
    ├── 清空内容按钮(红色文字)
    └── 字数统计(灰色)

核心代码解析

formatState 的不可变更新

在 ArkTS 中,@State 装饰的变量通过引用变化触发 UI 重新渲染。直接修改 this.formatState.bold = true 不会触发更新,因为引用没变。正确做法是创建新对象:

toggleBold(): void {
  this.formatState.bold = !this.formatState.bold;
  let newState: FormatState = new FormatState();
  newState.bold = this.formatState.bold;
  newState.italic = this.formatState.italic;
  // ... 复制所有属性
  this.formatState = newState;
  this.applyTypingStyle();
}

每个 toggle/select 方法都遵循这个模式:(1)修改属性(2)创建新对象复制所有属性(3)赋值 this.formatState = newState(4)调用 applyTypingStyle

虽然这个样板代码有些啰嗦,但它是 ArkTS 响应式系统的核心契约——只有当 @State 变量的引用发生变化时,框架才会通知所有依赖该变量的组件重新渲染。

格式按钮 Builder

格式按钮(B/I/U)封装为一个 @Builder 方法,提高代码复用性:

@Builder
formatButton(label: string, tooltip: string, active: boolean, onClick: () => void) {
  Text(label)
    .fontColor(active ? '#FFFFFF' : '#444455')
    .fontWeight(active ? FontWeight.Bold : FontWeight.Normal)
    .fontStyle(label === 'I' ? FontStyle.Italic : FontStyle.Normal)
    .decoration({ type: label === 'U' ? TextDecorationType.Underline : TextDecorationType.None })
    .textAlign(TextAlign.Center)
    .width(30).height(30)
    .borderRadius(6)
    .backgroundColor(active ? '#1677FF' : '#F5F5F5')
    .onClick(onClick)
}

每个按钮的视觉样式反映了它的语义:B 按钮用粗体显示,I 按钮用斜体显示,U 按钮显示下划线。激活态统一使用蓝底白字,非激活态使用灰底暗字。这种"所见即所得"的按钮样式让用户无需猜测每个按钮的含义。

RichEditor vs TextArea 对比

维度 TextArea RichEditor
文字模型 单一整体 Span 集合
样式作用域 全局统一 每个 Span 独立
格式化方式 属性设置 .fontColor() Controller 方法 setTypingStyle()
图片插入 不支持 支持 addImageSpan()
内容操作 placeholderText + text addTextSpan + deleteSpans + getSpans
适用场景 表单、评论、搜索 笔记、编辑器、图文动态
API 复杂度 低(属性式) 高(Controller 式)

RichEditor 不是 TextArea 的"升级版"——它们是解决不同问题层面的工具。大部分场景用 TextArea 就够了,只有当你需要混合样式时,才需要请出 RichEditor。

进阶方向

本文 Demo 展示了 RichEditor 的基础能力:格式工具栏和模板插入。在此基础上可以延伸出更强大的功能:

1. 选中后修改样式(updateSpanStyle)

用户可以长按选中一段文字,然后点击格式按钮,通过 updateSpanStyle() 修改选中区域的样式。这需要结合 RichEditorRangegetSelection() 来获取当前选区范围。

2. 图文混排(addImageSpan)

addImageSpan(value: PixelMap | ResourceStr, options?) 可以在文字中间插入图片。对于笔记应用来说,这是和文字编辑一样重要的需求。

3. 保存与恢复(StyledString)

RichEditor 支持通过 StyledString API 将编辑内容序列化为可存储的字符串格式,下次打开编辑器时恢复。结合 @ohos.data.preferences(本系列第 8 篇文章已讲解)可实现笔记的持久化存储。

4. 自定义 Builder Span

API 12+ 支持在 RichEditor 中嵌入 @Builder 方法渲染的自定义组件作为 Span——这意味着你可以在文字中间插入任意 ArkUI 组件,而不仅仅是文字和图片。

总结

本文通过构建一个完整的"富文本编辑器",深入讲解了 HarmonyOS NEXT ArkUI 中 RichEditor 富文本编辑组件的核心用法:

  1. Span-Based 文字模型:RichEditor 将文字建模为独立样式的 Span 集合,每个 Span 可以有不同的字体、颜色、大小、粗细
  2. RichEditorController:用 Controller 模式将视图和控制分离,setTypingStyleaddTextSpandeleteSpansgetSpans 四大方法覆盖了大部分编辑操作
  3. 格式工具栏:Bold / Italic / Underline / Color / Size 五维控制,通过 setTypingStyle 实现"先设格式再输入"的交互模式
  4. 不可变状态更新@State formatState 通过创建新对象触发 UI 更新,使工具栏按钮的激活态实时反映当前格式
  5. 实时字数统计:通过 onDidChange 回调和 getSpans 遍历实现

RichEditor 是富文本应用的基石。掌握了它,你就可以构建从简单格式笔记到复杂图文混排的各种编辑场景。它比 TextArea 多了一层抽象(Span 模型 + Controller),但一旦理解了这层抽象,所有富文本功能——格式工具栏、图文混排、模板插入、样式实时预览——都变得清晰而自然。

在某种意义上,富文本编辑器是移动端应用的"通用需求"——几乎每个内容生产型应用都需要某种程度的富文本能力。RichEditor 让这个需求在 ArkUI 中有了标准答案。


Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐