
📖 引言
经过前两篇的学习,我们已经掌握了 ArkTS 语言基础和声明式 UI 的核心思想。从这一篇开始,我们进入实战阶段------用具体的组件来构建界面。
万丈高楼平地起,再复杂的界面也是由基础组件组成的。Text(文本)、Image(图片)、Button(按钮)是内容的三要素;Column(列布局)、Row(行布局)是布局的两大利器。掌握了这五个基础组件,你已经可以构建 80% 以上的常见界面了。
但"会用"和"用好"是两回事。你知道 Text 的 maxLines 和 textOverflow 怎么配合吗?Image 的各种 objectFit 模式有什么区别?Column 的 justifyContent 和 alignItems 怎么选?这些细节决定了界面的精致程度。
本文将以「民族图鉴」项目中的民族卡片为例,从基础用法到高级技巧,从 API 到设计原理,带你深入掌握这五个最常用的基础组件。读完本文,你不仅能写出正确的布局,更能写出优雅、高效、可维护的布局代码。
🎯 学习目标
完成本文后,你将能够:
- ✅ 熟练使用 Text 组件,掌握文本截断、富文本、自适应等高级特性
- ✅ 熟练使用 Image 组件,理解各种缩放模式与性能优化技巧
- ✅ 熟练使用 Button 组件,掌握按钮的状态管理与自定义样式
- ✅ 精通 Column/Row 布局组件,理解 Flex 布局原理
- ✅ 掌握 justifyContent 和 alignItems 的区别与使用场景
- ✅ 能够用基础组件构建精美的民族卡片
- ✅ 写出结构清晰、易于维护的布局代码
💡 需求分析
为什么要深入学习基础组件?
很多开发者觉得基础组件简单------Text 就是显示文字,Image 就是显示图片,有什么好学的?但实际上:
- 基础组件用得最多:一个页面里 90% 的组件都是基础组件
- 细节决定体验:文字截断方式、图片缩放模式、间距设置,这些细节积累起来就是"精致感"
- 性能影响大:列表里的每一个卡片都用这些组件,优化一个卡片,整个列表都会变快
- 高级组件的基础:复杂组件都是由基础组件组合而来的
「民族图鉴」的基础组件应用
「民族图鉴」项目中有一个非常典型的组件------民族卡片(EthnicCard)。它综合运用了 Text、Image、Column、Row 等基础组件:
┌─────────────────────────────────┐
│ ┌──────────────┐ │
│ │ │ 汉族 │ ← Row:图片 + 文字信息
│ │ 封面图片 │ 人口:13亿 │
│ │ │ 地区:全国 │
│ └──────────────┘ │
└─────────────────────────────────┘
一个小小的民族卡片,就包含了:
- Image:封面图,圆角,缩放模式
- Text(多个):名称、人口、地区,字号颜色各不相同
- Column:右侧文字区域垂直排列
- Row:整体水平排列
- 各种属性:padding、margin、圆角、阴影、背景色
学会了做这个卡片,其他类似的卡片(音乐卡片、测验题目卡片等)就都不在话下了。
🛠️ 核心实现
步骤1:Text 组件------文本显示的艺术
1.1 基础用法
Text 是最基础也是最常用的组件,用来显示文本。
typescript
// 最简单的用法
Text('Hello, World!')
// 「民族图鉴」中的基本用法
Text('汉族')
.fontSize(20)
.fontColor('#333333')
.fontWeight(FontWeight.Bold)
常用属性一览:
| 属性 | 类型 | 说明 |
|---|---|---|
| fontSize | number | string | 字体大小 |
| fontColor | Color | string | 字体颜色 |
| fontWeight | FontWeight | number | 字重(粗细) |
| fontFamily | string | 字体 |
| fontStyle | FontStyle | 字体样式(正常/斜体) |
| textAlign | TextAlign | 文本对齐方式 |
| lineHeight | number | string | 行高 |
| maxLines | number | 最大行数 |
| textOverflow | TextOverflow | 文本溢出时的处理 |
| decoration | TextDecoration | 装饰线(下划线、删除线) |
| letterSpacing | number | string | 字间距 |
1.2 文本截断:maxLines + textOverflow
当文本太长,显示不下的时候怎么办?这是开发中最常见的场景之一。
typescript
// 「民族图鉴」中的民族简介
Text(this.ethnic.description)
.fontSize(14)
.fontColor('#666666')
.maxLines(2) // 最多显示2行
.textOverflow(TextOverflow.Ellipsis) // 超出部分显示省略号
.lineHeight(22)
textOverflow 的三种模式:
| 模式 | 效果 | 适用场景 |
|---|---|---|
| TextOverflow.Clip | 直接截断,没有省略号 | 精确控制,不需要省略号 |
| TextOverflow.Ellipsis | 显示省略号... | 列表项、卡片简介(最常用) |
| TextOverflow.None | 不截断,全部显示 | 文本可以换行显示 |
💡 常见坑 :
maxLines和textOverflow要配合使用。只设maxLines不设textOverflow,超出的部分会直接被切掉(默认是 Clip)。只设textOverflow不设maxLines,不会有任何效果------因为默认是无限行,不会溢出。
1.3 文本对齐:textAlign
typescript
Text('居中显示')
.width('100%') // 必须有宽度,对齐才有意义
.textAlign(TextAlign.Center)
对齐方式:
| 对齐方式 | 说明 |
|---|---|
| TextAlign.Start | 左对齐(默认) |
| TextAlign.Center | 居中对齐 |
| TextAlign.End | 右对齐 |
| TextAlign.Justify | 两端对齐(多行文本时) |
⚠️ 注意:textAlign 是文本在自身宽度内的对齐。如果 Text 的宽度就是内容的宽度(默认情况),那设置 textAlign 看不出效果。必须给 Text 设置一个宽度(比如 width('100%')),对齐才有意义。
1.4 行高与字间距
typescript
Text('这是一段有行高的文字,\n换行之后行高依然生效。')
.fontSize(14)
.lineHeight(24) // 行高:两行文字基线之间的距离
.letterSpacing(2) // 字间距:每个字符之间的间距
行高的设计原则:
- 正文:行高 ≈ 字号 × 1.5 ~ 1.6
- 标题:行高 ≈ 字号 × 1.2 ~ 1.3
- 行高太小:文字拥挤,阅读累
- 行高太大:文字松散,占空间
1.5 富文本:Span 组件深度详解
如果一段文字里有不同的样式(比如部分文字红色,部分加粗),可以用 Span 组件。Span 是 Text 的子组件,一个 Text 可以包含多个 Span,每个 Span 可以独立设置样式。
1.5.1 基础用法
typescript
Text() {
Span('人口:')
.fontColor('#999999')
.fontSize(12)
Span('约13亿')
.fontColor('#333333')
.fontSize(12)
.fontWeight(FontWeight.Medium)
}
1.5.2 Span 的完整属性
Span 支持的属性比 Text 少一些,但核心样式都有:
| 属性 | 说明 | 示例 |
|---|---|---|
| fontColor | 文字颜色 | .fontColor('#E74C3C') |
| fontSize | 字号 | .fontSize(14) |
| fontWeight | 字重 | .fontWeight(FontWeight.Bold) |
| fontStyle | 字体样式 | .fontStyle(FontStyle.Italic) |
| fontFamily | 字体 | .fontFamily('serif') |
| letterSpacing | 字间距 | .letterSpacing(2) |
| textDecoration | 装饰线 | .decoration({ type: TextDecorationType.Underline }) |
| textCase | 文本大小写 | .textCase(TextCase.UpperCase) |
1.5.3 「民族图鉴」中的 Span 应用场景
场景1:标签+值的组合(同一行内)
在民族详情页的信息卡片中,经常需要"标签灰色 + 值黑色"的组合:
typescript
// components/InfoItem.ets
@Component
struct InfoItem {
@Prop label: string;
@Prop value: string;
build() {
Row() {
Text() {
Span(this.label)
.fontSize(14)
.fontColor('#999999')
Span(this.value)
.fontSize(14)
.fontColor('#333333')
.fontWeight(FontWeight.Medium)
}
.layoutWeight(1)
}
.width('100%')
.height(40)
}
}
场景2:高亮关键词
在搜索结果中高亮匹配的关键词:
typescript
Text() {
Span('汉')
.fontSize(18)
.fontColor('#E74C3C') // 高亮部分用红色
.fontWeight(FontWeight.Bold)
Span('族,是中国人口最多的民族...')
.fontSize(18)
.fontColor('#333333')
}
.maxLines(2)
.textOverflow(TextOverflow.Ellipsis)
场景3:价格展示(整数+小数不同字号)
typescript
Text() {
Span('¥')
.fontSize(14)
.fontColor('#E74C3C')
Span('99')
.fontSize(24)
.fontColor('#E74C3C')
.fontWeight(FontWeight.Bold)
Span('.00')
.fontSize(14)
.fontColor('#E74C3C')
}
1.5.4 Span vs 多个 Text:如何选择?
这是开发者经常困惑的问题。让我们做一个完整的对比:
| 对比维度 | Span | 多个 Text(Row/Column 包裹) |
|---|---|---|
| 换行行为 | 整体换行,像一段文字 | 每个 Text 独立,不会在中间换行 |
| 布局方式 | 只能在 Text 内部,沿文字方向排列 | 可以灵活用 Row/Column 布局 |
| 性能 | 更好(一个组件渲染) | 稍差(多个组件) |
| 交互 | 整体响应点击 | 每个 Text 可以独立响应点击 |
| 适用场景 | 同一段文字内的样式变化 | 独立的文字块布局 |
决策指南:
需要在同一段文字内有不同样式?
├─ 是 → 用 Span(比如标签+值、价格展示、高亮关键词)
└─ 否 → 是左右排列还是上下排列?
├─ 左右 → Row 包裹多个 Text
└─ 上下 → Column 包裹多个 Text
1.5.5 Span 的常见坑
坑1:Span 不支持 maxLines 和 textOverflow
maxLines 和 textOverflow 是 Text 的属性,不是 Span 的属性。要在父 Text 上设置:
typescript
// ✅ 正确
Text() {
Span('很长的文字...')
}
.maxLines(2)
.textOverflow(TextOverflow.Ellipsis)
// ❌ 错误(Span 上没有这些属性)
Text() {
Span('很长的文字...')
.maxLines(2)
}
坑2:Span 之间不会自动加空格
多个 Span 是紧挨着的,如果需要空格,要手动加:
typescript
Text() {
Span('标签:') // 这里的冒号后面有空格
Span('值')
}
💡 最佳实践:Span 是富文本的利器,但不要滥用。如果只是简单的文字布局,用 Row/Column + 多个 Text 更直观。Span 最适合"同一段文字内的样式变化"这种场景。
1.6 文本自适应
有时候希望文字大小能根据容器宽度自动调整:
typescript
Text('长文本可能显示不下')
.width(200)
.maxLines(1)
.textOverflow(TextOverflow.Ellipsis)
或者用 fontSize 的自适应写法:
typescript
// 还可以通过布局来间接实现自适应
// 比如用百分比宽度,让文字自然换行
步骤2:Image 组件------图片加载与优化
2.1 基础用法
Image 组件用来显示图片,支持多种来源。
typescript
// 从资源加载(media 目录)
Image($r('app.media.icon_home'))
// 从 rawfile 加载
Image($rawfile('coverImage/01_han.jpg'))
// 从网络加载
Image('https://example.com/image.jpg')
常用属性:
| 属性 | 类型 | 说明 |
|---|---|---|
| width | number | string | 宽度 |
| height | number | string | 高度 |
| objectFit | ImageFit | 缩放模式 |
| borderRadius | number | ... | 圆角 |
| alt | string | Resource | 加载中占位图 |
| matchTextDirection | boolean | 是否跟随文本方向 |
| autoResize | boolean | 自动调整大小(默认 true) |
2.2 缩放模式:objectFit 深度解析
这是 Image 组件最重要的属性之一,决定了图片在容器内怎么缩放。选对了 objectFit,图片就好看;选错了,要么变形,要么显示不全。
2.2.1 六种模式完整对比
typescript
Image($rawfile('coverImage/01_han.jpg'))
.width(120)
.height(120)
.objectFit(ImageFit.Cover) // 覆盖模式
.borderRadius(8)
让我们用一张图来理解六种模式的区别。假设原图是 400×300(宽高比 4:3),容器是 200×200(正方形):
| 模式 | 效果图示 | 是否变形 | 是否填满 | 是否完整显示 | 适用场景 |
|---|---|---|---|---|---|
| Cover | 等比缩放填满,超出裁剪 | ❌ 不变形 | ✅ 填满 | ❌ 可能被裁剪 | 封面图、头像、列表图(最常用) |
| Contain | 等比缩放完整显示,留空白 | ❌ 不变形 | ❌ 可能留空 | ✅ 完整显示 | 查看大图、商品详情图 |
| Fill | 拉伸填满,不保持比例 | ✅ 会变形 | ✅ 填满 | ✅ 完整显示(但变形了) | 极少数特殊场景(尽量别用) |
| None | 不缩放,原始大小 | ❌ 不变形 | ❌ 不一定 | ❌ 可能显示不全 | 图片大小刚好等于容器时 |
| ScaleDown | 大图缩小,小图原样 | ❌ 不变形 | ❌ 可能留空 | ✅ 大图完整,小图原样 | 不确定图片大小时 |
| Auto | 框架自动选择 | - | - | - | 不确定时(但建议手动选) |
2.2.2 「民族图鉴」中的 objectFit 选择策略
场景1:列表封面图(100×100 正方形)
typescript
// 民族列表中的封面图
Image($rawfile(`coverImage/${ethnic.coverImage}`))
.width(100)
.height(100)
.objectFit(ImageFit.Cover) // ✅ 选 Cover
.borderRadius(8)
为什么选 Cover?
- 列表项空间有限,必须填满 100×100 的区域
- 民族封面图主体通常在中间,裁剪边缘影响不大
- 变形比裁剪更难看,所以宁愿裁剪也不要变形
场景2:详情页头图(宽度 100%,高度 300vp)
typescript
// 详情页顶部的大图
Image($rawfile(`coverImage/${this.ethnic.coverImage}`))
.width('100%')
.height(300)
.objectFit(ImageFit.Cover) // ✅ 还是 Cover
为什么还是 Cover?
- 头图需要有视觉冲击力,必须填满宽度
- 高度固定 300vp,上下裁剪一点不影响整体效果
- 如果用 Contain,左右会有空白,不好看
场景3:民族服饰展示图
typescript
// 服饰图片需要完整展示
Image($rawfile(`costume/${costume.image}`))
.width('100%')
.height(250)
.objectFit(ImageFit.Contain) // ✅ 选 Contain
.backgroundColor('#F5F5F5')
为什么选 Contain?
- 服饰图片必须完整展示,不能裁剪
- 背景用浅灰色填充空白区域,不突兀
2.2.3 objectFit 选择决策树
图片需要完整显示吗?
├─ 是 → 用 Contain(可能有空白,但完整)
└─ 否 → 图片需要填满容器吗?
├─ 是 → 可以接受变形吗?
│ ├─ 可以 → Fill(但尽量别用)
│ └─ 不可以 → Cover(裁剪但不变形,90% 场景选这个)
└─ 否 → 不确定图片大小?
├─ 是 → ScaleDown(大图缩小,小图原样)
└─ 否 → None(图片和容器一样大时)
2.2.4 objectFit 与 objectRepeat 的配合
如果你熟悉 CSS 的 background-repeat,ArkUI 也有类似的 objectRepeat 属性:
typescript
// 重复平铺(类似 CSS 的 background-repeat: repeat)
Image($r('app.media.pattern'))
.width('100%')
.height(200)
.objectFit(ImageFit.None) // 原始大小
.objectRepeat(ImageRepeat.XY) // 两个方向都重复
| 重复模式 | 说明 |
|---|---|
| ImageRepeat.NoRepeat | 不重复(默认) |
| ImageRepeat.X | 水平方向重复 |
| ImageRepeat.Y | 垂直方向重复 |
| ImageRepeat.XY | 两个方向都重复 |
「民族图鉴」中的应用:装饰性的图案背景可以用平铺。
💡 经验总结:90% 的场景用 Cover 就对了。Cover 的特点是"不变形 + 填满 + 可能裁剪",这正是大多数 UI 场景需要的。Contain 用在"必须完整显示"的场景。Fill 尽量别用,变形太丑。
2.3 圆角与圆形图片
typescript
// 圆角矩形
Image($r('app.media.avatar'))
.width(64)
.height(64)
.borderRadius(8)
// 圆形图片(圆角等于宽度的一半)
Image($r('app.media.avatar'))
.width(64)
.height(64)
.borderRadius(32) // 64 / 2 = 32
// 或者直接用 50%
Image($r('app.media.avatar'))
.width(64)
.height(64)
.borderRadius('50%')
⚠️ 注意:borderRadius 要和 objectFit 配合好。如果用 Cover 模式,圆角是在裁剪后的图片上生效的,效果最好。
2.4 图片性能优化
图片是最占用内存的资源之一,优化很重要。
优化技巧:
- 合适的尺寸:不要用 2000px 的大图显示在 100px 的位置
- 按需加载:列表里的图片滚动到可视区域再加载(懒加载)
- 使用合适的格式:WebP > PNG > JPEG(根据场景选择)
- 内存管理:页面销毁时释放大图片
- 缓存:已加载的图片缓存起来,避免重复加载
「民族图鉴」中的优化实践:
- 封面图使用合适的分辨率(不是越大越好)
- 列表使用 LazyForEach 实现懒加载
- 详情页大图只在进入详情页时加载
步骤3:Button 组件------交互的入口
3.1 基础用法
Button 是按钮组件,用于响应用户点击。
typescript
// 文字按钮
Button('点击我')
.onClick(() => {
console.log('按钮被点击了');
})
// 自定义按钮内容
Button() {
Row({ space: 8 }) {
Image($r('app.media.icon_favorite'))
.width(20)
.height(20)
Text('收藏')
.fontSize(14)
}
}
.width(120)
.height(40)
.backgroundColor('#E74C3C')
.borderRadius(20)
常用属性:
| 属性 | 类型 | 说明 |
|---|---|---|
| type | ButtonType | 按钮类型(Capsule/Circle/Normal) |
| stateEffect | boolean | 是否启用点击效果 |
| enabled | boolean | 是否可用 |
| fontColor | Color | 文字颜色 |
| fontSize | number | 文字大小 |
| fontWeight | FontWeight | 文字粗细 |
| backgroundColor | Color | 背景色 |
| borderRadius | number | 圆角 |
3.2 按钮类型与自定义样式深度解析
ArkUI 提供了三种预置的按钮类型,但实际开发中我们更多是自定义按钮样式。
3.2.1 三种预置类型对比
typescript
// 胶囊型按钮(圆角大,最常用)
Button('胶囊按钮', { type: ButtonType.Capsule })
// 圆形按钮
Button('圆', { type: ButtonType.Circle })
.width(60)
.height(60)
// 普通按钮(默认,圆角小)
Button('普通按钮', { type: ButtonType.Normal })
| 类型 | 默认圆角 | 特点 | 适用场景 |
|---|---|---|---|
| Capsule | 高度的一半(完全圆角) | 胶囊形,视觉圆润 | 主要操作按钮、确认按钮(最常用) |
| Circle | 50%(圆形) | 圆形,占空间小 | 悬浮按钮、图标按钮、返回按钮 |
| Normal | 小圆角 | 矩形,中规中矩 | 次要操作、表单按钮、工具栏 |
3.2.2 自定义按钮样式(实战必备)
预置类型只是基础,实际项目中几乎都需要自定义样式。让我们看看「民族图鉴」中的几种按钮样式。
样式1:主按钮(实心背景)
typescript
// 主按钮:红色背景 + 白色文字 + 圆角
Button('开始测验')
.width('100%')
.height(48)
.fontSize(16)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Medium)
.backgroundColor('#E74C3C')
.borderRadius(24)
.onClick(() => {
this.startQuiz();
})
样式2:次要按钮(描边按钮)
typescript
// 次要按钮:白色背景 + 红色边框 + 红色文字
Button('取消')
.width('100%')
.height(48)
.fontSize(16)
.fontColor('#E74C3C')
.fontWeight(FontWeight.Medium)
.backgroundColor('#FFFFFF')
.borderRadius(24)
.border({
width: 1,
color: '#E74C3C'
})
.onClick(() => {
this.handleCancel();
})
样式3:文字按钮(无背景无边框)
typescript
// 文字按钮:只有文字,没有背景和边框
Button('忘记密码?')
.fontSize(14)
.fontColor('#666666')
.backgroundColor(Color.Transparent)
.onClick(() => {
this.showForgotPassword();
})
样式4:图标+文字按钮
typescript
// 带图标的按钮
Button() {
Row({ space: 8 }) {
Image($r('app.media.icon_favorite'))
.width(20)
.height(20)
Text('收藏')
.fontSize(14)
.fontColor('#FFFFFF')
}
}
.width(120)
.height(40)
.backgroundColor('#E74C3C')
.borderRadius(20)
.onClick(() => {
this.toggleFavorite();
})
3.2.3 按钮状态管理
按钮不只有"正常"一种状态,不同状态需要不同的视觉表现。
完整的按钮状态机:
| 状态 | 视觉表现 | 用户感知 |
|---|---|---|
| 正常(Normal) | 正常颜色 | 可点击 |
| 按下(Pressed) | 颜色加深/透明度变化 | 点击反馈 |
| 禁用(Disabled) | 灰色,透明度低 | 不可点击 |
| 加载中(Loading) | 显示加载动画 | 正在处理,不要重复点击 |
「民族图鉴」中的完整按钮实现:
typescript
// components/PrimaryButton.ets
@Component
export struct PrimaryButton {
@Prop text: string;
@Prop isLoading: boolean = false;
@Prop isDisabled: boolean = false;
@Prop backgroundColor: string = '#E74C3C';
build() {
Button() {
if (this.isLoading) {
// 加载中:显示 loading 指示器
Row({ space: 8 }) {
// 用一个旋转的圆点模拟 loading
Text('\u{23F3}')
.fontSize(16)
Text('加载中...')
.fontSize(16)
.fontColor('#FFFFFF')
}
} else {
Text(this.text)
.fontSize(16)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Medium)
}
}
.width('100%')
.height(48)
.backgroundColor(
this.isDisabled ? '#CCCCCC' : this.backgroundColor
)
.borderRadius(24)
.enabled(!this.isLoading && !this.isDisabled)
.opacity(this.isDisabled ? 0.6 : 1)
.onClick(() => {
if (!this.isLoading && !this.isDisabled) {
// 通过事件通知父组件
}
})
}
}
3.2.4 按钮设计规范(来自「民族图鉴」)
为了保证视觉一致性,「民族图鉴」项目制定了按钮设计规范:
| 按钮类型 | 高度 | 字号 | 圆角 | 内边距 | 使用场景 |
|---|---|---|---|---|---|
| 大按钮(Large) | 48vp | 16fp | 24vp | 左右 24vp | 页面底部主操作 |
| 中按钮(Medium) | 40vp | 14fp | 20vp | 左右 20vp | 卡片内操作 |
| 小按钮(Small) | 32vp | 12fp | 16vp | 左右 16vp | 标签、筛选 |
💡 最佳实践:
- 不要依赖预置 type:实际项目中几乎都用自定义样式
- 封装按钮组件:把常用的按钮样式封装成组件,保持全项目统一
- 状态要完整:正常、按下、禁用、加载中,四种状态都要有
- 点击区域足够大:按钮高度至少 44vp(移动端推荐),方便用户点击
3.3 「民族图鉴」收藏按钮实战
让我们看一个「民族图鉴」中实际使用的按钮案例------收藏按钮。它有两种状态(已收藏/未收藏),颜色、文字、图标都不一样,通过状态变量来控制:
typescript
// 收藏按钮(带图标的自定义按钮)
@State isFavorite: boolean = false;
Button() {
Row({ space: 6 }) {
Text(this.isFavorite ? '\u{2764}\u{FE0F}' : '\u{1F90D}')
.fontSize(16)
Text(this.isFavorite ? '已收藏' : '收藏')
.fontSize(14)
.fontColor(this.isFavorite ? '#FFFFFF' : '#E74C3C')
}
}
.width(120)
.height(36)
.backgroundColor(this.isFavorite ? '#E74C3C' : '#FFFFFF')
.borderRadius(18)
.border({
width: 1,
color: '#E74C3C'
})
.onClick(() => {
this.isFavorite = !this.isFavorite;
})
这个按钮完美体现了"状态驱动UI"的思想:状态变量变化,按钮样式自动跟着变。
步骤4:Column & Row------Flex 布局的基础
4.1 什么是 Flex 布局?
Column(列)和 Row(行)是 ArkUI 中最基础、最重要的布局组件。它们的底层都是 Flex 布局(弹性布局)。
- Column:子组件沿垂直方向排列(主轴是纵向的)
- Row:子组件沿水平方向排列(主轴是横向的)
typescript
// Column:垂直排列
Column({ space: 10 }) {
Text('第一行')
Text('第二行')
Text('第三行')
}
.width('100%')
.height(300)
.backgroundColor('#F5F5F5')
// Row:水平排列
Row({ space: 10 }) {
Text('左')
Text('中')
Text('右')
}
.width('100%')
.height(100)
.backgroundColor('#F5F5F5')
4.2 主轴与交叉轴
理解 Flex 布局,首先要理解两个轴:
| 轴 | 说明 | Column 中 | Row 中 |
|---|---|---|---|
| 主轴(Main Axis) | 子组件排列的方向 | 垂直方向(上下) | 水平方向(左右) |
| 交叉轴(Cross Axis) | 与主轴垂直的方向 | 水平方向(左右) | 垂直方向(上下) |
为什么要区分主轴和交叉轴?因为 justifyContent 和 alignItems 是针对不同轴的:
- justifyContent:主轴方向的对齐方式
- alignItems:交叉轴方向的对齐方式
很多人搞混这两个属性,就是因为没搞清楚主轴和交叉轴。
4.3 justifyContent:主轴对齐
justifyContent 控制子组件在主轴上怎么排列。
typescript
// Column 中:垂直方向的对齐
Column({ space: 8 }) {
Text('A')
Text('B')
Text('C')
}
.width('100%')
.height(300)
.justifyContent(FlexAlign.Center) // 垂直居中
七种对齐方式:
| 对齐方式 | 说明 | 效果 |
|---|---|---|
| FlexAlign.Start | 起点对齐 | 挤在顶部/左边 |
| FlexAlign.Center | 居中对齐 | 位于中间 |
| FlexAlign.End | 终点对齐 | 挤在底部/右边 |
| FlexAlign.SpaceBetween | 两端对齐,中间平分 | 第一个在起点,最后一个在终点,中间均匀分布 |
| FlexAlign.SpaceAround | 每个元素周围空间相等 | 元素之间的间距是两端的两倍 |
| FlexAlign.SpaceEvenly | 所有空间都相等 | 元素之间和两端的间距都一样 |
记忆口诀:
- justify = 主轴(主要的排列方向)
- align = 交叉轴(另外那个方向)
4.4 alignItems:交叉轴对齐
alignItems 控制子组件在交叉轴上怎么对齐。
typescript
// Row 中:交叉轴是垂直方向
Row({ space: 8 }) {
Text('左')
Text('中')
Text('右')
}
.width('100%')
.height(100)
.alignItems(VerticalAlign.Center) // 垂直居中
Column 和 Row 的对齐属性对照:
| 组件 | 主轴方向 | 主轴对齐 | 交叉轴对齐 |
|---|---|---|---|
| Column | 垂直 | justifyContent(FlexAlign) | alignItems(HorizontalAlign) |
| Row | 水平 | justifyContent(FlexAlign) | alignItems(VerticalAlign) |
⚠️ 注意 :Column 的 alignItems 用的是
HorizontalAlign(左中右),Row 的 alignItems 用的是VerticalAlign(上中下)。别搞混了!
4.5 组合效果:justifyContent × alignItems 的 8 种经典组合
justifyContent 和 alignItems 可以组合使用,产生丰富的布局效果。掌握了这些组合,你就能应对 90% 以上的布局场景。
4.5.1 以 Row 为例(主轴水平,交叉轴垂直)
组合1:左上对齐(默认)
typescript
Row() {
Text('A')
Text('B')
}
.width('100%')
.height(200)
.backgroundColor('#F5F5F5')
// 默认值:
// justifyContent = FlexAlign.Start
// alignItems = VerticalAlign.Top
效果:子组件挤在左上角。
组合2:左中对齐(最常用的文字+图标对齐)
typescript
Row({ space: 8 }) {
Image($r('app.media.icon'))
.width(20)
.height(20)
Text('文字')
.fontSize(16)
}
.width('100%')
.height(60)
.justifyContent(FlexAlign.Start) // 主轴:左边开始
.alignItems(VerticalAlign.Center) // 交叉轴:垂直居中
效果:图标和文字都在左边,并且垂直居中对齐。这是最常用的组合之一!
组合3:水平垂直都居中
typescript
Row() {
Text('居中')
.fontSize(18)
}
.width('100%')
.height(200)
.justifyContent(FlexAlign.Center) // 主轴:水平居中
.alignItems(VerticalAlign.Center) // 交叉轴:垂直居中
.backgroundColor('#F5F5F5')
效果:文字完全居中。注意:用 Column 也能实现同样的效果。
组合4:右中对齐
typescript
Row({ space: 8 }) {
Text('查看更多')
.fontSize(14)
.fontColor('#999999')
Text('>')
.fontSize(14)
.fontColor('#999999')
}
.width('100%')
.height(50)
.justifyContent(FlexAlign.End) // 主轴:右边
.alignItems(VerticalAlign.Center) // 交叉轴:垂直居中
效果:"查看更多 >" 靠右边显示,垂直居中。
组合5:两端对齐 + 垂直居中
typescript
Row() {
Text('民族名称')
.fontSize(16)
.fontWeight(FontWeight.Bold)
Text('查看详情')
.fontSize(14)
.fontColor('#E74C3C')
}
.width('100%')
.height(50)
.justifyContent(FlexAlign.SpaceBetween) // 主轴:两端对齐
.alignItems(VerticalAlign.Center) // 交叉轴:垂直居中
效果:左边标题,右边"查看详情",两端对齐,垂直居中。这也是非常常用的组合!
4.5.2 组合速查表(Row 版本)
| 组合 | justifyContent | alignItems | 效果 | 常用程度 |
|---|---|---|---|---|
| 左上 | Start | Top | 挤在左上角 | ⭐ |
| 左中 | Start | Center | 左边对齐,垂直居中 | ⭐⭐⭐⭐⭐ |
| 左下 | Start | Bottom | 挤在左下角 | ⭐ |
| 中上 | Center | Top | 顶部水平居中 | ⭐⭐ |
| 居中 | Center | Center | 完全居中 | ⭐⭐⭐⭐ |
| 中下 | Center | Bottom | 底部水平居中 | ⭐⭐ |
| 右上 | End | Top | 挤在右上角 | ⭐ |
| 右中 | End | Center | 右边对齐,垂直居中 | ⭐⭐⭐⭐ |
| 两端居中 | SpaceBetween | Center | 两端对齐,垂直居中 | ⭐⭐⭐⭐⭐ |
💡 记忆技巧:
justifyContent管"排列方向"(Row是左右,Column是上下)alignItems管"另一个方向"(Row是上下,Column是左右)- 90% 的场景用
xxx + Center就够了
4.6 Flex 布局高级技巧
掌握了基础的 justifyContent 和 alignItems 之后,让我们来学习一些更高级的 Flex 布局技巧。
4.6.1 flexGrow、flexShrink、flexBasis 详解
在 Web 开发中,Flex 布局有三个核心属性:flex-grow、flex-shrink、flex-basis。在 ArkUI 中,这些概念同样适用,只是表现形式略有不同。
flexGrow(放大比例):
对应 ArkUI 中的 layoutWeight 属性。当容器有剩余空间时,子组件按比例分配剩余空间。
typescript
Row() {
// 左边占 1 份
Column() {
Text('左')
}
.layoutWeight(1) // flexGrow: 1
.backgroundColor('#FFE0E0')
// 右边占 2 份
Column() {
Text('右')
}
.layoutWeight(2) // flexGrow: 2
.backgroundColor('#E0E0FF')
}
.width('100%')
.height(100)
效果:左边占 1/3,右边占 2/3。
flexShrink(缩小比例):
当空间不足时,子组件按比例缩小。ArkUI 中这个行为是自动的,但可以通过设置 minWidth / minHeight 来限制最小尺寸。
typescript
Row() {
Text('很短')
.backgroundColor('#FFE0E0')
Text('这是一段非常非常非常长的文字,可能会超出容器宽度')
.layoutWeight(1) // 空间不足时,这个会先被压缩
.maxLines(1)
.textOverflow(TextOverflow.Ellipsis)
.backgroundColor('#E0E0FF')
}
.width(300) // 固定宽度,空间有限
flexBasis(基准大小):
在分配剩余空间之前,子组件的初始大小。ArkUI 中对应的就是 width / height 属性。
typescript
// 工作原理:
// 1. 所有子组件先按 flexBasis(即 width/height)分配空间
// 2. 计算剩余空间 = 容器总宽度 - 所有子组件 flexBasis 之和
// 3. 剩余空间按 flexGrow(即 layoutWeight)比例分配给各子组件
// 4. 最终大小 = flexBasis + 分配到的剩余空间
4.6.2 「民族图鉴」中的 Flex 实战
实战1:卡片标题行(左标题 + 右更多)
typescript
// 这是用得最多的布局模式之一
Row() {
Text('民族服饰')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#1A1A1A')
Text('查看更多 >')
.fontSize(14)
.fontColor('#999999')
}
.width('100%')
.height(50)
.justifyContent(FlexAlign.SpaceBetween) // 两端对齐
.alignItems(VerticalAlign.Center) // 垂直居中
实战2:左右固定 + 中间自适应
typescript
// 导航栏:左返回 + 中间标题 + 右菜单
Row() {
// 左边:返回按钮(固定宽度)
Button() {
Text('<')
.fontSize(20)
.fontColor('#333333')
}
.width(44)
.height(44)
.backgroundColor(Color.Transparent)
// 中间:标题(占满剩余空间,居中显示)
Text('民族详情')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.layoutWeight(1) // 占满剩余空间
.textAlign(TextAlign.Center) // 文字居中
// 右边:分享按钮(固定宽度)
Button() {
Text('⋯')
.fontSize(20)
.fontColor('#333333')
}
.width(44)
.height(44)
.backgroundColor(Color.Transparent)
}
.width('100%')
.height(56)
.alignItems(VerticalAlign.Center)
实战3:底部操作栏(两个按钮平分)
typescript
// 底部两个按钮,平分宽度
Row({ space: 16 }) {
Button('收藏')
.layoutWeight(1)
.height(48)
.fontColor('#E74C3C')
.backgroundColor('#FFFFFF')
.borderRadius(24)
.border({ width: 1, color: '#E74C3C' })
Button('开始测验')
.layoutWeight(1)
.height(48)
.fontColor('#FFFFFF')
.backgroundColor('#E74C3C')
.borderRadius(24)
}
.width('100%')
.padding({ left: 16, right: 16 })
.alignItems(VerticalAlign.Center)
4.6.3 Flex 布局常见问题与解决方案
问题1:子组件超出容器宽度
typescript
// ❌ 有问题:文字太长,把右边挤出容器了
Row({ space: 8 }) {
Image(cover)
.width(80)
.height(80)
Text('很长很长很长很长很长的标题')
.fontSize(16)
.maxLines(1)
}
解决方法 :给 Text 加 layoutWeight(1),让它占剩余空间,而不是撑开 Row:
typescript
// ✅ 正确:Text 占剩余空间,超出显示省略号
Row({ space: 8 }) {
Image(cover)
.width(80)
.height(80)
Text('很长很长很长很长很长的标题')
.fontSize(16)
.maxLines(1)
.textOverflow(TextOverflow.Ellipsis)
.layoutWeight(1) // 关键:占满剩余空间
}
问题2:多个 layoutWeight 的子组件怎么分配空间?
答案:按权重比例分配。
总权重 = 1 + 2 + 1 = 4
子组件A 占 1/4
子组件B 占 2/4 = 1/2
子组件C 占 1/4
问题3:如何让一个子组件撑满剩余高度?
用 layoutWeight(1),Column 和 Row 都适用:
typescript
Column() {
Text('头部')
.height(60)
// 中间内容区:撑满剩余高度
Scroll() {
// ... 内容
}
.layoutWeight(1) // 关键
Text('底部')
.height(50)
}
.width('100%')
.height('100%')
4.5 space:子组件间距
space 是最简单、最常用的设置间距的方式:
typescript
// 子组件之间的间距是 16vp
Column({ space: 16 }) {
Text('A')
Text('B')
Text('C')
}
space 的特点:
- 只在子组件之间产生间距
- 第一个子组件前面和最后一个子组件后面没有间距
- 所有子组件之间的间距相等
space vs padding vs margin:
| 方式 | 作用 | 适用场景 |
|---|---|---|
| space | 子组件之间的间距 | 所有子组件间距相同时(最简单) |
| padding | 容器的内边距 | 容器内部和内容之间的距离 |
| margin | 单个组件的外边距 | 某个组件需要特殊间距时 |
4.6 layoutWeight:权重分配
layoutWeight 是 Flex 布局中非常强大的属性,用来分配剩余空间。它对应 Web Flex 布局中的 flex-grow 属性。
typescript
Row() {
// 左侧图片:固定宽度
Image($rawfile('cover.jpg'))
.width(100)
.height(100)
// 右侧文字:占满剩余空间
Column({ space: 8 }) {
Text('标题')
Text('简介简介简介')
}
.layoutWeight(1) // 占满剩余空间
}
.width('100%')
.padding(16)
layoutWeight 的工作原理:
父容器总宽度 = 200
子组件A 宽度 = 50(固定)
子组件B layoutWeight = 1
剩余空间 = 200 - 50 = 150
子组件B 宽度 = 150 × (1 / 1) = 150
多个 layoutWeight 按比例分配:
typescript
Row() {
// 左边占 1 份
Column() { Text('左') }
.layoutWeight(1)
.backgroundColor('#FFE0E0')
// 右边占 2 份
Column() { Text('右') }
.layoutWeight(2)
.backgroundColor('#E0E0FF')
}
.width('100%')
.height(100)
效果:左边占 1/3,右边占 2/3。
💡 最常用的场景:左边固定宽度的图片,右边文字占满剩余空间。这在列表卡片中非常常见。记住这个模式,你会用到无数次。
4.7 组合效果:justifyContent × alignItems 的 8 种经典组合
justifyContent 和 alignItems 可以组合使用,产生丰富的布局效果。掌握了这些组合,你就能应对 90% 以上的布局场景。
4.7.1 以 Row 为例(主轴水平,交叉轴垂直)
组合1:左上对齐(默认)
typescript
Row() {
Text('A')
Text('B')
}
.width('100%')
.height(200)
.backgroundColor('#F5F5F5')
// 默认值:
// justifyContent = FlexAlign.Start
// alignItems = VerticalAlign.Top
效果:子组件挤在左上角。
组合2:左中对齐(最常用的文字+图标对齐)
typescript
Row({ space: 8 }) {
Image($r('app.media.icon'))
.width(20)
.height(20)
Text('文字')
.fontSize(16)
}
.width('100%')
.height(60)
.justifyContent(FlexAlign.Start) // 主轴:左边开始
.alignItems(VerticalAlign.Center) // 交叉轴:垂直居中
效果:图标和文字都在左边,并且垂直居中对齐。这是最常用的组合之一!
组合3:水平垂直都居中
typescript
Row() {
Text('居中')
.fontSize(18)
}
.width('100%')
.height(200)
.justifyContent(FlexAlign.Center) // 主轴:水平居中
.alignItems(VerticalAlign.Center) // 交叉轴:垂直居中
.backgroundColor('#F5F5F5')
效果:文字完全居中。注意:用 Column 也能实现同样的效果。
组合4:两端对齐 + 垂直居中
typescript
Row() {
Text('民族名称')
.fontSize(16)
.fontWeight(FontWeight.Bold)
Text('查看详情')
.fontSize(14)
.fontColor('#E74C3C')
}
.width('100%')
.height(50)
.justifyContent(FlexAlign.SpaceBetween) // 主轴:两端对齐
.alignItems(VerticalAlign.Center) // 交叉轴:垂直居中
效果:左边标题,右边"查看详情",两端对齐,垂直居中。这也是非常常用的组合!
4.7.2 组合速查表(Row 版本)
| 组合 | justifyContent | alignItems | 效果 | 常用程度 |
|---|---|---|---|---|
| 左上 | Start | Top | 挤在左上角 | ⭐ |
| 左中 | Start | Center | 左边对齐,垂直居中 | ⭐⭐⭐⭐⭐ |
| 左下 | Start | Bottom | 挤在左下角 | ⭐ |
| 中上 | Center | Top | 顶部水平居中 | ⭐⭐ |
| 居中 | Center | Center | 完全居中 | ⭐⭐⭐⭐ |
| 中下 | Center | Bottom | 底部水平居中 | ⭐⭐ |
| 右上 | End | Top | 挤在右上角 | ⭐ |
| 右中 | End | Center | 右边对齐,垂直居中 | ⭐⭐⭐⭐ |
| 两端居中 | SpaceBetween | Center | 两端对齐,垂直居中 | ⭐⭐⭐⭐⭐ |
💡 记忆技巧:
justifyContent管"排列方向"(Row是左右,Column是上下)alignItems管"另一个方向"(Row是上下,Column是左右)- 90% 的场景用
xxx + Center就够了
4.8 Flex 布局进阶:原理与实战
如果你熟悉 Web 开发的 Flex 布局,一定知道 flex-grow、flex-shrink、flex-basis 这三兄弟。在 ArkUI 中,这些概念同样适用:
| Web Flex 属性 | ArkUI 对应 | 说明 |
|---|---|---|
| flex-grow | layoutWeight | 放大比例,分配剩余空间 |
| flex-shrink | (自动行为) | 缩小比例,空间不足时自动收缩 |
| flex-basis | width / height | 基准大小,分配空间前的初始尺寸 |
空间分配原理:
1. 所有子组件先按 flexBasis(即 width/height)分配空间
2. 计算剩余空间 = 容器总宽度 - 所有子组件 flexBasis 之和
3. 剩余空间按 layoutWeight 比例分配
4. 最终大小 = flexBasis + 分配到的剩余空间
「民族图鉴」Flex 实战1:导航栏布局
typescript
// 经典的"左-中-右"布局:左返回 + 中标题 + 右菜单
Row() {
Button() { Text('<').fontSize(20).fontColor('#333') }
.width(44).height(44).backgroundColor(Color.Transparent)
Text('民族详情')
.fontSize(18).fontWeight(FontWeight.Bold)
.layoutWeight(1) // 占满剩余空间
.textAlign(TextAlign.Center)
Button() { Text('⋯').fontSize(20).fontColor('#333') }
.width(44).height(44).backgroundColor(Color.Transparent)
}
.width('100%')
.height(56)
.alignItems(VerticalAlign.Center)
「民族图鉴」Flex 实战2:底部双按钮
typescript
// 两个按钮平分宽度
Row({ space: 16 }) {
Button('收藏')
.layoutWeight(1).height(48)
.fontColor('#E74C3C').backgroundColor('#FFF')
.borderRadius(24).border({ width: 1, color: '#E74C3C' })
Button('开始测验')
.layoutWeight(1).height(48)
.fontColor('#FFF').backgroundColor('#E74C3C')
.borderRadius(24)
}
.width('100%')
.padding({ left: 16, right: 16 })
Flex 布局最常见的坑:
文字太长把容器撑开了?给 Text 加 layoutWeight(1)!
typescript
// ❌ 错误:文字太长,把右边挤出容器
Row({ space: 8 }) {
Image(cover).width(80).height(80)
Text('很长很长的标题').fontSize(16).maxLines(1)
}
// ✅ 正确:Text 占剩余空间,超出显示省略号
Row({ space: 8 }) {
Image(cover).width(80).height(80)
Text('很长很长的标题')
.fontSize(16).maxLines(1)
.textOverflow(TextOverflow.Ellipsis)
.layoutWeight(1) // 关键!
}
💡 Flex 三句话心法:
- 先固定,后自适应:固定宽的先排,自适应的(layoutWeight)后分配
- 主轴 justify,交叉轴 align:搞不清就先问"子组件怎么排的"
- 装不下就加 layoutWeight:给需要压缩的那个加上,问题就解决了
步骤5:实战------构建民族卡片
理论说了这么多,让我们来实战一下,用基础组件构建一个精美的民族卡片。
5.1 设计分析
先看看「民族图鉴」中一个民族卡片长什么样:
┌────────────────────────────────────────────┐
│ ┌──────────┐ │
│ │ │ 汉族 │
│ │ 封面图 │ 人口:约13亿 排名:1 │
│ │ 100×100 │ 地区:全国 │
│ └──────────┘ │
└────────────────────────────────────────────┘
← 16vp → ← 12vp →
内边距 间距
组件分解:
整体卡片(Row)
├─ 左侧:封面图(Image)
└─ 右侧:文字区域(Column)
├─ 民族名称(Text,加粗,大字号)
├─ 人口排名行(Row,两个 Text)
└─ 分布地区(Text,灰色,小字号)
5.2 完整实现
typescript
// components/EthnicCard.ets
import { EthnicGroup } from '../models/EthnicModels';
import router from '@ohos.router';
@Component
export struct EthnicCard {
// 民族信息(从父组件传入)
@Prop ethnic: EthnicGroup;
build() {
// 卡片整体:水平排列
Row({ space: 12 }) {
// 左侧:封面图
this.buildCoverImage()
// 右侧:文字区域(占满剩余空间)
Column({ space: 6 }) {
// 民族名称
this.buildName()
// 人口和排名
this.buildMetaRow()
// 分布地区
this.buildRegion()
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.shadow({
radius: 8,
color: '#1A000000',
offsetX: 0,
offsetY: 2
})
.onClick(() => {
this.navigateToDetail();
})
}
@Builder
buildCoverImage(): void {
Image($rawfile(`coverImage/${this.ethnic.coverImage}`))
.width(100)
.height(100)
.objectFit(ImageFit.Cover)
.borderRadius(8)
.backgroundColor('#F5F5F5')
}
@Builder
buildName(): void {
Text(this.ethnic.name)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#1A1A1A')
.maxLines(1)
.textOverflow(TextOverflow.Ellipsis)
}
@Builder
buildMetaRow(): void {
Row({ space: 16 }) {
Row({ space: 4 }) {
Text('人口:')
.fontSize(12)
.fontColor('#999999')
Text(this.ethnic.population)
.fontSize(12)
.fontColor('#666666')
.fontWeight(FontWeight.Medium)
}
Row({ space: 4 }) {
Text('排名:')
.fontSize(12)
.fontColor('#999999')
Text(`第${this.ethnic.populationRank}位`)
.fontSize(12)
.fontColor('#666666')
.fontWeight(FontWeight.Medium)
}
}
}
@Builder
buildRegion(): void {
Row({ space: 4 }) {
Text('地区:')
.fontSize(12)
.fontColor('#999999')
Text(this.ethnic.region)
.fontSize(12)
.fontColor('#666666')
.maxLines(1)
.textOverflow(TextOverflow.Ellipsis)
.layoutWeight(1)
}
}
private navigateToDetail(): void {
router.pushUrl({
url: 'pages/EthnicDetailPage',
params: { ethnicId: this.ethnic.id }
});
}
}
5.3 设计要点解析
让我们分析一下这个卡片的设计细节:
1. 层级结构清晰
Row(卡片容器)
├─ Image(封面,固定宽度)
└─ Column(文字,layoutWeight 占满剩余)
├─ Text(名称)
├─ Row(人口+排名)
└─ Row(地区)
每一层的职责都很明确,用 @Builder 拆分后,代码清晰易读。
2. 文字层次分明
| 文字 | 字号 | 字重 | 颜色 | 作用 |
|---|---|---|---|---|
| 民族名称 | 18 | Bold | #1A1A1A | 主标题,最醒目 |
| 人口/排名数值 | 12 | Medium | #666666 | 次要信息 |
| 人口/排名标签 | 12 | Regular | #999999 | 辅助标签,最淡 |
通过字号、字重、颜色的组合,形成了清晰的视觉层次。用户扫一眼就能抓到重点。
3. 间距有节奏
卡片内边距:16vp
图片和文字间距:12vp
文字行间距:6vp
人口和排名间距:16vp
不是所有间距都一样------大间距分隔大区块,小间距分隔小区块。这就是"间距的节奏"。
4. 交互反馈
- 点击整个卡片都能跳转(大点击区域,友好)
- 卡片有阴影,有立体感
- 圆角 12vp,圆润不生硬
⚠️ 常见问题与解决方案
问题1:Column/Row 的对齐怎么都不对,为什么?
现象 :
设置了 justifyContent 或 alignItems,但效果和预期不一样。
常见原因:
| 原因 | 说明 |
|---|---|
| 容器没有固定的宽/高 | 容器大小就是内容大小,对齐看不出来 |
| justifyContent 和 alignItems 搞反了 | 主轴交叉轴搞混了 |
| 属性名写错了 | Column 用 VerticalAlign,Row 用 HorizontalAlign |
| 子组件自己有宽度 | 对齐是在容器剩余空间里对齐 |
排查步骤:
第1步:给容器加个背景色,看看容器到底多大
↓
第2步:确认容器有明确的宽高(或占满父容器)
↓
第3步:确认主轴是什么:
- Column → 主轴是垂直方向 → justifyContent 管上下
- Row → 主轴是水平方向 → justifyContent 管左右
↓
第4步:确认你用的属性对不对:
- Column 的 alignItems → HorizontalAlign(左中右)
- Row 的 alignItems → VerticalAlign(上中下)
示例:垂直水平都居中
typescript
// 方式1:Column + justifyContent + alignItems
Column() {
Text('居中')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center) // 主轴(垂直)居中
.alignItems(HorizontalAlign.Center) // 交叉轴(水平)居中
// 方式2:Stack + 对齐(后面会讲 Stack 布局)
问题2:文字过长导致布局错乱,怎么处理?
现象 :
文字太长,把容器撑开了,或者超出了边界。
解决方案:
方案1:maxLines + textOverflow(截断显示省略号)
typescript
Text('很长很长的文字很长很长的文字')
.maxLines(1)
.textOverflow(TextOverflow.Ellipsis)
.width(200) // 要有宽度才知道什么时候溢出
方案2:layoutWeight(在 Row 中占满剩余空间)
typescript
Row() {
Text('标签:')
.fontSize(14)
Text('很长很长的内容,可能显示不下')
.fontSize(14)
.maxLines(1)
.textOverflow(TextOverflow.Ellipsis)
.layoutWeight(1) // 占满剩余空间,不会把右边挤出去
}
.width('100%')
方案3:自动换行(多行显示)
typescript
Text('很长很长的文字,会自动换行显示的,不用担心')
.fontSize(14)
.lineHeight(22)
.width('100%')
选择建议:
| 场景 | 方案 |
|---|---|
| 列表项标题 | 单行省略号(方案1) |
| 卡片简介 | 2-3行省略号(方案1) |
| 标签+值,值可能很长 | layoutWeight + 省略号(方案2) |
| 正文内容 | 自动换行(方案3) |
问题3:Image 图片变形了,怎么回事?
现象 :
图片显示出来是扁的或者被拉伸了,变形了。
原因 :
默认的 objectFit 是 ImageFit.Auto,某些情况下会变形。或者你设置的宽高比和图片原始比例不一致。
解决方法:
typescript
// 大多数场景:Cover(铺满,保持比例,超出裁剪)
Image(src)
.width(100)
.height(100)
.objectFit(ImageFit.Cover) // 不变形,填满,可能裁剪
// 需要完整显示:Contain
Image(src)
.width(100)
.height(100)
.objectFit(ImageFit.Contain) // 不变形,完整显示,可能有空白
💡 99% 的图片变形问题都可以通过设置正确的 objectFit 解决。如果你不知道用什么,先用 Cover 试试,大多数时候它都是对的。
问题4:布局嵌套太深了,会不会影响性能?
现象 :
为了实现复杂布局,Column 里面套 Row,Row 里面又套 Column,嵌套了五六层。
答案:适度的嵌套不会有明显的性能问题,但要注意不要过度嵌套。
ArkUI 的布局性能:
ArkUI 的布局引擎做了大量优化,一般来说:
- 3-5 层嵌套:完全没问题
- 5-10 层嵌套:大多数场景也没问题
- 10 层以上:可能需要考虑优化
优化嵌套的技巧:
- 用 Flex 代替不必要的容器
typescript
// ❌ 嵌套太深
Column() {
Row() {
Text('内容')
}
.width('100%')
.justifyContent(FlexAlign.Center)
}
// ✅ 直接设置 Text 的 textAlign
Text('内容')
.width('100%')
.textAlign(TextAlign.Center)
- 合理使用 padding 和 margin
不要为了加间距而套一层容器,直接用 padding 或 margin。
- 列表项的嵌套控制在 4-5 层以内
列表是性能敏感区域,每个卡片的布局要尽量扁平。
⚠️ 不要过早优化:先把功能做对,再考虑性能。大多数时候,嵌套深一点根本感觉不到。等真的出现性能问题了,再用工具分析瓶颈。
问题5:怎么让文字和图标垂直居中对齐?
现象 :
Row 里面左边是图标,右边是文字,但文字和图标总是对不齐。
解决方法:
typescript
Row({ space: 8 }) {
Image($r('app.media.icon'))
.width(20)
.height(20)
Text('文字')
.fontSize(16)
}
.alignItems(VerticalAlign.Center) // 交叉轴(垂直)居中
如果还是不对齐?
可能是因为文字有行高,或者图标周围有空白。可以微调:
typescript
// 用 margin 微调
Text('文字')
.fontSize(16)
.margin({ top: 2 }) // 往下挪 2vp
或者用 offset:
typescript
Text('文字')
.fontSize(16)
.offset({ y: 2 }) // 向下偏移 2vp
💡 经验 :图标和文字对齐,90% 的情况用
alignItems(VerticalAlign.Center)就够了。如果差 1-2 像素,用 margin 或 offset 微调一下。字体、图标设计不同,可能会有一点点视觉偏差,正常的。
📝 本章小结
核心知识点
本文从基础到实战,深度解析了五个最常用的基础组件:
1. Text 组件
- 基础属性:fontSize、fontColor、fontWeight
- 文本截断:maxLines + textOverflow(Ellipsis 最常用)
- 文本对齐:textAlign(要配合 width 使用)
- 行高字距:lineHeight、letterSpacing
- 富文本:Span 组件
2. Image 组件
- 图片来源: r ( ) 、 r()、 r()、rawfile()、网络 URL
- 缩放模式:Cover(填满裁剪)、Contain(完整显示)、Fill(拉伸变形)
- 圆角图片:borderRadius
- 性能优化:合适尺寸、懒加载、缓存
3. Button 组件
- 三种类型:Capsule、Circle、Normal
- 按钮状态:正常、按下、禁用、加载中
- 自定义按钮:在 Button 里放自定义内容
- 状态驱动:按钮样式由状态变量控制
4. Column & Row 布局
- 主轴 vs 交叉轴:排列方向是主轴,另一个是交叉轴
- justifyContent:主轴对齐(FlexAlign)
- alignItems:交叉轴对齐(Column 用 HorizontalAlign,Row 用 VerticalAlign)
- space:子组件间距
- layoutWeight:权重分配剩余空间(超级常用)
5. 实战:民族卡片
- 结构分解:先拆分布局,再写代码
- 视觉层次:字号、字重、颜色形成层次
- 间距节奏:大间距分隔大块,小间距分隔小块
- 代码组织:用 @Builder 拆分,清晰易维护
最佳实践总结
✅ 先想结构,再写代码
拿到设计稿,先别急着写代码
1. 分析布局结构:哪些地方用 Row,哪些用 Column
2. 拆解组件:哪些可以抽成 @Builder,哪些要做自定义组件
3. 确定状态:哪些数据是变化的,用 @State
想清楚了再动手,事半功倍
✅ objectFit 选 Cover 准没错
typescript
// 90% 的场景用 Cover 都是对的
Image(src)
.width(100)
.height(100)
.objectFit(ImageFit.Cover)
.borderRadius(8)
✅ 文字截断三件套:width + maxLines + textOverflow
typescript
// 缺一不可
Text('长文本')
.width(200) // 1. 要有宽度
.maxLines(1) // 2. 最大行数
.textOverflow(TextOverflow.Ellipsis) // 3. 溢出显示省略号
✅ 左右布局:固定 + layoutWeight(1)
typescript
// 左固定,右占满,写了一万遍的经典模式
Row({ space: 12 }) {
Image(cover)
.width(100)
.height(100)
Column() {
// 文字内容...
}
.layoutWeight(1) // 占满剩余空间
}
✅ 对齐搞不清?先找主轴
先问自己:子组件是怎么排的?
上下排 → Column → 主轴是垂直 → justifyContent 管上下
左右排 → Row → 主轴是水平 → justifyContent 管左右
另一个方向就是交叉轴 → alignItems 管
✅ 布局嵌套不要怕,但要有度
3-5 层嵌套:正常,大胆写
5-10 层嵌套:看情况,一般也没问题
10 层以上:考虑优化一下
不要为了减少嵌套把代码写得很难懂
可读性 > 嵌套层级(在合理范围内)
下一步预告
在下一篇文章中,我们将:
- 🔄 深入学习 ArkUI 的状态管理体系
- 📦 掌握 @State、@Prop、@Link 的区别与使用场景
- 🔗 学习 @Provide/@Consume 跨组件层级传值
- 💾 掌握 @StorageLink 持久化状态
- 🏪 理解状态提升与单向数据流原则
- 🚀 为复杂页面的状态管理打下基础
🔗 相关链接
- 项目源码 : GitCode 仓库
- Text 组件 : 官方文档
- Image 组件 : 官方文档
- Button 组件 : 官方文档
- Column 组件 : 官方文档
- Row 组件 : 官方文档
- Flex 布局 : 官方文档
💡 提示:基础组件是 UI 开发的基石。不要觉得简单就掉以轻心------真正的高手,是能把简单的东西用得出神入化。一个精致的界面,不是靠多么复杂的组件,而是靠每一个文字、每一张图片、每一个间距的精雕细琢。多写多练,多观察优秀的设计,你的 UI 实现能力会越来越强。