对应Demo: RichTextDemo | 难度: 中级 | 关键词: Span, ContainerSpan, ImageSpan, textBackgroundStyle, 富文本
“一段文字里要有红色加粗、蓝色斜体、删除线、还有个图标和可点击的链接”——产品经理甩出这个需求的时候,我第一反应是想用WebView加载HTML。后来发现HarmonyOS NEXT的Text组件配合Span/ImageSpan/ContainerSpan就能搞定大部分场景,性能比WebView好得多,只是API有些地方跟直觉不太一样,特别是ContainerSpan那个背景样式属性名,坑了不少人。
Span:同一段文字多种样式
最基础的富文本需求:一行文字里不同部分有不同样式。传统的Text组件只能设一种fontSize/fontColor,想要混合样式就得用Span:
Text(){Span('这是').fontSize(14).fontColor('#333333')Span('红色加粗').fontSize(14).fontColor('#d32f2f').fontWeight(FontWeight.Bold)Span('和').fontSize(14).fontColor('#333333')Span('蓝色斜体').fontSize(14).fontColor('#1a73e8').fontStyle(FontStyle.Italic)Span('以及').fontSize(14).fontColor('#333333')Span('删除线').fontSize(14).fontColor('#888888').decoration({type:TextDecorationType.LineThrough,color:'#888888'})Span('的混排。').fontSize(14).fontColor('#333333')}Span的写法很直觉:Text()里放多个Span(),每个Span独立设置样式。支持的样式属性包括fontSize、fontColor、fontWeight、fontStyle、decoration(下划线/删除线)。
注意Span不是组件,是Text的子元素,所以它没有onClick?不,Span其实支持onClick,但体验上有点特殊——它是文字级别的点击,没有视觉反馈(没有按压态),需要你自己通过decoration或者fontColor的变化来暗示可点击性。这点后面讲可点击段落的时候再展开。
一个容易忽略的细节:Span之间不要有空格和换行。ArkTS的声明式语法允许你在Span之间换行写,但如果你不小心在两个Span之间加了一个空格字符或者换行符,那个空白会被渲染出来,导致文字间出现意外的空格。所以Span要么紧贴着写,要么刻意控制空白字符。
ContainerSpan:给一组文字加背景
Span只能控制文字样式,如果你想要一段文字带背景色(比如高亮标签效果),就需要ContainerSpan:
Text(){Span('外层文字 ').fontSize(14).fontColor('#333333')ContainerSpan(){Span('背景高亮组').fontSize(14).fontColor('#ffffff')}.textBackgroundStyle({color:'#1a73e8',radius:4})Span(' 继续外层').fontSize(14).fontColor('#333333')}ContainerSpan的用法:它是一个容器,里面放Span(或其他行内元素),然后通过.textBackgroundStyle()给整个容器加背景。注意API名字是textBackgroundStyle,不是backgroundStyle,也不是backgroundColor——这个属性名我踩过坑,写成了backgroundStyle发现完全不生效,查了文档才发现是textBackgroundStyle。属性值接受{ color, radius },color是背景色,radius是圆角半径。
ContainerSpan的本质是"给一组行内元素统一加背景"。你可以把它理解为HTML的<span style="background-color: blue; border-radius: 4px">,但它比HTML span更局限——只支持背景色和圆角,不支持边框、内边距等。如果你的设计稿要求标签有border,ContainerSpan做不到,得考虑别的方案。
另外一个注意点:ContainerSpan里的Span和外面的Span共享基线对齐。如果你给ContainerSpan里的Span设了不同的fontSize,基线会偏移,效果可能不如预期。ContainerSpan适合整组文字同字号不同背景色的场景,不适合字号差异太大的组合。
ImageSpan:文字中间插图片
这是富文本里最实用的功能之一——在文字行内插入图标或小图片,比如"✓ 认证通过"这种通知样式:
Text(){Span('收到 ').fontSize(14).fontColor('#333333')ImageSpan($r('sys.symbol.checkmark_circle_fill')).width(18).height(18).verticalAlign(ImageSpanAlignment.CENTER)Span(' 认证通过通知').fontSize(14).fontColor('#34a853').fontWeight(FontWeight.Medium)}ImageSpan有几个必须遵守的规则:
第一,必须指定width和height。这不是可选的,是强制的。如果你不写width/height,ImageSpan不会报错,但图片不显示——就是一片空白。这个坑非常隐蔽,因为没有任何提示,你以为图片资源没加载,其实是尺寸没设。
第二,verticalAlign建议用CENTER。ImageSpan默认对齐方式是BASELINE,图片底边跟文字基线对齐。但这在大多数场景下看起来图片偏下了——因为图片的视觉中心比基线高。用verticalAlign(ImageSpanAlignment.CENTER)让图片垂直居中对齐文字,视觉效果更好。
**第三,图片资源用r()引用系统符号。∗∗上面例子用了‘r()引用系统符号。** 上面例子用了`r()引用系统符号。∗∗上面例子用了‘r(‘sys.symbol.checkmark_circle_fill’),这是HarmonyOS的系统符号图标,不需要自己准备图片资源。常用的系统符号还有exclamationmark_triangle_fill(警告)、star_fill(星标)等。如果要用自己的图片,就用$r(‘app.media.xxx’)`或者直接传URI。
复杂图文混排的例子:
Text(){ImageSpan($r('sys.symbol.star_fill')).width(16).height(16).verticalAlign(ImageSpanAlignment.CENTER)Span(' 精选推荐 ').fontSize(16).fontColor('#e65100').fontWeight(FontWeight.Bold)ImageSpan($r('sys.symbol.star_fill')).width(16).height(16).verticalAlign(ImageSpanAlignment.CENTER)Span('\n')Span('HarmonyOS NEXT 完全突破了传统移动操作系统的限制,从内核到框架全面重构。').fontSize(13).fontColor('#333333').lineHeight(22)Span('\n')Span('—— 摘自官方介绍').fontSize(11).fontColor('#888888').fontStyle(FontStyle.Italic)}这个例子展示了图标+标题+正文+引用的完整混排。注意用Span('\n')来换行——在Text()的子元素里,换行不是自动的,需要显式插入换行Span。
可点击文字段落:"用户协议"风格
注册页面的"请阅读《用户协议》和《隐私政策》"是最经典的富文本交互需求——普通文字+带下划线的可点击链接。Span支持onClick,配合decoration可以做出这个效果:
Text(){Span('请阅读').fontSize(14).fontColor('#333333')Span('《用户协议》').fontSize(14).fontColor('#1a73e8').decoration({type:TextDecorationType.Underline,color:'#1a73e8'}).onClick(()=>{// 跳转到用户协议页面})Span('和').fontSize(14).fontColor('#333333')Span('《隐私政策》').fontSize(14).fontColor('#1a73e8').decoration({type:TextDecorationType.Underline,color:'#1a73e8'}).onClick(()=>{// 跳转到隐私政策页面})}这里有两个要点:
- decoration设下划线:
TextDecorationType.Underline配合fontColor相同的color,做出经典链接样式 - onClick:Span的onClick可以直接用,不需要额外的手势处理
但Span的onClick有一个体验问题:没有按压态。普通Button按下去有颜色变化反馈,Span点击时文字没有任何视觉变化。用户可能不确定自己点没点上。解决方案是在onClick里手动改变Span的fontColor(需要用State变量控制),或者干脆用ContainerSpan包一层,点击时改变背景色。不过对于"用户协议"这种低频点击场景,没有按压态其实问题不大。
Text+Span方案 vs RichText组件
HarmonyOS还提供了RichText组件,可以直接渲染HTML字符串。看起来很方便,但有明显的局限:
| 方案 | 交互 | 图片 | 性能 | 适用场景 |
|---|---|---|---|---|
| Text+Span | 支持(Span.onClick) | ImageSpan | 高 | 标签/通知/短文本 |
| RichText | 有限(仅链接点击) | img标签 | 低 | HTML内容展示 |
| Web | 完整 | 完整 | 低 | 完整HTML页面 |
RichText只支持有限的HTML标签子集——基本的<b>、<i>、<u>、<a>、<img>可以,复杂的CSS样式和JavaScript完全不支持。而且RichText的onClick只支持<a>标签的链接点击,想做更灵活的交互是不行的。
性能方面,Text+Span是原生组件渲染,RichText需要解析HTML再渲染,开销更大。如果你只是做几种样式的文字混排,不要用RichText——用Text+Span既快又灵活。
Web组件是终极方案,什么HTML都能渲染,但代价是性能开销大、内存占用高、交互延迟。除非你真的要展示一个完整的网页,否则别碰Web组件。
坑点汇总
坑1:ImageSpan必须指定width和height。不指定不报错但图片不显示,这是最隐蔽的坑。每次写ImageSpan先写width/height再写其他属性,养成习惯。
坑2:ContainerSpan的背景属性名是textBackgroundStyle。不是backgroundStyle,不是backgroundColor,也不是background。写错属性名不会报错,只是不生效——然后你查半天为什么背景没出来。
坑3:Span的onClick没有按压态反馈。如果需要视觉反馈,手动用State变量控制fontColor或背景色变化。
坑4:ContainerSpan的baselineOffset效果不稳定。不同API版本表现可能不同,如果发现对齐异常,尝试调整ContainerSpan内部Span的fontSize保持一致。
坑5:ImageSpan的verticalAlign建议用CENTER。默认BASELINE对齐在大多数场景下图片偏下,CENTER对齐视觉效果更好。
坑6:RichText组件只支持有限HTML标签子集。不要期望RichText能渲染任意HTML,它只是一个轻量级的HTML展示组件。复杂HTML请用Web组件。
写在最后
Text+Span+ImageSpan+ContainerSpan这套组合拳覆盖了90%的富文本需求,记住ImageSpan必须设尺寸、ContainerSpan用textBackgroundStyle、Span的onClick没按压态这三个点,少走很多弯路。