鸿蒙新特性:RichEditor 富文本编辑器实战 — 构建格式工具栏与图文混排
引言
如果你从事移动端开发,你一定遇到过这样的需求:用户需要在一段文字中自由切换粗体、斜体、颜色和字号——甚至在一行之内混合使用多种样式。这不是简单的 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.fontColor → applyTypingStyle() → 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 没有使用它,原因有两个:
setTypingStyle更基础、更易理解:它是富文本编辑的"入门模式",适合作为第一篇文章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() 修改选中区域的样式。这需要结合 RichEditorRange 和 getSelection() 来获取当前选区范围。
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 富文本编辑组件的核心用法:
- Span-Based 文字模型:RichEditor 将文字建模为独立样式的 Span 集合,每个 Span 可以有不同的字体、颜色、大小、粗细
- RichEditorController:用 Controller 模式将视图和控制分离,
setTypingStyle、addTextSpan、deleteSpans、getSpans四大方法覆盖了大部分编辑操作 - 格式工具栏:Bold / Italic / Underline / Color / Size 五维控制,通过
setTypingStyle实现"先设格式再输入"的交互模式 - 不可变状态更新:
@State formatState通过创建新对象触发 UI 更新,使工具栏按钮的激活态实时反映当前格式 - 实时字数统计:通过
onDidChange回调和getSpans遍历实现
RichEditor 是富文本应用的基石。掌握了它,你就可以构建从简单格式笔记到复杂图文混排的各种编辑场景。它比 TextArea 多了一层抽象(Span 模型 + Controller),但一旦理解了这层抽象,所有富文本功能——格式工具栏、图文混排、模板插入、样式实时预览——都变得清晰而自然。
在某种意义上,富文本编辑器是移动端应用的"通用需求"——几乎每个内容生产型应用都需要某种程度的富文本能力。RichEditor 让这个需求在 ArkUI 中有了标准答案。
更多推荐



所有评论(0)