对应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(() => {
      // 跳转到隐私政策页面
    })
}

这里有两个要点:

  1. decoration设下划线TextDecorationType.Underline配合fontColor相同的color,做出经典链接样式
  2. 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没按压态这三个点,少走很多弯路。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐