HarmonyOS应用<民族图鉴>开发第6篇:基础组件——Text/Image/Button/Column/Row深度解析

📖 引言

经过前两篇的学习,我们已经掌握了 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 就是显示图片,有什么好学的?但实际上:

  1. 基础组件用得最多:一个页面里 90% 的组件都是基础组件
  2. 细节决定体验:文字截断方式、图片缩放模式、间距设置,这些细节积累起来就是"精致感"
  3. 性能影响大:列表里的每一个卡片都用这些组件,优化一个卡片,整个列表都会变快
  4. 高级组件的基础:复杂组件都是由基础组件组合而来的

「民族图鉴」的基础组件应用

「民族图鉴」项目中有一个非常典型的组件------民族卡片(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 不截断,全部显示 文本可以换行显示

💡 常见坑maxLinestextOverflow 要配合使用。只设 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 图片性能优化

图片是最占用内存的资源之一,优化很重要。

优化技巧

  1. 合适的尺寸:不要用 2000px 的大图显示在 100px 的位置
  2. 按需加载:列表里的图片滚动到可视区域再加载(懒加载)
  3. 使用合适的格式:WebP > PNG > JPEG(根据场景选择)
  4. 内存管理:页面销毁时释放大图片
  5. 缓存:已加载的图片缓存起来,避免重复加载

「民族图鉴」中的优化实践

  • 封面图使用合适的分辨率(不是越大越好)
  • 列表使用 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 标签、筛选

💡 最佳实践

  1. 不要依赖预置 type:实际项目中几乎都用自定义样式
  2. 封装按钮组件:把常用的按钮样式封装成组件,保持全项目统一
  3. 状态要完整:正常、按下、禁用、加载中,四种状态都要有
  4. 点击区域足够大:按钮高度至少 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) 与主轴垂直的方向 水平方向(左右) 垂直方向(上下)

为什么要区分主轴和交叉轴?因为 justifyContentalignItems 是针对不同轴的:

  • 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 三句话心法

  1. 先固定,后自适应:固定宽的先排,自适应的(layoutWeight)后分配
  2. 主轴 justify,交叉轴 align:搞不清就先问"子组件怎么排的"
  3. 装不下就加 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 层以上:可能需要考虑优化

优化嵌套的技巧

  1. 用 Flex 代替不必要的容器
typescript 复制代码
// ❌ 嵌套太深
Column() {
  Row() {
    Text('内容')
  }
  .width('100%')
  .justifyContent(FlexAlign.Center)
}

// ✅ 直接设置 Text 的 textAlign
Text('内容')
  .width('100%')
  .textAlign(TextAlign.Center)
  1. 合理使用 padding 和 margin

不要为了加间距而套一层容器,直接用 padding 或 margin。

  1. 列表项的嵌套控制在 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 持久化状态
  • 🏪 理解状态提升与单向数据流原则
  • 🚀 为复杂页面的状态管理打下基础

🔗 相关链接


💡 提示:基础组件是 UI 开发的基石。不要觉得简单就掉以轻心------真正的高手,是能把简单的东西用得出神入化。一个精致的界面,不是靠多么复杂的组件,而是靠每一个文字、每一张图片、每一个间距的精雕细琢。多写多练,多观察优秀的设计,你的 UI 实现能力会越来越强。