HarmonyOS ArkUI RichEditor 富文本编辑器:内联样式、Span 管理与格式工具栏

发布时间:2026/7/19 3:04:04

HarmonyOS ArkUI RichEditor 富文本编辑器:内联样式、Span 管理与格式工具栏
系列鸿蒙 HarmonyOS 6.1 新特性实战 · 第 40 篇RichEditor 是 ArkUI 提供的富文本编辑组件支持在用户输入的同时对不同文字片段应用独立的颜色、字号、粗体等样式。与 TextInput 不同RichEditor 的内容由多个“Span“组成每个 Span 可以有独立样式适合评论编辑器、邮件客户端、笔记应用等场景。运行效果初始状态RichEditor 加载完成初始内容已插入点击粗体、应用样式、插入文字后的富文本效果一、创建 RichEditorprivate controller: RichEditorController new RichEditorController() RichEditor({ controller: this.controller }) .width(100%) .height(200) .backgroundColor(#fff) .borderRadius(8) .border({ width: 1, color: #ccc }) .padding(12) .onReady(() { // 编辑器就绪后插入初始内容 this.controller.addTextSpan(Hello HarmonyOS, { style: { fontColor: #0066ff, fontSize: 18, fontWeight: FontWeight.Bold } }) this.controller.addTextSpan(\n普通文字内容, { style: { fontColor: #555, fontSize: 14 } }) })注意必须等onReady回调触发后才能调用 controller 的方法否则会报错。二、addTextSpan 插入文字this.controller.addTextSpan(【插入的文字】, { style: { fontColor: #e74c3c, fontSize: 14, fontWeight: FontWeight.Bold } })每次调用addTextSpan都在光标位置或末尾追加一个新 Span。三、updateSpanStyle 修改已选中文字样式this.controller.updateSpanStyle({ textStyle: { fontColor: this.selectedColor, fontSize: this.selectedSize, fontWeight: this.isBold ? FontWeight.Bold : FontWeight.Normal } })updateSpanStyle作用于当前选中的文字范围。用户选中文字后点击工具栏按钮即可改变样式这是实现格式工具栏的核心 API。四、getSpans / deleteSpans// 获取所有 Span 信息 const spans this.controller.getSpans() this.spanCount spans.length // spans 是 RichEditorSpan 数组 // 清空全部内容 this.controller.deleteSpans() // 删除指定范围可选参数 this.controller.deleteSpans({ start: 0, end: 5 })五、onSelectionChange 监听选中变化.onSelectionChange((range: RichEditorRange) { const spans this.controller.getSpans() this.spanCount spans.length // range.start / range.end 是选中范围的字符索引 })陷阱RichEditor 没有.onChange()方法监听内容变化要用.onSelectionChange()。完整代码Entry Component struct Index { private controller: RichEditorController new RichEditorController() State spanCount: number 0 State selectedColor: string #000000 State selectedSize: number 16 State isBold: boolean false State logLines: string[] [] addLog(msg: string): void { const arr this.logLines.slice(-5) arr.push(msg) this.logLines arr } applyStyle(): void { this.controller.updateSpanStyle({ textStyle: { fontColor: this.selectedColor, fontSize: this.selectedSize, fontWeight: this.isBold ? FontWeight.Bold : FontWeight.Normal } }) } build() { Column({ space: 0 }) { Text(RichEditor 富文本编辑器) .fontSize(20).fontWeight(FontWeight.Bold).fontColor(#1a1a1a) .padding({ left: 20, right: 20, top: 16, bottom: 8 }) .width(100%).backgroundColor(#f5f5f5) Scroll() { Column({ space: 16 }) { // 格式工具栏 Column({ space: 8 }) { Text(格式工具栏).fontSize(13).fontWeight(FontWeight.Bold).fontColor(#333) // 加粗 Row({ space: 8 }) { Text(加粗).fontSize(13).fontColor(#555).width(50) Button(B).fontSize(16).fontWeight(FontWeight.Bold).width(44).height(36) .backgroundColor(this.isBold ? #0066ff : #e8f0ff) .fontColor(this.isBold ? #fff : #0066ff).borderRadius(6) .onClick(() { this.isBold !this.isBold }) Text(this.isBold ? 已开启 : 未开启).fontSize(12).fontColor(#888) } .alignItems(VerticalAlign.Center) // 颜色 Row({ space: 8 }) { Text(颜色).fontSize(13).fontColor(#555).width(50) ForEach([#000000, #e74c3c, #0066ff, #27ae60, #e67e22], (c: string) { Column() .width(28).height(28).borderRadius(14).backgroundColor(c) .border({ width: this.selectedColor c ? 3 : 0, color: #333 }) .onClick(() { this.selectedColor c }) }) } .alignItems(VerticalAlign.Center) // 字号 Row({ space: 8 }) { Text(字号).fontSize(13).fontColor(#555).width(50) ForEach([12, 14, 16, 20, 24], (s: number) { Button(s.toString()).fontSize(12).width(36).height(32).borderRadius(6) .backgroundColor(this.selectedSize s ? #0066ff : #e8f0ff) .fontColor(this.selectedSize s ? #fff : #0066ff) .onClick(() { this.selectedSize s }) }) } .alignItems(VerticalAlign.Center) Button(应用选中样式).width(100%).height(40).backgroundColor(#0066ff).fontColor(#fff) .onClick(() { this.applyStyle() this.addLog(应用颜色${this.selectedColor} 字号${this.selectedSize} 加粗${this.isBold}) }) } .padding(12).backgroundColor(#fff).borderRadius(8).width(100%) .border({ width: 1, color: #e0e0e0 }).alignItems(HorizontalAlign.Start) // 编辑区 Column({ space: 8 }) { Text(编辑区选中文字后点工具栏应用格式).fontSize(13).fontWeight(FontWeight.Bold).fontColor(#333) RichEditor({ controller: this.controller }) .width(100%).height(180) .backgroundColor(#fff).borderRadius(8) .border({ width: 1, color: #ccc }).padding(12) .onReady(() { this.controller.addTextSpan(Hello HarmonyOS, { style: { fontColor: #0066ff, fontSize: 18, fontWeight: FontWeight.Bold } }) this.controller.addTextSpan(\n这里可以输入任意文字选中后点击上方工具栏修改样式。, { style: { fontColor: #555, fontSize: 14 } }) this.addLog(编辑器就绪初始内容已插入) }) .onSelectionChange((_: RichEditorRange) { this.spanCount this.controller.getSpans().length }) } .padding(12).backgroundColor(#f8f8f8).borderRadius(8).width(100%) .border({ width: 1, color: #e0e0e0 }).alignItems(HorizontalAlign.Start) // API 演示按钮 Column({ space: 8 }) { Text(RichEditor API 演示).fontSize(13).fontWeight(FontWeight.Bold).fontColor(#333) Row({ space: 8 }) { Button(插入文字).fontSize(12).height(36).layoutWeight(1) .backgroundColor(#e8f0ff).fontColor(#0066ff) .onClick(() { this.controller.addTextSpan(【插入】, { style: { fontColor: #e74c3c, fontSize: 14, fontWeight: FontWeight.Bold } }) this.addLog(addTextSpan 插入红色文字) }) Button(获取 Spans).fontSize(12).height(36).layoutWeight(1) .backgroundColor(#e8f0ff).fontColor(#0066ff) .onClick(() { this.spanCount this.controller.getSpans().length this.addLog(getSpans() 返回 ${this.spanCount} 个 span) }) Button(清空内容).fontSize(12).height(36).layoutWeight(1) .backgroundColor(#fff0f0).fontColor(#e74c3c) .onClick(() { this.controller.deleteSpans() this.spanCount 0 this.addLog(deleteSpans() 清空全部内容) }) } .width(100%) Text(Span 数量 this.spanCount.toString()).fontSize(13).fontColor(#0066ff) } .padding(12).backgroundColor(#fff).borderRadius(8).width(100%) .border({ width: 1, color: #e0e0e0 }).alignItems(HorizontalAlign.Start) // 操作日志 if (this.logLines.length 0) { Column({ space: 4 }) { Text(操作日志).fontSize(13).fontWeight(FontWeight.Bold).fontColor(#333) ForEach(this.logLines, (line: string) { Text(• line).fontSize(11).fontColor(#555).fontFamily(monospace).width(100%) }) } .padding(12).backgroundColor(#fafafa).borderRadius(8).width(100%) .border({ width: 1, color: #e0e0e0 }).alignItems(HorizontalAlign.Start) } } .padding(20).width(100%) } .layoutWeight(1) } .width(100%).height(100%).backgroundColor(#f5f5f5) } }API 速查方法说明new RichEditorController()创建控制器RichEditor({ controller })绑定控制器.onReady(() {})编辑器初始化完成回调.onSelectionChange((range) {})选中范围变化回调controller.addTextSpan(text, { style })插入文字 Spancontroller.updateSpanStyle({ textStyle })修改选中文字样式controller.getSpans()获取所有 Spancontroller.deleteSpans()清空内容controller.deleteSpans({ start, end })删除指定范围内容小结onReady 后才能操作addTextSpan等方法必须在onReady后调用没有 onChange监听内容变化用.onSelectionChange()而非.onChange()这是常见踩坑点updateSpanStyle 作用于选中范围需要用户先选中文字才有效果Span 独立样式每个 Span 可有独立颜色、字号、粗体适合表单类富文本上一篇Badge 徽标与 Chip 标签 | 下一篇第 41 篇ArkUI 布局系列

相关新闻

FastAPI OAuth2认证实现与安全实践

FastAPI OAuth2认证实现与安全实践

2026/7/19 3:04:04

1. FastAPI中的OAuth2认证机制解析在构建现代Web应用时,认证系统是保障数据安全的第一道防线。FastAPI作为高性能Python框架,提供了开箱即用的OAuth2支持。不同于传统的Session认证,OAuth2采用令牌(Token)机制,特别适合前后端分离…

及时做APP开发实战(十二)-LazyForEach懒加载优化实践

及时做APP开发实战(十二)-LazyForEach懒加载优化实践

2026/7/19 2:54:04

及时做APP开发实战(十二)-LazyForEach懒加载优化实践本文将详细介绍如何使用LazyForEach实现长列表懒加载,提升应用渲染性能。一、长列表性能问题 1.1 问题分析 【图1:ForEach vs LazyForEach】 ForEach渲染方式: ┌────────────…

Tableau实战指南:从数据连接到业务决策的全流程解析

Tableau实战指南:从数据连接到业务决策的全流程解析

2026/7/19 2:54:04

1. 为什么我坚持用Tableau做可视化——一个从业十年的数据分析师的真心话在数据行业摸爬滚打十年,我经手过上百个分析项目,从初创公司三个人的Excel报表,到跨国集团覆盖27个国家的实时BI看板。见过太多人把“会画图”当成“会分析”&#xff…

Claude Code、Codex、Cursor为何触发安全告警?Agent权限治理实录

Claude Code、Codex、Cursor为何触发安全告警?Agent权限治理实录

2026/7/19 6:14:15

2026 年 7 月上旬,一组终端安全数据把 Claude Code、Cursor 和 Codex 放到了同一张告警图里。 Claude Code、Cursor、Codex 在开发者电脑上正常干活,却触发了原本用来抓攻击者的检测规则。不是因为这些工具突然变成了恶意软件,而是它们做的事…

Python条件判断if语句详解与实战技巧

Python条件判断if语句详解与实战技巧

2026/7/19 6:14:15

1. Python条件判断基础入门if语句是Python编程中最基础也是最重要的控制流工具之一。作为编程新手,掌握if语句的使用是迈向Python编程世界的第一步。if语句的核心作用是根据条件的真假来决定程序的执行路径,这种逻辑判断能力是所有程序的基础构建块。在P…

TI Camera ISP核心模块CBUFF与CSI1B寄存器配置实战指南

TI Camera ISP核心模块CBUFF与CSI1B寄存器配置实战指南

2026/7/19 6:14:15

1. 项目概述与核心价值在嵌入式视觉系统的开发中,图像信号处理器(ISP)的寄存器配置往往是决定项目成败的“临门一脚”。很多开发者面对动辄数百页的芯片手册,常常感到无从下手,要么是配置后图像异常却找不到原因&#…

流式接口返回 200 但工具调用失败:SSE 分帧、finish_reason 与 tool_calls 增量拼接排障

流式接口返回 200 但工具调用失败:SSE 分帧、finish_reason 与 tool_calls 增量拼接排障

2026/7/19 6:14:15

在支持工具调用的 AI 应用中,最难定位的一类问题不是 HTTP 请求失败,而是请求返回了 200,界面也开始出现流式内容,最后却出现“工具参数不是合法 JSON”“函数名为空”“工具重复执行”或“回答停在半截”。这类故障经常被误判为模…

Altera Agilex7 FPGA实现AI超分辨率:毫秒级540p转4K视频处理方案

Altera Agilex7 FPGA实现AI超分辨率:毫秒级540p转4K视频处理方案

2026/7/19 6:14:15

在视频处理领域,实时高清视频流的需求日益增长,但传统软件方案往往面临延迟高、功耗大的瓶颈。Altera Agilex7 FPGA结合AI加速技术,为实时超分辨率上变换提供了硬件级解决方案,能够在毫秒级延迟内实现从540p到4K的视频质量提升。本…

【Linux】13:进程优先级和进程的切换与调度

【Linux】13:进程优先级和进程的切换与调度

2026/7/19 6:04:14

目录 一、优先级的基本概念 1.1 什么是优先级 1.2 为什么要有优先级 二、如何查看并且修改进程优先级 2.1 PRI 2.2 NI 2.3 优先级极限测试 2.4 UID 2.4.1 uid概念 2.4.2 完整流程:你怎么操作资源 2.4.3 特殊情况:进程 UID 会变化(提…

微服务进阶:服务网格与Istio

微服务进阶:服务网格与Istio

2026/7/19 0:03:49

541|微服务进阶:服务网格与Istio 上篇文章我们聊了微服务的基本概念和拆分方法。 但微服务多了,问题也多了: 服务之间怎么通信? 怎么监控每个服务的调用链路? 熔断、限流、重试怎么做? 安全认证怎么统一? 以前这些都靠SDK库(比如Hystrix、Feign),每个服务都要集成…

零售超级终端全域协同:ShareKit 碰一碰商品流转业务落地案例

零售超级终端全域协同:ShareKit 碰一碰商品流转业务落地案例

2026/7/19 0:03:49

一、零售门店全域协同业务背景与行业痛点 1.1 门店超级终端设备矩阵(连锁便利店/商超标准配置) 自助收银Kiosk一体机:顾客结算、自助核销优惠券、商品素材预览;运营折叠平板:店长后台商品上新、图片录入、活动配置、…

噗叽短视频界面分析

噗叽短视频界面分析

2026/7/19 0:03:49

1 和小红书类似,可以采用类似判断方法------------其实他比小红书好判断,因为他没有图片,控件位置几乎是固定的,都不用判断------------2 因为他没有点赞按钮------------而且几乎所有控件位置都是完全一样的,所以我就…

微服务进阶:服务网格与Istio

微服务进阶:服务网格与Istio

2026/7/19 0:03:49

541|微服务进阶:服务网格与Istio 上篇文章我们聊了微服务的基本概念和拆分方法。 但微服务多了,问题也多了: 服务之间怎么通信? 怎么监控每个服务的调用链路? 熔断、限流、重试怎么做? 安全认证怎么统一? 以前这些都靠SDK库(比如Hystrix、Feign),每个服务都要集成…

零售超级终端全域协同:ShareKit 碰一碰商品流转业务落地案例

零售超级终端全域协同:ShareKit 碰一碰商品流转业务落地案例

2026/7/19 0:03:49

一、零售门店全域协同业务背景与行业痛点 1.1 门店超级终端设备矩阵(连锁便利店/商超标准配置) 自助收银Kiosk一体机:顾客结算、自助核销优惠券、商品素材预览;运营折叠平板:店长后台商品上新、图片录入、活动配置、…

噗叽短视频界面分析

噗叽短视频界面分析

2026/7/19 0:03:49

1 和小红书类似,可以采用类似判断方法------------其实他比小红书好判断,因为他没有图片,控件位置几乎是固定的,都不用判断------------2 因为他没有点赞按钮------------而且几乎所有控件位置都是完全一样的,所以我就…