【HarmonyOS】富文本编辑器RichEditor详解
一、前言
在信息化高速发展的今天,普通的文本容器,已经不能够承载用户丰富的表达欲。富文本展示已经是移动开发中,必备要解决的问题,在鸿蒙中,通过在系统层提供RichEditor控件,来解决富文本展示的问题。
HarmonyOS推出的RichEditor控件,提供了从基础文本输入到复杂图文混排的完整解决方案。
从API version 10开始支持的RichEditor控件,不仅具备文本输入、样式设置等基础能力,还创新性地支持自定义键盘、图文混排、事件回调等高级特性。
随着版本迭代,RichEditor不断进化,从API version 11开始支持元服务调用,到API version 20引入AI菜单和撤销样式保留等功能,已发展为一个成熟稳定的富文本解决方案。
本文将从实际使用流程和完整实战Demo出发,详细解析RichEditor控件的核心功能与应用场景,帮助开发者快速掌握这一强大工具的使用方法。
二、使用流程
1、组件创建方式
RichEditor控件提供了两种创建方式:
(1)使用属性字符串构建

这种方式一般用于比较简单的富文本场景,例如上图颜色不同的一段话。 基于属性字符串(StyledString/MutableStyledString)构建,持有属性字符串对象来管理数据,通过修改属性字符串对象的内容、样式,再传递给组件,实现对富文本组件内容的更新。 相比于使用controller接口进行内容样式更新,使用起来更加灵活便捷。
typescript
@Entry
@Component
struct Index {
// 定义字体样式对象,设置字体颜色为粉色
fontStyle: TextStyle = new TextStyle({
fontColor: Color.Pink
});
// 创建可变样式字符串,用于存储富文本内容及其样式
// 初始文本为"使用属性字符串构建的RichEditor组件"
// 并为前5个字符("使用属性字")应用上面定义的粉色字体样式
mutableStyledString: MutableStyledString = new MutableStyledString("使用属性字符串构建的RichEditor组件",
[{
start: 0, // 样式起始位置(从0开始)
length: 5, // 样式作用的字符长度
styledKey: StyledStringKey.FONT, // 样式类型为字体样式
styledValue: this.fontStyle // 具体的样式值
}]);
// 初始化属性字符串模式的RichEditor控制器
// 该控制器专门用于处理基于属性字符串的富文本操作
controller: RichEditorStyledStringController = new RichEditorStyledStringController();
// 配置RichEditor组件的选项,将控制器传入
options: RichEditorStyledStringOptions = { controller: this.controller };
build() {
Column() {
// 构建RichEditor组件,使用上面配置的选项
RichEditor(this.options)
// 组件初始化完成回调
// 当RichEditor组件准备好后,将之前创建的可变样式字符串设置到编辑器中
.onReady(() => {
this.controller.setStyledString(this.mutableStyledString);
})
}
.height('100%') // Column高度占满整个父容器
.width('100%') // Column宽度占满整个父容器
.justifyContent(FlexAlign.Center) // 垂直方向居中对齐子组件
}
}
(2)使用RichEditorController构建

这种方式一般用于复杂内容场景,通过RichEditorController提供的接口实现内容、样式的管理。
typescript
@Entry
@Component
struct IndexPage2 {
// 初始化富文本编辑器控制器,用于管理RichEditor组件
controller: RichEditorController = new RichEditorController();
// 配置RichEditor组件选项,传入控制器实例
options: RichEditorOptions = { controller: this.controller };
build() {
Column() {
Column() {
// 创建RichEditor组件并应用配置选项
RichEditor(this.options)
// 组件初始化完成回调,用于设置初始内容
.onReady(() => {
// 1. 添加第一段文本内容
// 使用addTextSpan方法添加文本,并设置橙色字体、16px大小
this.controller.addTextSpan('使用RichEditorController', {
style: {
fontColor: Color.Orange,
fontSize: 16
}
});
// 2. 添加符号内容
// 使用addSymbolSpan方法添加系统内置符号(篮球图标)
// 设置符号大小为30px
this.controller.addSymbolSpan($r("sys.symbol.basketball_fill"), {
style: {
fontSize: 30
}
});
// 3. 添加第二段文本内容
// 使用addTextSpan方法添加文本,并设置红色字体、20px大小
this.controller.addTextSpan('构建富文本!!!', {
style: {
fontColor: Color.Red,
fontSize: 20
}
});
})
}.width('100%') // 内部Column宽度占满父容器
}.height('100%') // 外部Column高度占满父容器
}
}
2、组件的属性配置参数效果
RichEditor提供了丰富的属性来定制编辑体验,下面介绍几个常用属性的配置方法。
(1)自定义选择菜单
通过bindSelectionMenu属性可以设置自定义选择菜单,替代组件默认的文本选择菜单,实现更丰富的菜单功能,如翻译、加粗等。
typescript
// 自定义菜单构建器
@Builder
CustomMenu() {
Column() {
Menu() {
MenuItemGroup() {
MenuItem({
startIcon: $r('app.media.icon_bold'),
content: "加粗"
})
MenuItem({
startIcon: $r('app.media.icon_italic'),
content: "斜体"
})
MenuItem({
startIcon: $r('app.media.icon_underline'),
content: "下划线"
})
}
}
.radius(8)
.backgroundColor(Color.White)
.width(200)
}
}
// 在RichEditor中绑定自定义菜单
RichEditor(this.options)
.onReady(() => {
this.controller.addTextSpan('长按触发自定义菜单', {
style: {
fontColor: Color.Black,
fontSize: 16
}
})
})
.bindSelectionMenu(RichEditorSpanType.TEXT, this.CustomMenu, ResponseType.LongPress)
.width(300)
.height(200)
(2)光标和手柄颜色设置
通过caretColor属性可以设置输入框光标和手柄的颜色,提高视觉辨识度,使光标颜色与应用整体风格相协调。
typescript
RichEditor(this.options)
.onReady(() => {
this.controller.addTextSpan('设置了橙色光标和手柄的富文本', {
style: {
fontColor: Color.Black,
fontSize: 16
}
})
})
.caretColor(Color.Orange)
.width(300)
.height(100)
(3)占位文本设置
通过placeholder属性可以设置无输入时的提示文本,引导用户正确操作。
typescript
RichEditor(this.options)
.placeholder("请输入您的内容...", {
fontColor: Color.Gray,
font: {
size: 14,
family: "HarmonyOS Sans"
}
})
.width(300)
.height(80)
3、组件的事件监听与交互控制逻辑
RichEditor提供了丰富的事件监听接口,实现更灵活的编辑交互逻辑。
(1)初始化完成事件
初始化回调函数,一般在这里进行数据的加载,或者组件文本的拼接等。
typescript
RichEditor(this.options)
.onReady(() => {
console.info('RichEditor初始化完成');
})
(2)选择变化事件
内容选择区域或光标位置变化时触发,可用于实时更新工具栏状态。
typescript
RichEditor(this.options)
.onSelectionChange((range) => {
console.info(`选中范围变化: start=${range.start}, end=${range.end}`);
// 根据选中范围更新工具栏按钮状态
this.updateToolbarState(range);
})
(3)粘贴事件
粘贴操作前触发,可用于自定义粘贴内容处理。
typescript
RichEditor(this.options)
.onPaste((event) => {
// 阻止默认粘贴行为
event.preventDefault();
// 自定义粘贴处理逻辑
this.handleCustomPaste(event);
})
4、内容操作与管理
通过控制器可以实现对编辑内容的程序化操作。
添加文本内容
typescript
// 添加普通文本
this.controller.addTextSpan('新添加的文本内容', {
style: {
fontSize: 16,
fontColor: Color.Blue
}
});
// 在指定位置添加文本
this.controller.addTextSpan('在指定位置添加的文本', {
style: {
fontSize: 16,
fontStyle: FontStyle.Italic
},
offset: 10 // 在偏移量10的位置添加
});
5、添加图片内容
typescript
this.controller.addImageSpan($r('app.media.image'), {
imageStyle: {
size: [300, 200], // 图片大小
objectFit: ImageFit.Contain, // 图片缩放类型
verticalAlign: ImageSpanAlignment.MIDDLE // 垂直对齐方式
}
});
6、更新文本样式
typescript
// 更新指定范围的文本样式
this.controller.updateSpanStyle({
start: 0,
end: 5,
textStyle: {
fontWeight: 700, // 加粗
decoration: {
type: TextDecorationType.Underline, // 下划线
color: Color.Red
}
}
});
三、DEMO源码
DEMO实现了一个富文本编辑器界面,支持字体样式设置、段落缩进控制、内容选中与编辑等功能,并通过自定义标记生成器实现列表缩进的可视化展示。
typescript
const canvasWidth = 1000;
const canvasHeight = 100;
const Indentation = 40;
// 段落缩进标记生成器类
class LeadingMarginCreator {
private settings: RenderingContextSettings = new RenderingContextSettings(true); // 渲染上下文设置
private offscreenCanvas: OffscreenCanvas = new OffscreenCanvas(canvasWidth, canvasHeight); // 离屏画布
private offContext: OffscreenCanvasRenderingContext2D = this.offscreenCanvas.getContext("2d", this.settings); // 离屏画布渲染上下文
public static instance: LeadingMarginCreator = new LeadingMarginCreator(); // 单例实例
// 获得字体字号级别(0-4级)
public getFontSizeLevel(fontSize: number) {
const fontScaled: number = Number(fontSize) / 16; // 字体缩放比例(相对于16px基准)
enum FontSizeScaleThreshold {
SMALL = 0.9, // 小字体阈值
NORMAL = 1.1, // 正常字体阈值
LEVEL_1_LARGE = 1.2, // 1级大字体阈值
LEVEL_2_LARGE = 1.4, // 2级大字体阈值
LEVEL_3_LARGE = 1.5 // 3级大字体阈值
}
let fontSizeLevel: number = 1; // 初始字号级别为1
// 根据缩放比例确定字号级别
if (fontScaled < FontSizeScaleThreshold.SMALL) {
fontSizeLevel = 0;
} else if (fontScaled < FontSizeScaleThreshold.NORMAL) {
fontSizeLevel = 1;
} else if (fontScaled < FontSizeScaleThreshold.LEVEL_1_LARGE) {
fontSizeLevel = 2;
} else if (fontScaled < FontSizeScaleThreshold.LEVEL_2_LARGE) {
fontSizeLevel = 3;
} else if (fontScaled < FontSizeScaleThreshold.LEVEL_3_LARGE) {
fontSizeLevel = 4;
} else {
fontSizeLevel = 1;
}
return fontSizeLevel;
}
// 获得缩进级别比例(根据缩进宽度计算比例)
public getmarginLevel(width: number) {
let marginlevel: number = 1; // 初始缩进比例为1
// 根据不同缩进宽度设置对应的比例
if (width === 40) {
marginlevel = 2.0;
} else if (width === 80) {
marginlevel = 1.0;
} else if (width === 120) {
marginlevel = 2/3;
} else if (width === 160) {
marginlevel = 0.5;
} else if (width === 200) {
marginlevel = 0.4;
}
return marginlevel;
}
// 生成文本标记(将文本转换为像素图)
public genStrMark(fontSize: number, str: string): PixelMap {
this.offContext = this.offscreenCanvas.getContext("2d", this.settings); // 重新获取渲染上下文
this.clearCanvas(); // 清空画布
this.offContext.font = fontSize + 'vp sans-serif'; // 设置字体样式
this.offContext.fillText(str + '.', 0, fontSize * 0.9); // 绘制文本(末尾加点以确保宽度)
// 获取像素图(根据文本长度计算宽度)
return this.offContext.getPixelMap(0, 0, fontSize * (str.length + 1) / 1.75, fontSize);
}
// 生成方形标记(绘制正方形并转换为像素图)
public genSquareMark(fontSize: number): PixelMap {
this.offContext = this.offscreenCanvas.getContext("2d", this.settings); // 重新获取渲染上下文
this.clearCanvas(); // 清空画布
const coordinate = fontSize * (1 - 1 / 1.5) / 2; // 计算起始坐标
const sideLength = fontSize / 1.5; // 计算正方形边长
this.offContext.fillRect(coordinate, coordinate, sideLength, sideLength); // 绘制正方形
// 获取正方形像素图
return this.offContext.getPixelMap(0, 0, fontSize, fontSize);
}
// 生成圆圈符号标记(根据缩进级别、字体大小等参数绘制圆形标记)
public genCircleMark(fontSize: number, width: number, level?: number): PixelMap {
const indentLevel = level ?? 1; // 缩进级别(默认1)
const offsetLevel = [22, 28, 32, 34, 38]; // 不同字号级别的垂直偏移量
const fontSizeLevel = this.getFontSizeLevel(fontSize); // 获取字号级别
const marginlevel = this.getmarginLevel(width); // 获取缩进比例
const newCanvas = new OffscreenCanvas(canvasWidth, canvasHeight); // 创建新的离屏画布
const newOffContext: OffscreenCanvasRenderingContext2D = newCanvas.getContext("2d", this.settings); // 新画布的渲染上下文
const centerCoordinate = 50; // 圆心水平坐标基准
const radius = 10; // 圆半径基准
this.clearCanvas(); // 清空画布
// 绘制椭圆(根据参数计算位置和大小)
newOffContext.ellipse(
100 * (indentLevel + 1) - centerCoordinate * marginlevel, // 圆心x坐标
offsetLevel[fontSizeLevel], // 圆心y坐标(根据字号级别)
radius * marginlevel, // 水平半径(根据缩进比例)
radius, // 垂直半径
0, 0, 2 * Math.PI // 椭圆参数(起始角度、结束角度)
);
newOffContext.fillStyle = '66FF0000'; // 填充颜色(半透明红色)
newOffContext.fill(); // 填充图形
// 获取圆形标记的像素图(根据缩进级别计算宽度)
return newOffContext.getPixelMap(0, 0, 100 + 100 * indentLevel, 100);
}
private clearCanvas() {
this.offContext.clearRect(0, 0, canvasWidth, canvasHeight); // 清空画布
}
}
@Entry
@Component
struct IndexPage3 {
// 富文本控制器(用于操作编辑器内容和样式)
controller: RichEditorController = new RichEditorController();
options: RichEditorOptions = { controller: this.controller }; // 富文本编辑器选项
// 缩进标记生成器实例(使用单例模式)
private leadingMarkCreatorInstance = LeadingMarginCreator.instance;
private fontNameRawFile: string = 'MiSans-Bold'; // 自定义字体名称
// 状态变量(用于界面交互和数据展示)
@State fs: number = 30; // 字体大小
@State cl: number = Color.Black; // 字体颜色
@State start: number = -1; // 选中起始位置
@State end: number = -1; // 选中结束位置
@State message: string = "[-1, -1]"; // 选中范围提示信息
@State content: string = ""; // 选中内容
private leftMargin: Dimension = 0; // 左缩进量
private richEditorTextStyle: RichEditorTextStyle = {}; // 富文本样式
// 新增:光标颜色和选中背景色状态
@State cursorColor: Color|string = Color.Black; // 光标颜色
@State selectionColor: Color|string = Color.Gray; // 选中背景色
aboutToAppear() {
// 注册自定义字体(应用启动时加载字体文件)
this.getUIContext().getFont().registerFont({
familyName: 'MiSans-Bold',
familySrc: '/font/MiSans-Bold.ttf'
});
}
build() {
Scroll() {
Column() {
// 颜色控制区域(切换界面主题颜色)
Row() {
Button("红色主题").onClick(() => {
this.cursorColor = Color.Red; // 设置红色光标
this.selectionColor = "#FFCCCC"; // 设置红色选中背景
}).width("30%");
Button("绿色主题").onClick(() => {
this.cursorColor = Color.Green; // 设置绿色光标
this.selectionColor = "#CCFFCC"; // 设置绿色选中背景
}).width("30%");
Button("蓝色主题").onClick(() => {
this.cursorColor = Color.Blue; // 设置蓝色光标
this.selectionColor = "#CCCCFF"; // 设置蓝色选中背景
}).width("30%");
}
.width("100%")
.justifyContent(FlexAlign.SpaceBetween)
.margin({ bottom: 10 });
// 选中范围和内容显示区域(展示当前选中的位置和内容)
Column() {
Text("selection range:").width("100%").fontSize(16); // 选中范围标题
Text() {
Span(this.message) // 显示选中范围信息
}.width("100%").fontSize(16);
Text("selection content:").width("100%").fontSize(16); // 选中内容标题
Text() {
Span(this.content) // 显示选中内容
}.width("100%").fontSize(16);
}
.borderWidth(1)
.borderColor(Color.Red)
.width("100%")
.padding(10)
.margin({ bottom: 10 });
// 样式操作按钮区域(对选中内容进行样式修改)
Row() {
Button("加粗").onClick(() => {
// 更新选中区域文本样式(设置为加粗)
this.controller.updateSpanStyle({
start: this.start,
end: this.end,
textStyle: { fontWeight: FontWeight.Bolder }
});
}).width("25%");
Button("获取选中内容").onClick(() => {
this.content = ""; // 清空内容显示
// 获取选中范围内的所有文本片段
this.controller.getSpans({ start: this.start, end: this.end }).forEach(item => {
if (typeof(item as RichEditorImageSpanResult)['imageStyle'] !== 'undefined') {
// 处理图片片段
this.content += (item as RichEditorImageSpanResult).valueResourceStr + "\n";
} else {
if (typeof(item as RichEditorTextSpanResult)['symbolSpanStyle'] !== 'undefined') {
// 处理符号片段(显示字号)
this.content += (item as RichEditorTextSpanResult).symbolSpanStyle?.fontSize + "\n";
} else {
// 处理普通文本片段(显示文本内容)
this.content += (item as RichEditorTextSpanResult).value + "\n";
}
}
});
}).width("25%");
Button("删除选中内容").onClick(() => {
// 删除选中区域内容
this.controller.deleteSpans({ start: this.start, end: this.end });
this.start = -1; // 重置选中起始位置
this.end = -1; // 重置选中结束位置
this.message = "[" + this.start + ", " + this.end + "]"; // 更新选中范围提示
}).width("25%");
Button("设置样式1").onClick(() => {
// 设置输入时的默认样式
this.controller.setTypingStyle({
fontWeight: 'medium', // 中等粗细
fontFamily: this.fontNameRawFile, // 自定义字体
fontColor: Color.Blue, // 蓝色
fontSize: 50, // 字号50
fontStyle: FontStyle.Italic, // 斜体
decoration: { type: TextDecorationType.Underline, color: Color.Green } // 绿色下划线
});
}).width("25%");
}
.borderWidth(1)
.borderColor(Color.Red)
.width("100%")
.height("10%")
.margin({ bottom: 10 });
// 富文本编辑器区域(核心编辑界面)
Column() {
RichEditor(this.options)
.onReady(() => {
// 编辑器准备就绪时初始化内容
this.controller.addTextSpan("0123456789\n", {
style: {
fontWeight: 'medium', // 中等粗细
fontFamily: this.fontNameRawFile, // 自定义字体
fontColor: Color.Red, // 红色
fontSize: 50, // 字号50
fontStyle: FontStyle.Italic, // 斜体
decoration: { type: TextDecorationType.Underline, color: Color.Green } // 绿色下划线
}
});
this.controller.addTextSpan("abcdefg", {
style: {
fontWeight: FontWeight.Lighter, // 更细
fontFamily: 'HarmonyOS Sans', // HarmonyOS默认字体
fontColor: 'rgba(0,128,0,0.5)', // 半透明绿色
fontSize: 30, // 字号30
fontStyle: FontStyle.Normal, // 正常样式
decoration: { type: TextDecorationType.Overline, color: 'rgba(169, 26, 246, 0.50)' } // 半透明紫色上划线
}
});
})
.onSelect((value: RichEditorSelection) => {
// 选中事件回调(更新选中范围状态)
this.start = value.selection[0];
this.end = value.selection[1];
this.message = "[" + this.start + ", " + this.end + "]";
})
.caretColor(this.cursorColor) // 设置光标颜色(来自状态变量)
.selectedBackgroundColor(this.selectionColor) // 设置选中背景色(来自状态变量)
.borderWidth(1)
.borderColor(Color.Green)
.width("100%")
.height("30%")
.margin({ bottom: 10 });
}
.borderWidth(1)
.borderColor(Color.Red)
.width("100%")
.padding(10);
// 缩进操作按钮区域(控制段落缩进)
Column() {
Row({ space: 5 }) {
Button("向右列表缩进").onClick(() => {
let margin = Number(this.leftMargin); // 当前左缩进量
if (margin < 200) {
margin += Indentation; // 增加缩进量(40像素)
this.leftMargin = margin;
}
// 更新段落样式(设置带标记的缩进)
this.controller.updateParagraphStyle({
start: -10,
end: -10,
style: {
leadingMargin: {
pixelMap: this.leadingMarkCreatorInstance.genCircleMark(100, margin, 1), // 圆形缩进标记
size: [margin, 40] // 缩进标记大小
}
}
});
}).width("48%");
Button("向左列表缩进").onClick(() => {
let margin = Number(this.leftMargin); // 当前左缩进量
if (margin > 0) {
margin -= Indentation; // 减少缩进量(40像素)
this.leftMargin = margin;
}
// 更新段落样式(设置带标记的缩进)
this.controller.updateParagraphStyle({
start: -10,
end: -10,
style: {
leadingMargin: {
pixelMap: this.leadingMarkCreatorInstance.genCircleMark(100, margin, 1), // 圆形缩进标记
size: [margin, 40] // 缩进标记大小
}
}
});
}).width("48%");
}
.margin({ bottom: 10 });
Row({ space: 5 }) {
Button("向右空白缩进").onClick(() => {
let margin = Number(this.leftMargin); // 当前左缩进量
if (margin < 200) {
margin += Indentation; // 增加缩进量(40像素)
this.leftMargin = margin;
}
// 更新段落样式(设置纯空白缩进)
this.controller.updateParagraphStyle({
start: -10,
end: -10,
style: { leadingMargin: margin } // 仅设置缩进宽度
});
}).width("48%");
Button("向左空白缩进").onClick(() => {
let margin = Number(this.leftMargin); // 当前左缩进量
if (margin > 0) {
margin -= Indentation; // 减少缩进量(40像素)
this.leftMargin = margin;
}
// 更新段落样式(设置纯空白缩进)
this.controller.updateParagraphStyle({
start: -10,
end: -10,
style: { leadingMargin: margin } // 仅设置缩进宽度
});
}).width("48%");
}
.margin({ bottom: 10 });
Button("获取当前样式").onClick(() => {
this.richEditorTextStyle = this.controller.getTypingStyle();
console.info("RichEditor getTypingStyle:" + JSON.stringify(this.richEditorTextStyle));
})
.width("100%")
.margin({ bottom: 10 });
}
.width("100%")
.padding(10);
}
.width("100%")
.padding(10);
}
}
}