大多数 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 不是独立组件,不能给它设置 widthheightmarginpadding 等布局属性。

在这里插入图片描述

三、富文本的设计模式

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 }

四个交互点

  1. 字体大小切换 — 三档切换(14/16/18),选中态背景色+边框色变化
  2. 点赞按钮 — ❤ 按钮,已赞/未赞状态切换,数字+1/-1
  3. 滚动阅读 — 整个文章滚动浏览,各级标题和正文层叠
  4. 视觉层级感知 — 关键词、代码、引用块通过颜色/大小/粗细区分

五、完整代码

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 方法。如果需要可点击的文字片段,有两个方案:

  1. 把可点击的文字拆成独立 Text,放在 Row 中,给 Text 加 onClick
  2. 在整段 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 内部的样式结构,就能让文字从"白纸黑字"变成"精心设计的排版"。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐