鸿蒙新特性:Span 富文本组件 — 混合样式与精美排版深度解析
大多数 ArkUI 页面中,一段文本只有一种字体、一种颜色、一种大小。但当你想在一句话里强调某个关键词、标注某个术语、或者混用不同字体时,把文本拆成多个 Text 组件会导致换行错位、对齐困难。Span 组件解决了这个问题——在同一个 Text 内混用多种样式,Text 负责排版,Span 负责样式。本文用它构建一个"文章阅读器",展示富文本的完整设计模式。
一、为什么需要 Span?
在传统的 Android 开发中,要实现一句话中不同文字有不同样式,需要用 SpannableString:
SpannableString spannable = new SpannableString("HarmonyOS NEXT");
spannable.setSpan(new ForegroundColorSpan(Color.RED), 0, 9, 0);
spannable.setSpan(new StyleSpan(Typeface.BOLD), 10, 14, 0);
textView.setText(spannable);
代码冗长、索引计算容易出错、样式嵌套复杂。
在早期的 ArkUI 中,缺少 Span 时,开发者只能这样做:
// 错误做法:拆成多个 Text 组件
Row() {
Text('HarmonyOS NEXT 于').fontSize(16)
Text('2024 年').fontSize(16).fontColor(Color.Red).fontWeight(FontWeight.Bold)
Text('正式发布').fontSize(16)
}
这有严重问题——Row 会把文本当作独立的元素排列,英文和中文之间的空格处理混乱,长文本换行时各段独立换行,导致"参差不齐"的排版效果。
Span 解决的就是这个问题:同一个 Text 内的多段文本共享排版上下文(换行、对齐、行高),但每段可以独立控制样式。
// 正确做法:Span 实现混合样式
Text() {
Span('HarmonyOS NEXT 于')
.fontSize(16)
Span('2024 年')
.fontSize(16)
.fontColor(Color.Red)
.fontWeight(FontWeight.Bold)
Span('正式发布')
.fontSize(16)
}
所有 Span 在同一个 Text 容器内,文本自然流转、自动换行、对齐一致——但每个 Span 可以有不同的颜色、大小、粗细、背景色。

二、Span 核心 API 详解
2.1 基本语法
Text() {
Span('这是一段普通文字')
.fontSize(16)
.fontColor('#333333')
Span('这是高亮关键词')
.fontSize(16)
.fontColor('#E74C3C')
.fontWeight(FontWeight.Bold)
Span('回到普通文字')
.fontSize(16)
}
Span 只能作为 Text 的直接子组件。在 Text 外部(比如 Row/Column 中)使用 Span 会导致编译错误。
2.2 Span 支持的样式属性
| 属性 | 说明 | 示例 |
|---|---|---|
.fontSize(size) |
字体大小 | .fontSize(18) |
.fontColor(color) |
字体颜色 | .fontColor('#E74C3C') |
.fontWeight(weight) |
粗细 | .fontWeight(FontWeight.Bold) |
.fontStyle(style) |
斜体 | .fontStyle(FontStyle.Italic) |
.backgroundColor(color) |
背景色(高亮标记) | .backgroundColor('#FFD700') |
.lineHeight(height) |
行高 | .lineHeight(26) |
.fontFamily(family) |
字体族 | .fontFamily('monospace') |
.decoration(...) |
文本装饰(下划线、删除线) | .decoration({ type: TextDecorationType.Underline }) |
.letterSpacing(spacing) |
字间距 | .letterSpacing(2) |
2.3 背景色高亮:代码/术语标记
Text() {
Span('鸿蒙内核采用')
.fontSize(16)
Span(' 微内核设计 ')
.fontSize(16)
.fontColor(Color.White)
.backgroundColor('#1a1a2e')
.fontWeight(FontWeight.Medium)
Span(',仅保留最基础的进程管理和内存管理。')
.fontSize(16)
}
这比单纯改变颜色更醒目——白色文字 + 深色背景,模仿 IDE 中的 “code highlight” 效果。适合标记技术术语、API 名称、代码片段。
2.4 多级文本层级
Span 可以在同一个 Text 内创建多个视觉层级:
// 第一级:大号标题
Span('一、').fontSize(20).fontColor('#1677FF').fontWeight(FontWeight.Bold)
Span('内核与架构').fontSize(20).fontColor('#333').fontWeight(FontWeight.Bold)
// 第二级:正文字体
Span('鸿蒙内核采用微内核设计').fontSize(16).fontColor('#333')
// 第三级:注释/辅助文字(小号灰色)
Span('数据来源:华为开发者白皮书').fontSize(12).fontColor('#999')
三个层级分别承载不同的信息密度——标题(概览)、正文(详情)、注释(来源/时间)。
2.5 Span 的局限性
- Span 不支持
onClick事件。如果需要可点击的文字片段,需要用Text+onClick包裹,或者拆分为多个 Text 组件放在 Row 中。 - Span 只能作为 Text 的直接子组件,不能在 Span 内部嵌套其他组件。
- Span 不是独立组件,不能给它设置
width、height、margin、padding等布局属性。

三、富文本的设计模式
3.1 首字下沉(Drop Cap)
Text() {
Span('H')
.fontSize(36)
.fontColor('#1677FF')
.fontWeight(FontWeight.Bold)
Span('armonyOS NEXT 于 2024 年正式发布')
.fontSize(16)
.fontColor('#333')
}
第一个字母用 36 号字 + 蓝色粗体,后续文字用 16 号字——在同一行内形成视觉冲击力。这是杂志排版中的经典手法,Span 让它在移动端也触手可及。
3.2 内联代码块(Inline Code)
Text() {
Span('使用 ')
.fontSize(16)
Span('@ObservedV2')
.fontSize(16)
.fontColor('#722ED1')
.fontWeight(FontWeight.Bold)
Span(' 标记类,使用 ')
.fontSize(16)
Span('@Trace')
.fontSize(16)
.fontColor('#722ED1')
.fontWeight(FontWeight.Bold)
Span(' 标记属性。')
.fontSize(16)
}
在技术文章中,API 名称、类名、函数名用彩色粗体标记,让读者立刻识别出"这是代码中的符号"而不是普通文字。
3.3 引用块(Blockquote)
Row() {
Row()
.width(4).height('100%')
.backgroundColor('#1677FF')
.borderRadius(2)
Column() {
Text() {
Span('"鸿蒙声明式 UI 的最大优势在于:')
.fontSize(15)
.fontColor('#555')
.fontStyle(FontStyle.Italic)
Span('状态驱动视图,而非视图驱动状态')
.fontSize(15)
.fontColor('#E74C3C')
.fontWeight(FontWeight.Bold)
.fontStyle(FontStyle.Italic)
Span('。"')
.fontSize(15)
.fontColor('#555')
.fontStyle(FontStyle.Italic)
}
Text() {
Span('— 引自 ')
.fontSize(12)
.fontColor('#999')
Span('HarmonyOS 开发者白皮书 (2025)')
.fontSize(12)
.fontColor('#1677FF')
.fontWeight(FontWeight.Medium)
}
}
.layoutWeight(1)
.margin({ left: 12 })
}
左边框(4vp 宽的蓝色竖条)+ 斜体文本 + 来源标注。三个组件(Row + 两个 Text with Span)组成一个完整的引用块。
四、Demo:文章阅读器
本 Demo 构建一个精美的技术文章阅读器——标题渐变区、多级文本层级、关键词高亮、引用块、代码内联、标签云、字体大小切换、点赞交互。
页面结构
ArticleReaderPage (~240行)
├── Header(深色渐变 + 标题 + 作者 + 日期 + 分类标签)
├── 工具栏(字体大小切换 A/A/A + ❤ 点赞)
├── Scroll > Column
│ ├── 引言(首字下沉 + 关键词红色粗体)
│ ├── 第一节:内核与架构(标题 + 内联代码高亮 + 彩色数据)
│ ├── 第二节:声明式 UI(引用块 + 左蓝边)
│ ├── 第三节:状态管理 V2(@ObservedV2/@Trace 高亮)
│ ├── 第四节:实践建议(编号列表 + 彩色图标 + 命令式代码警告)
│ └── 标签云(🏷 ArkUI / HarmonyOS / 声明式UI)
字体大小切换
工具栏提供三种字号(A 小/中/大),点击切换:
@State fontSize: number = 16;
// 小号 (14)
Text() { Span('A').fontSize(12) }
.width(28).height(28).borderRadius(14)
.backgroundColor(this.fontSize === 14 ? AppColors.PRIMARY + '22' : Color.Transparent)
.border({ width: 1, color: this.fontSize === 14 ? AppColors.PRIMARY : '#E0E0E0' })
.textAlign(TextAlign.Center)
.onClick(() => { this.fontSize = 14; })
选中状态通过背景色和边框颜色变化来指示。不同字号的圈大小也不同(28/32/36),视觉上暗示"小/中/大"。
内联代码高亮
文章中的技术术语使用不同的视觉样式:
- API 名称(@ObservedV2、@Trace):紫色粗体
- 关键概念(Android 兼容、自主可控):红色粗体
- 数据指标(30%、40%):红色粗体
- 普通代码(微内核设计):白色文字 + 深色背景
Span(' @ObservedV2 ')
.fontSize(this.fontSize)
.fontColor('#722ED1')
.fontWeight(FontWeight.Bold)
引用块
“鸿蒙声明式 UI 的最大优势在于:状态驱动视图”——左边 4vp 蓝色竖条,斜体引用文本,关键语句加红粗体,底部标注来源。
实践建议编号列表
三个建议用绿色/蓝色/橙色圆形编号标出,底部描述包含内联代码:
- 01 优先使用 @ComponentV2
- 02 用 @Trace 标记可变属性
- 03 拥抱声明式思维(含红色代码示例
if (condition) { text.text = value })
四个交互点
- 字体大小切换 — 三档切换(14/16/18),选中态背景色+边框色变化
- 点赞按钮 — ❤ 按钮,已赞/未赞状态切换,数字+1/-1
- 滚动阅读 — 整个文章滚动浏览,各级标题和正文层叠
- 视觉层级感知 — 关键词、代码、引用块通过颜色/大小/粗细区分
五、完整代码
import { AppColors, BorderRadius, FontSize, Spacing } from '../common/Constants';
@Entry
@Component
struct ArticleReaderPage {
@State fontSize: number = 16;
@State liked: boolean = false;
@State likeCount: number = 128;
build() {
Column() {
Column() {
Text('鸿蒙新特性深度解析')
.fontSize(FontSize.HEADLINE)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
.width('100%')
.margin({ bottom: 4 })
Row() {
Text() {
Span('作者:').fontSize(FontSize.CAPTION).fontColor('#FFFFFF88')
Span('HarmonyOS 技术团队').fontSize(FontSize.CAPTION)
.fontColor('#FFFFFFCC')
.fontWeight(FontWeight.Medium)
}
Blank()
Text() {
Span('📅 ').fontSize(FontSize.CAPTION)
Span('2026-06-07').fontSize(FontSize.CAPTION).fontColor('#FFFFFFAA')
}
}
.width('100%')
Row() {
Text() {
Span('🏷️ ').fontSize(FontSize.CAPTION)
Span('ArkUI').fontSize(FontSize.CAPTION)
.fontColor('#FFD700')
.fontWeight(FontWeight.Medium)
Span(' · ').fontSize(FontSize.CAPTION).fontColor('#FFFFFF44')
Span('HarmonyOS NEXT').fontSize(FontSize.CAPTION)
.fontColor('#FFD700')
.fontWeight(FontWeight.Medium)
Span(' · ').fontSize(FontSize.CAPTION).fontColor('#FFFFFF44')
Span('声明式UI').fontSize(FontSize.CAPTION)
.fontColor('#FFD700')
.fontWeight(FontWeight.Medium)
}
}
.width('100%')
.margin({ top: Spacing.MD })
}
.width('100%')
.padding({ left: Spacing.XXL, right: Spacing.XXL, top: 44, bottom: Spacing.XL })
.linearGradient({
angle: 180,
colors: [['#1a1a2e', 0.0], ['#16213e', 0.6], ['#0f3460', 1.0]]
})
// 工具栏
Row() {
Text() { Span('Aa').fontSize(14).fontColor(AppColors.TEXT_TERTIARY) }
Row() {
Text() { Span('A').fontSize(12).fontColor(AppColors.TEXT_TERTIARY) }
.width(28).height(28).borderRadius(14)
.backgroundColor(this.fontSize === 14 ? AppColors.PRIMARY + '22' :
Color.Transparent)
.border({ width: 1, color: this.fontSize === 14 ? AppColors.PRIMARY :
'#E0E0E0' })
.textAlign(TextAlign.Center)
.onClick(() => { this.fontSize = 14; })
Text() { Span('A').fontSize(16).fontColor(AppColors.TEXT_TERTIARY) }
.width(32).height(32).borderRadius(16)
.backgroundColor(this.fontSize === 16 ? AppColors.PRIMARY + '22' :
Color.Transparent)
.border({ width: 1, color: this.fontSize === 16 ? AppColors.PRIMARY :
'#E0E0E0' })
.textAlign(TextAlign.Center)
.margin({ left: 8 })
.onClick(() => { this.fontSize = 16; })
Text() { Span('A').fontSize(20).fontColor(AppColors.TEXT_TERTIARY) }
.width(36).height(36).borderRadius(18)
.backgroundColor(this.fontSize === 18 ? AppColors.PRIMARY + '22' :
Color.Transparent)
.border({ width: 1, color: this.fontSize === 18 ? AppColors.PRIMARY :
'#E0E0E0' })
.textAlign(TextAlign.Center)
.margin({ left: 8 })
.onClick(() => { this.fontSize = 18; })
}
Blank()
Row() {
Text() {
Span('❤').fontSize(14)
.fontColor(this.liked ? '#FF4757' : AppColors.TEXT_TERTIARY)
}
Text() {
Span(` ${this.likeCount}`).fontSize(FontSize.CAPTION)
.fontColor(AppColors.TEXT_TERTIARY)
}
}
.padding({ left: 12, right: 14, top: 6, bottom: 6 })
.backgroundColor('#FFF0F0')
.borderRadius(9999)
.onClick(() => {
this.liked = !this.liked;
this.likeCount += this.liked ? 1 : -1;
})
}
.width('100%')
.padding({ left: Spacing.XXL, right: Spacing.XXL, top: Spacing.MD,
bottom: Spacing.MD })
.backgroundColor(Color.White)
.border({ width: { bottom: 1 }, color: '#F0F0F0' })
Scroll() {
Column() {
// 引言 — 首字下沉 + 关键词高亮
Column() {
Text() {
Span('H')
.fontSize(36)
.fontColor(AppColors.PRIMARY)
.fontWeight(FontWeight.Bold)
Span('armonyOS NEXT 于 2024 年正式发布,')
.fontSize(this.fontSize).fontColor(AppColors.TEXT_PRIMARY)
.lineHeight(this.fontSize + 10)
Span('标志着鸿蒙生态从')
.fontSize(this.fontSize).fontColor(AppColors.TEXT_PRIMARY)
.lineHeight(this.fontSize + 10)
Span(' Android 兼容 ')
.fontSize(this.fontSize).fontColor('#E74C3C')
.fontWeight(FontWeight.Bold).lineHeight(this.fontSize + 10)
Span(' 走向 ')
.fontSize(this.fontSize).fontColor(AppColors.TEXT_PRIMARY)
.lineHeight(this.fontSize + 10)
Span('自主可控')
.fontSize(this.fontSize).fontColor('#E74C3C')
.fontWeight(FontWeight.Bold).lineHeight(this.fontSize + 10)
Span('。与上一代相比,新版本在以下四个维度实现了质的飞跃:')
.fontSize(this.fontSize).fontColor(AppColors.TEXT_PRIMARY)
.lineHeight(this.fontSize + 10)
}
.width('100%')
}
.width('100%').padding(Spacing.XL)
.backgroundColor(Color.White).borderRadius(BorderRadius.MD)
.margin({ bottom: Spacing.MD })
// 第一节:内核与架构
Column() {
Text() {
Span('一、').fontSize(FontSize.HEADLINE)
.fontColor(AppColors.PRIMARY).fontWeight(FontWeight.Bold)
Span('内核与架构').fontSize(FontSize.HEADLINE)
.fontColor(AppColors.TEXT_PRIMARY).fontWeight(FontWeight.Bold)
}
.width('100%').margin({ bottom: Spacing.MD })
Text() {
Span('鸿蒙内核采用')
.fontSize(this.fontSize).fontColor(AppColors.TEXT_PRIMARY)
.lineHeight(this.fontSize + 10)
Span(' 微内核设计 ')
.fontSize(this.fontSize).fontColor(Color.White)
.backgroundColor('#1a1a2e').fontWeight(FontWeight.Medium)
.lineHeight(this.fontSize + 10)
Span(',仅保留最基础的进程管理、内存管理和IPC。文件系统、网络协议栈、设备驱动等模块运行在用户态。这种设计使内核代码量缩减到')
.fontSize(this.fontSize).fontColor(AppColors.TEXT_PRIMARY)
.lineHeight(this.fontSize + 10)
Span(' Linux 内核的千分之一 ')
.fontSize(this.fontSize).fontColor('#E74C3C')
.fontWeight(FontWeight.Bold).lineHeight(this.fontSize + 10)
Span(',攻击面大幅缩小。')
.fontSize(this.fontSize).fontColor(AppColors.TEXT_PRIMARY)
.lineHeight(this.fontSize + 10)
}
.width('100%').margin({ bottom: Spacing.SM })
Text() {
Span('华为官方数据显示,鸿蒙 NEXT 的系统流畅度比上一代提升了 ')
.fontSize(this.fontSize).fontColor(AppColors.TEXT_SECONDARY)
.lineHeight(this.fontSize + 8)
Span('30%')
.fontSize(this.fontSize).fontColor('#E74C3C')
.fontWeight(FontWeight.Bold).lineHeight(this.fontSize + 8)
Span(',应用启动速度提升了 ')
.fontSize(this.fontSize).fontColor(AppColors.TEXT_SECONDARY)
.lineHeight(this.fontSize + 8)
Span('40%')
.fontSize(this.fontSize).fontColor('#E74C3C')
.fontWeight(FontWeight.Bold).lineHeight(this.fontSize + 8)
Span('。这得益于方舟编译器对 ArkTS 代码的 AOT 编译优化。')
.fontSize(this.fontSize).fontColor(AppColors.TEXT_SECONDARY)
.lineHeight(this.fontSize + 8)
}
.width('100%')
}
.width('100%').padding(Spacing.XL)
.backgroundColor(Color.White).borderRadius(BorderRadius.MD)
.margin({ bottom: Spacing.MD })
// 第二节:声明式 UI + 引用块
Column() {
Text() {
Span('二、').fontSize(FontSize.HEADLINE)
.fontColor(AppColors.PRIMARY).fontWeight(FontWeight.Bold)
Span('声明式 UI 范式').fontSize(FontSize.HEADLINE)
.fontColor(AppColors.TEXT_PRIMARY).fontWeight(FontWeight.Bold)
}
.width('100%').margin({ bottom: Spacing.MD })
Text() {
Span('ArkUI 是鸿蒙的声明式 UI 框架,')
.fontSize(this.fontSize).fontColor(AppColors.TEXT_PRIMARY)
.lineHeight(this.fontSize + 10)
Span('核心设计哲学与 Flutter 和 SwiftUI 一致')
.fontSize(this.fontSize).fontColor('#1a1a2e')
.fontWeight(FontWeight.Medium).lineHeight(this.fontSize + 10)
Span(':UI = f(state)。当 @State 变量变化时,框架自动计算差异并更新组件树。')
.fontSize(this.fontSize).fontColor(AppColors.TEXT_PRIMARY)
.lineHeight(this.fontSize + 10)
}
.width('100%').margin({ bottom: Spacing.LG })
Row() {
Row()
.width(4).height('100%')
.backgroundColor(AppColors.PRIMARY).borderRadius(2)
Column() {
Text() {
Span('"鸿蒙声明式 UI 的最大优势在于:')
.fontSize(this.fontSize - 1).fontColor('#555')
.fontStyle(FontStyle.Italic).lineHeight(this.fontSize + 8)
Span('状态驱动视图,而非视图驱动状态')
.fontSize(this.fontSize - 1).fontColor('#E74C3C')
.fontWeight(FontWeight.Bold).fontStyle(FontStyle.Italic)
.lineHeight(this.fontSize + 8)
Span('。这意味着开发者的心智负担从「如何更新 UI」转移到「如何管理数据」。"')
.fontSize(this.fontSize - 1).fontColor('#555')
.fontStyle(FontStyle.Italic).lineHeight(this.fontSize + 8)
}
.width('100%')
Text() {
Span('— 引自 ').fontSize(FontSize.CAPTION)
.fontColor(AppColors.TEXT_TERTIARY)
Span('HarmonyOS 开发者白皮书 (2025)')
.fontSize(FontSize.CAPTION).fontColor(AppColors.PRIMARY)
.fontWeight(FontWeight.Medium)
}
.width('100%').margin({ top: Spacing.SM })
}
.layoutWeight(1).margin({ left: Spacing.MD })
}
.width('100%').padding({ top: Spacing.SM, bottom: Spacing.SM })
}
.width('100%').padding(Spacing.XL)
.backgroundColor(Color.White).borderRadius(BorderRadius.MD)
.margin({ bottom: Spacing.MD })
// 第三节:状态管理 V2
Column() {
Text() {
Span('三、').fontSize(FontSize.HEADLINE)
.fontColor(AppColors.PRIMARY).fontWeight(FontWeight.Bold)
Span('状态管理 V2').fontSize(FontSize.HEADLINE)
.fontColor(AppColors.TEXT_PRIMARY).fontWeight(FontWeight.Bold)
}
.width('100%').margin({ bottom: Spacing.MD })
Text() {
Span('最令人期待的特性是 ')
.fontSize(this.fontSize).fontColor(AppColors.TEXT_PRIMARY)
.lineHeight(this.fontSize + 10)
Span('@ObservedV2')
.fontSize(this.fontSize).fontColor('#722ED1')
.fontWeight(FontWeight.Bold).lineHeight(this.fontSize + 10)
Span(' 和 ')
.fontSize(this.fontSize).fontColor(AppColors.TEXT_PRIMARY)
.lineHeight(this.fontSize + 10)
Span('@Trace')
.fontSize(this.fontSize).fontColor('#722ED1')
.fontWeight(FontWeight.Bold).lineHeight(this.fontSize + 10)
Span('。V1 版本的状态管理是「类级别」的——修改一个 boolean 会触发整个对象的 @State 重建。V2 实现了')
.fontSize(this.fontSize).fontColor(AppColors.TEXT_PRIMARY)
.lineHeight(this.fontSize + 10)
Span(' 属性级响应式 ')
.fontSize(this.fontSize).fontColor(Color.White)
.backgroundColor('#722ED1').fontWeight(FontWeight.Medium)
.lineHeight(this.fontSize + 10)
Span(',只重建使用了被修改属性的组件,性能提升显著。')
.fontSize(this.fontSize).fontColor(AppColors.TEXT_PRIMARY)
.lineHeight(this.fontSize + 10)
}
.width('100%')
}
.width('100%').padding(Spacing.XL)
.backgroundColor(Color.White).borderRadius(BorderRadius.MD)
.margin({ bottom: Spacing.MD })
// 第四节:实践建议
Column() {
Text() {
Span('四、').fontSize(FontSize.HEADLINE)
.fontColor(AppColors.PRIMARY).fontWeight(FontWeight.Bold)
Span('实践建议').fontSize(FontSize.HEADLINE)
.fontColor(AppColors.TEXT_PRIMARY).fontWeight(FontWeight.Bold)
}
.width('100%').margin({ bottom: Spacing.MD })
Column() {
Row() {
Text() {
Span('01').fontSize(20).fontColor(AppColors.PRIMARY)
.fontWeight(FontWeight.Bold)
}
.width(36).height(36).borderRadius(18)
.backgroundColor(AppColors.PRIMARY + '15')
.textAlign(TextAlign.Center).margin({ right: Spacing.MD })
Column() {
Text() {
Span('优先使用 @ComponentV2').fontSize(this.fontSize - 1)
.fontColor(AppColors.TEXT_PRIMARY).fontWeight(FontWeight.Bold)
}
Text() {
Span('新项目从第一天就启用 V2 装饰器,')
.fontSize(FontSize.CAPTION).fontColor(AppColors.TEXT_SECONDARY)
Span('避免将来迁移成本。')
.fontSize(FontSize.CAPTION).fontColor(AppColors.TEXT_SECONDARY)
}
}
.layoutWeight(1)
}
.width('100%').margin({ bottom: Spacing.LG })
Row() {
Text() {
Span('02').fontSize(20).fontColor('#52C41A')
.fontWeight(FontWeight.Bold)
}
.width(36).height(36).borderRadius(18)
.backgroundColor('#52C41A15')
.textAlign(TextAlign.Center).margin({ right: Spacing.MD })
Column() {
Text() {
Span('用 @Trace 标记可变属性').fontSize(this.fontSize - 1)
.fontColor(AppColors.TEXT_PRIMARY).fontWeight(FontWeight.Bold)
}
Text() {
Span('只有被 @Trace 标记的属性会触发 UI 更新,')
.fontSize(FontSize.CAPTION).fontColor(AppColors.TEXT_SECONDARY)
Span('精确控制渲染粒度。')
.fontSize(FontSize.CAPTION).fontColor(AppColors.TEXT_SECONDARY)
}
}
.layoutWeight(1)
}
.width('100%').margin({ bottom: Spacing.LG })
Row() {
Text() {
Span('03').fontSize(20).fontColor('#FAAD14')
.fontWeight(FontWeight.Bold)
}
.width(36).height(36).borderRadius(18)
.backgroundColor('#FAAD1415')
.textAlign(TextAlign.Center).margin({ right: Spacing.MD })
Column() {
Text() {
Span('拥抱声明式思维').fontSize(this.fontSize - 1)
.fontColor(AppColors.TEXT_PRIMARY).fontWeight(FontWeight.Bold)
}
Text() {
Span('不要让 UI 代码中出现 ')
.fontSize(FontSize.CAPTION).fontColor(AppColors.TEXT_SECONDARY)
Span('if (condition) { text.text = value }')
.fontSize(FontSize.CAPTION).fontColor('#E74C3C')
.fontWeight(FontWeight.Medium)
Span(' 这样的命令式语句。')
.fontSize(FontSize.CAPTION).fontColor(AppColors.TEXT_SECONDARY)
}
}
.layoutWeight(1)
}
.width('100%')
}
.width('100%')
}
.width('100%').padding(Spacing.XL)
.backgroundColor(Color.White).borderRadius(BorderRadius.MD)
.margin({ bottom: Spacing.MD })
// 标签云
Row() {
Text() {
Span('🏷️ ').fontSize(FontSize.CAPTION)
Span('ArkUI').fontSize(FontSize.CAPTION)
.fontColor(AppColors.PRIMARY).fontWeight(FontWeight.Medium)
}
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor(AppColors.PRIMARY + '12')
.borderRadius(9999).margin({ right: Spacing.SM })
Text() {
Span('🏷️ ').fontSize(FontSize.CAPTION)
Span('HarmonyOS').fontSize(FontSize.CAPTION)
.fontColor('#E74C3C').fontWeight(FontWeight.Medium)
}
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor('#E74C3C12').borderRadius(9999)
.margin({ right: Spacing.SM })
Text() {
Span('🏷️ ').fontSize(FontSize.CAPTION)
Span('声明式UI').fontSize(FontSize.CAPTION)
.fontColor('#52C41A').fontWeight(FontWeight.Medium)
}
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor('#52C41A12').borderRadius(9999)
}
.width('100%').margin({ bottom: Spacing.XXL })
}
.width('100%')
.padding({ left: Spacing.LG, right: Spacing.LG, top: Spacing.LG })
}
.layoutWeight(1).scrollBar(BarState.Off).backgroundColor('#F5F6FA')
}
.width('100%').height('100%')
}
}
六、常见面试题 / 踩坑点
6.1 Span 和 Text 的区别是什么?
Span 不是独立组件,它只能存在于 Text 内部。Text 负责段落级别的排版(换行、对齐、行高),Span 负责文字级别的样式(颜色、大小、粗细)。可以理解为——Text 是容器(决定"怎么排"),Span 是样式(决定"长什么样")。
6.2 Span 支持 onClick 吗?
不支持。Span 没有 onClick 方法。如果需要可点击的文字片段,有两个方案:
- 把可点击的文字拆成独立 Text,放在 Row 中,给 Text 加 onClick
- 在整段 Text 上加 onClick,通过回调参数区分点击位置(较复杂)
对于标签、链接等可点击元素,方案 1 更实用。
6.3 为什么我的 Span 样式不生效?
常见原因:
- Span 放在了 Text 外部(比如 Column 或 Row 中)——编译错误
- 同一个 Span 上链式调用了多个相同属性——后面的会覆盖前面的
.fontColor()传入了错误的颜色格式(必须是 ‘#RRGGBB’ 或 Color 常量)
6.4 Span 可以嵌套吗?
不可以。Span 不能作为其他 Span 的容器。如果需要"粗体中的红色词"这种嵌套样式,需要把文本拆成三个 Span:粗体普通色 → 粗体红色 → 粗体普通色。
6.5 如何让 Span 文本可选中/可复制?
Text 组件本身有 .copyOption(CopyOptions.LocalDevice) 属性,设置后整个 Text 内的所有 Span 文本都可以被用户长按选中和复制。但 Span 级别没有独立的 copyOption 控制。
七、总结
Span 是 ArkUI 文本系统中最被低估的组件。大部分开发者只用 Text + 统一样式,错过了富文本的视觉表达力。Span 的核心价值有三点:
1. 同段异样,排版不乱。 多个 Span 在同一个 Text 容器内,共享换行、对齐、行高等排版上下文。相比"拆成多个 Text 放 Row 里"的做法,Span 的排版是浑然一体的——不会有奇怪的换行错位。
2. 视觉层级,一眼即知。 通过颜色(红/蓝/紫)、粗细(Bold/Medium/Regular)、背景色(深色代码标记)、斜体等样式的组合,Span 让文本产生了"信息层级"——标题、正文、关键词、代码、注释——读者不需要读内容,扫一眼就能定位重点。
3. 代码简洁,声明式管理。 不需要 SpannableString 的 start/end 索引计算,不需要拆成多个 Text 再拼回 Row。Span 的声明式语法让富文本代码保持可读性——每个 Span 对应一段有独特样式的文字,线性排列,一目了然。
Span 特别适合以下场景:
- 技术文章 / 文档(API 名称高亮、代码内联)— 本文 Demo
- 搜索结果页(搜索关键词高亮)
- 聊天消息(@提及、#话题、URL 链接用不同颜色)
- 商品详情(价格红色、划线原价、优惠标签)
- 用户协议(关键条款加粗/红色提醒)
对于纯文本型页面,Span 是"从朴素到精致"最关键的组件——不需要大幅改动布局,只改变 Text 内部的样式结构,就能让文字从"白纸黑字"变成"精心设计的排版"。
更多推荐



所有评论(0)