鸿蒙 ArkUI Text 组件完全指南:从基础样式到富文本排版
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.ets与components/下的组件代码,其余配置沿用原工程即可。
三、核心 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 |
颜色 | 色值 | #333、rgba(...) |
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')
}
需要牢记三条规则:
Span不能独立渲染,必须是Text的子组件;Span之间无法单独绑定点击事件,交互只能挂在整块Text上;- 若要"图文混排",用
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 继承通用组件事件,可以绑定 onClick、onLongPress,并通过 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 的布局行为可以用一个判断链来理解:
核心属性对照:
| 属性 | 作用 | 备注 |
|---|---|---|
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 中声明了 EntryAbility 与 pages/Index 的路由,main_pages.json 指向 pages/Index。字符串与颜色资源放在 resources/base/element/ 下。这些都和通用 ArkTS 工程一致,不再赘述。
五、模拟器运行与效果展示
5.1 编译运行步骤
- 用 DevEco Studio 打开本文配套
ohos/目录; - 在
entry/src/main/resources/base/media/放入名为icon.png的图标(与module.json5中$media:icon对应); - 顶部选择 Phone 模拟器(或连接真机),点击 Run;
- 应用启动后进入
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:省略号不出现。
必须同时满足 maxLines 与 textOverflow({ overflow: TextOverflow.Ellipsis }),缺一不可;并且父容器要有限宽,否则没有"溢出"可言。
性能建议: 列表里大量 Text 时,稳定的字符串优先用资源引用而非每次拼接;频繁变化的数字文本建议放在 @State 粒度最小的组件里,避免整页重绘。如果一段文本永不变化,可以考虑用 $$ 或常量提取减少响应式监听开销。
七、总结与扩展
回过头看,Text 并不复杂,但它的能力是分层的:最底层是"显示字符串",往上是"好看"(字号字重颜色间距),再往上是"有结构"(Span 富文本),然后是"有反应"(状态与事件),最后是"排得下"(布局约束与截断)。每一层都对应一类真实业务:
- 标题、标签 → 基础用法 + 样式;
- 价格、热度、说明 → Span 富文本;
- 计数、状态提示 → 交互与状态;
- 卡片标题、列表项 → 布局与截断。
把这几层吃透,你在 ArkUI 里写文字类 UI 基本不会再卡壳。
如果还想往下走,建议顺着三条线扩展:
- 可编辑富文本:研究
RichEditor,它是Text在"可编辑 + 可点击"方向上的延伸; - 文本性能:结合
@State/@ObjectLink理解文本重绘的触发边界,配合 Profiler 看帧率; - 国际化:把
Text($r(...))与多语言限定词目录结合,理解资源在编译期的解析路径。
文字是界面的骨架,把 Text 用对、用稳,界面的可读性与专业感就先赢了一半。
更多推荐

所有评论(0)