Text 组件完全指南:从一行文字到富文本排版

本文基于 HarmonyOS(ArkTS 声明式开发范式,API 12 / 5.0.0)写作,所有示例均可在 DevEco Studio 模拟器中验证。


一、引言

在图形界面里,文字是信息密度最高、也最容易被轻视的元素。按钮上的两个字、列表里的一行标题、详情页成段的说明,背后都由同一个基础组件承担——Text。它看上去只是"显示一段字符串",但真正把它用顺手,要理解构造方式、样式体系、富文本拼接、交互边界与布局约束五条脉络。

很多初学者在 ArkUI 里写第一个界面时,几乎都会先写一句 Text('Hello')。这很自然,也容易让人误以为 Text 没有太多可讲的内容。等到项目变大,才发现同样一个标题,在卡片里要截断、在详情里要换行、在价格区要和数字拼成不同颜色、在长文里要控制行高与字间距——这时候再去补知识,往往要回头改很多处布局。

本文把 Text 拆成五个层次来讲:先用最朴素的方式把它跑起来,再谈字体与颜色的"皮肤",接着进入 Span 富文本这个真正的分水岭,然后讨论它与状态、事件的关系,最后落到布局约束这种决定"排版是否好看"的底层逻辑。读完之后,你应该能在大多数业务场景里——标题、正文、价格、标签、状态文案——直接选出对的写法,而不是每次都堆一堆试出来的属性。

顺带说一句架构层面的事:ArkUI 的 Text 属于声明式 UI 的基础组件,它在底层由渲染引擎按文本段落(paragraph)绘制,支持文本测量、断行算法与省略号截断。理解这一点很重要——很多"为什么我的文本没换行"的疑问,本质是父容器没有给 Text 一个确定的宽度约束,导致测量阶段它认为自己可以无限宽,于是就一行到底了。


二、环境准备

动手之前,先把环境理顺,避免后面卡在"工具不对"这种与 Text 无关的地方。

项目 推荐配置 说明
DevEco Studio 5.0 及以上 需支持 API 12 的 SDK
HarmonyOS SDK 5.0.0(12) compatibleSdkVersion 与之对应
设备 Phone 模拟器或真机 本文以模拟器验证为主
工程类型 Stage 模型 + ArkTS EntryAbility 继承 UIAbility

创建工程的路径有两种理解方式,需要区分清楚:

  • 方式一:在 DevEco Studio 新建一个 Empty Ability 工程,直接写 ArkTS 原生页面。本文的演示工程就是这种方式,干净、聚焦。
  • 方式二:在已有的 Flutter·鸿蒙壳工程里,把 ohos/entry/src/main/ets/pages/Index.ets 改成原生页面。这种方式下 EntryAbility 通常继承自 FlutterAbility,不建议为了演示 Text 去动它。

本文配套工程采用方式一,目录结构如下(关键文件已给出):

ohos/
├── AppScope/app.json5
├── build-profile.json5
└── entry/src/main/
    ├── module.json5
    └── ets/
        ├── entryability/EntryAbility.ets
        ├── pages/Index.ets
        └── components/*.ets

若你用的是方式二(Flutter 壳),只需关注 pages/Index.etscomponents/ 下的组件代码,其余配置沿用原工程即可。


三、核心 API 与原理解析

3.1 构造方式:字符串与资源

Text 的构造参数只有两种来源,这是它和很多框架里"什么都能塞"的文本组件不同之处:

// 1. 直接传字符串
Text('你好,鸿蒙!')

// 2. 传资源引用(便于国际化与主题管理)
Text($r('app.string.app_name'))

第一种写法直观,适合写死的中文文案;第二种写法把文案抽到 resources/base/element/string.json,好处是后续做多语言、做文案审核都不用改代码。一个常见误区是:$r 返回的是 Resource 类型,如果你在代码里做了字符串拼接,就得不到资源引用,只能退化成字面量。

3.2 样式体系:字号、字重、颜色、间距

文字的"皮肤"由一组链式属性组成,每个属性都作用于整个 Text

Text('大号 28 号字加粗')
  .fontSize(28)
  .fontWeight(FontWeight.Bold)

Text('拉开字间距')
  .fontSize(18)
  .letterSpacing(4)

Text('更宽松的行高')
  .fontSize(16)
  .lineHeight(26)

几个容易混淆的概念,用表格对齐一下:

属性 作用对象 单位 常见取值
fontSize 字号 vp 14 / 16 / 18 …
fontWeight 字重 枚举 Lighter/Normal/Bold/Bolder
fontColor 颜色 色值 #333rgba(...)
letterSpacing 字符间距 vp 正数拉宽、负数收紧
lineHeight 行高 vp 通常与字号比约 1.4–1.6
decoration 装饰线 对象 下划/中划/上划 + 颜色

这里提一个排版的常识公式:当正文阅读距离在手机屏幕这类近距离场景时,行高与字号的合理比值大致落在:

[
\text{lineHeight} \approx 1.4 \times \text{fontSize} \sim 1.6 \times \text{fontSize}
]

也就是说 16 号字配 22–26 的行高,阅读起来最不累。这个值不是 ArkUI 强制的,但是设计师和产品长期验证出来的舒适区间,写代码时照着取,能少挨几回"这行字太挤了"的反馈。

3.3 Span 富文本:真正的分水岭

Text 支持以 Span 作为子组件,在同一个文本段落里拼出多种样式。这是从"纯文字"走向"富文本"的关键一步。

Text() {
  Span('价格:')
    .fontSize(16)
    .fontColor('#333333')
  Span('¥99')
    .fontSize(24)
    .fontColor('#FF4500')
    .fontWeight(FontWeight.Bold)
  Span(' 起')
    .fontSize(14)
    .fontColor('#999999')
}

需要牢记三条规则:

  1. Span 不能独立渲染,必须是 Text 的子组件;
  2. Span 之间无法单独绑定点击事件,交互只能挂在整块 Text 上;
  3. 若要"图文混排",用 ImageSpan 而非 Image
Text() {
  Span('热度 ')
    .fontSize(16)
  ImageSpan($r('app.media.icon'))
    .width(16)
    .height(16)
    .verticalAlign(ImageSpanAlignment.CENTER)
  Span(' 1.2 万')
    .fontSize(16)
    .fontColor('#FF4500')
}

如果业务需要可点击的富文本片段(比如一段说明里"用户协议"四个字可跳转),Text + Span 做不到,应当改用 RichEditor。这是组件选型的边界,提前知道能省很多返工。

3.4 交互与状态:文本也能"活"起来

Text 继承通用组件事件,可以绑定 onClickonLongPress,并通过 copyOption 控制是否允许复制:

Text('点我会弹 Toast')
  .fontSize(18)
  .fontColor('#0A59F7')
  .onClick(() => {
    promptAction.showToast({ message: 'Text 被点击了' });
  })

Text('支持长按复制')
  .copyOption(CopyOptions.Local)

更常见的是和 @State 联动——文本随数据变化而刷新,这正是声明式 UI 的精髓:

@State count: number = 0;

Text(`当前点击次数:${this.count}`)
Button('点击 +1').onClick(() => { this.count += 1; })

3.5 布局与约束:决定排版好不好看

Text 的布局行为可以用一个判断链来理解:

是且未设 maxLines

是且设了 maxLines

Text 被放入父容器

父容器是否给定宽度约束?

按约束宽度测量内容

按内容撑开宽度

内容是否超过约束宽度?

自动换行

截断并省略号

单行显示

核心属性对照:

属性 作用 备注
width 显式宽度约束 最常用,给 Text 一个宽度
textAlign 文本对齐 Start/Center/End
maxLines 最大行数 配合 textOverflow
textOverflow 溢出处理 Ellipsis 才是省略号
Text('很长的一段内容……')
  .width('90%')
  .maxLines(2)
  .textOverflow({ overflow: TextOverflow.Ellipsis })

3.6 与其他技术方案的对比

Text 放到更大的图里看,它和几个"近亲"各司其职:

组件 适合场景 能否富文本 能否编辑 能否局部点击
Text 展示型文字 是(Span)
Span 作为 Text 子片段
RichEditor 可编辑富文本
TextInput 单行输入
TextArea 多行输入

选型的时候记住一句话:只展示就用 Text,要编辑就上 TextInput/TextArea,既要富文本又要可交互就交给 RichEditor


四、完整代码实现

下面给出演示工程的完整可运行代码。工程以 Tabs 把五个演示模块组织起来,每个模块是一个独立组件,便于阅读与验证。

4.1 入口:EntryAbility.ets

import { UIAbility } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';

export default class EntryAbility extends UIAbility {
  private readonly TAG: string = 'TextGuideAbility';

  onCreate(want: object, launchParam: object): void {
    hilog.info(0x0000, this.TAG, '%{public}s', 'Ability onCreate');
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        hilog.error(0x0000, this.TAG, 'Failed to load: %{public}s', JSON.stringify(err));
        return;
      }
      hilog.info(0x0000, this.TAG, '%{public}s', 'Succeeded in loading the content.');
    });
  }

  onForeground(): void { hilog.info(0x0000, this.TAG, '%{public}s', 'onForeground'); }
  onBackground(): void { hilog.info(0x0000, this.TAG, '%{public}s', 'onBackground'); }
  onDestroy(): void { hilog.info(0x0000, this.TAG, '%{public}s', 'onDestroy'); }
  onWindowStageDestroy(): void { hilog.info(0x0000, this.TAG, '%{public}s', 'onWindowStageDestroy'); }
}

4.2 主页面:Index.ets

import { BasicTextDemo } from '../components/BasicTextDemo';
import { TextStyleDemo } from '../components/TextStyleDemo';
import { TextSpanDemo } from '../components/TextSpanDemo';
import { TextInteractionDemo } from '../components/TextInteractionDemo';
import { TextLayoutDemo } from '../components/TextLayoutDemo';

@Entry
@Component
struct Index {
  @State currentIndex: number = 0;

  build() {
    Column() {
      Tabs({ index: this.currentIndex }) {
        TabContent() { BasicTextDemo() }.tabBar('基础用法')
        TabContent() { TextStyleDemo() }.tabBar('样式与字体')
        TabContent() { TextSpanDemo() }.tabBar('Span 富文本')
        TabContent() { TextInteractionDemo() }.tabBar('交互与状态')
        TabContent() { TextLayoutDemo() }.tabBar('布局与约束')
      }
      .barMode(BarMode.Scrollable)
      .width('100%')
      .height('100%')
    }
    .width('100%')
    .height('100%')
  }
}

4.3 基础用法:BasicTextDemo.ets

@Component
export struct BasicTextDemo {
  build() {
    Scroll() {
      Column({ space: 12 }) {
        Text('1. 最简字符串 Text').fontSize(16).fontWeight(FontWeight.Bold)
        Text('你好,鸿蒙!').fontSize(20)
        Text($r('app.string.app_name') as Resource).fontSize(18).fontColor('#666666')
        Text('这是一段较长的文本,用于演示 Text 在容器宽度不足时如何自动换行,'
          + '以及文本的对齐方式与截断表现。')
          .fontSize(16).lineHeight(22).width('90%')
        Text('')
          .fontSize(16).height(20).backgroundColor('#F2F2F2').width('90%')
      }
      .width('100%').padding(16)
    }
    .width('100%').height('100%')
  }
}

4.4 样式与字体:TextStyleDemo.ets

@Component
export struct TextStyleDemo {
  build() {
    Scroll() {
      Column({ space: 12 }) {
        Text('大号 28 号字加粗').fontSize(28).fontWeight(FontWeight.Bold)
        Text('主题色文字').fontSize(18).fontColor('#0A59F7')
        Text('带透明度').fontSize(18).fontColor('rgba(255, 69, 0, 0.6)')
        Text('拉开字间距 letterSpacing').fontSize(18).letterSpacing(4)
        Text('下划线文本')
          .fontSize(18)
          .decoration({ type: TextDecorationType.Underline, color: '#0A59F7' })
        Text('带阴影的标题')
          .fontSize(24).fontWeight(FontWeight.Bold)
          .shadow({ radius: 6, color: '#33000000', offsetX: 2, offsetY: 3 })
      }
      .width('100%').padding(16)
    }
    .width('100%').height('100%')
  }
}

4.5 Span 富文本:TextSpanDemo.ets

@Component
export struct TextSpanDemo {
  build() {
    Scroll() {
      Column({ space: 12 }) {
        Text() {
          Span('价格:').fontSize(16).fontColor('#333333')
          Span('¥99').fontSize(24).fontColor('#FF4500').fontWeight(FontWeight.Bold)
          Span(' 起').fontSize(14).fontColor('#999999')
        }.width('90%')

        Text() {
          Span('热度 ').fontSize(16).fontColor('#666666')
          ImageSpan($r('app.media.icon') as Resource)
            .width(16).height(16).verticalAlign(ImageSpanAlignment.CENTER)
          Span(' 1.2 万').fontSize(16).fontColor('#FF4500')
        }.width('90%')
      }
      .width('100%').padding(16)
    }
    .width('100%').height('100%')
  }
}

4.6 交互与状态:TextInteractionDemo.ets

import { promptAction } from '@kit.ArkUI';

@Component
export struct TextInteractionDemo {
  @State count: number = 0;
  @State tip: string = '点击下方按钮统计点击次数';

  build() {
    Scroll() {
      Column({ space: 16 }) {
        Text(this.tip).fontSize(16).fontColor('#333333').width('90%')
        Text(`当前点击次数:${this.count}`)
          .fontSize(20).fontWeight(FontWeight.Bold).fontColor('#0A59F7')
        Button('点击 +1').onClick(() => {
          this.count += 1;
          this.tip = `已累计点击 ${this.count}`;
        })
        Text('这段文字支持长按复制')
          .fontSize(16).width('90%').copyOption(CopyOptions.Local)
        Text('点我会弹 Toast')
          .fontSize(18).fontColor('#0A59F7')
          .onClick(() => promptAction.showToast({ message: 'Text 被点击了', duration: 1500 }))
      }
      .width('100%').padding(16)
    }
    .width('100%').height('100%')
  }
}

4.7 布局与约束:TextLayoutDemo.ets

@Component
export struct TextLayoutDemo {
  build() {
    Scroll() {
      Column({ space: 12 }) {
        Text('居中对齐文本')
          .width('90%').textAlign(TextAlign.Center)
          .backgroundColor('#F7F7F7').padding(8)
        Text('这是一段内容很长的文本,当超过两行时应当被截断并以省略号收尾,'
          + '避免把整个页面撑得过长。')
          .fontSize(16).lineHeight(22).width('90%')
          .maxLines(2)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
          .textAlign(TextAlign.Start)
      }
      .width('100%').padding(16)
    }
    .width('100%').height('100%')
  }
}

4.8 模块配置要点

module.json5 中声明了 EntryAbilitypages/Index 的路由,main_pages.json 指向 pages/Index。字符串与颜色资源放在 resources/base/element/ 下。这些都和通用 ArkTS 工程一致,不再赘述。


五、模拟器运行与效果展示

5.1 编译运行步骤

  1. 用 DevEco Studio 打开本文配套 ohos/ 目录;
  2. entry/src/main/resources/base/media/ 放入名为 icon.png 的图标(与 module.json5$media:icon 对应);
  3. 顶部选择 Phone 模拟器(或连接真机),点击 Run;
  4. 应用启动后进入 Index 页面,底部/顶部 Tab 可在五个演示间切换。

5.2 各 Tab 的预期效果

在这里插入图片描述

图 1:基础用法。左起依次是加粗小标题、20 号问候语、灰色资源文本,以及一段触发自动换行的长文本。

在这里插入图片描述

图 2:样式与字体。展示字号阶梯、主题色与带透明度文字、三种装饰线,以及带投影的标题。

在这里插入图片描述

图 3:Span 富文本。同一行内"价格:¥99 起"三种样式拼接,以及"热度 + 图标 + 1.2 万"的图文混排。

在这里插入图片描述

图 4:布局与约束。展示左/中/右对齐,以及超过两行后以省略号截断的长文本。
在这里插入图片描述

5.3 交互验证

在"交互与状态"Tab 中,连续点击"点击 +1",当前点击次数 会实时递增;长按灰色文本可调起系统复制菜单;点击蓝色文本会弹出 Toast。三者分别验证了 @State 驱动、复制策略与事件绑定。


六、调试与常见问题

问题 1:文本不换行,一行撑出屏幕。
根因几乎都是父容器没给 Text 宽度。写法上要么给 Text 显式 .width('90%'),要么把它放进有确定宽度的 Column/Row。记住前文的流程图:没有宽度约束,Text 就按内容撑开。

问题 2:$r('app.string.xxx') 报红或显示成资源名。
检查 string.json 里是否真的定义了该 name,且资源目录是否匹配当前语言(中文在 base 即可,多语言要建 zh_CN 等限定词目录)。

问题 3:想让富文本里某几个字可点击,但 Span 不响应。
这是 Text 的设计边界。改用 RichEditor,或把整段拆成多个 Text/Row 拼接,对其中需要的片段单独绑事件。

问题 4:省略号不出现。
必须同时满足 maxLinestextOverflow({ overflow: TextOverflow.Ellipsis }),缺一不可;并且父容器要有限宽,否则没有"溢出"可言。

性能建议: 列表里大量 Text 时,稳定的字符串优先用资源引用而非每次拼接;频繁变化的数字文本建议放在 @State 粒度最小的组件里,避免整页重绘。如果一段文本永不变化,可以考虑用 $$ 或常量提取减少响应式监听开销。


七、总结与扩展

回过头看,Text 并不复杂,但它的能力是分层的:最底层是"显示字符串",往上是"好看"(字号字重颜色间距),再往上是"有结构"(Span 富文本),然后是"有反应"(状态与事件),最后是"排得下"(布局约束与截断)。每一层都对应一类真实业务:

  • 标题、标签 → 基础用法 + 样式;
  • 价格、热度、说明 → Span 富文本;
  • 计数、状态提示 → 交互与状态;
  • 卡片标题、列表项 → 布局与截断。

把这几层吃透,你在 ArkUI 里写文字类 UI 基本不会再卡壳。

如果还想往下走,建议顺着三条线扩展:

  1. 可编辑富文本:研究 RichEditor,它是 Text 在"可编辑 + 可点击"方向上的延伸;
  2. 文本性能:结合 @State/@ObjectLink 理解文本重绘的触发边界,配合 Profiler 看帧率;
  3. 国际化:把 Text($r(...)) 与多语言限定词目录结合,理解资源在编译期的解析路径。

文字是界面的骨架,把 Text 用对、用稳,界面的可读性与专业感就先赢了一半。

Logo

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

更多推荐