《HarmonyOS技术精讲-UI开发》第3篇:布局系统深入

从"不知道用哪个布局"说起

HarmonyOS NEXT 开发里,布局容器选型经常让人犯难。官方文档把 Column、Row、Flex、Stack 的 API 列得很清楚,但实际写页面时,到底哪个容器适合什么场景,参数怎么配才能达到预期效果,官方没有给太多指导。

很多刚接触 ArkUI 的人容易犯一个错误:所有页面都用 Column 套 Row,再用一堆固定宽高去硬怼。代码能跑,但一旦要适配不同屏幕尺寸,或者要修改排版,维护成本直接飙升。

这一篇把几个核心容器梳理一遍,结合三个最常见的布局场景,看看实际项目中怎么用更稳。

布局容器的定位与差异

这几个容器解决的问题不同:

容器 核心特性 适用场景 不适用场景
Column 垂直排列子组件 表单、列表、信息流 需要横向滚动的内容
Row 水平排列子组件 导航栏、标签栏、按钮组 内容垂直排布
Flex 弹性布局,可控制伸缩 等分布局、自适应卡片 简单垂直/水平排列时杀鸡用牛刀
Stack 层叠定位 悬浮按钮、遮罩层、水印 内容需要回避的布局

Stack 的使用频率远低于前三个,但它的层叠能力在某些场景下不可替代。Flex 很多人当进阶版 Row/Column 用,其实它的核心价值在于 flexGrow 和 flexShrink,这两个参数能让布局自适应,减少很多硬编码宽高。

环境说明

text 复制代码
DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:手机

三个核心示例

1. 水平垂直居中 ------ Column + Row 组合

这个需求在界面中很常见:弹窗居中、加载动画居中、欢迎页面居中。

typescript 复制代码
// 页面居中布局示例
@Entry
@Component
struct CenterLayoutDemo {
  build() {
    // 外层容器填满屏幕
    Column() {
      Column() {
        Text('居中的内容')
          .fontSize(20)
          .fontColor(Color.White)
          .padding(20)
      }
      .width(200)
      .height(200)
      .backgroundColor(0x317AFF)
      .borderRadius(12)
      .justifyContent(FlexAlign.Center) // 主轴(垂直)居中
      .alignItems(HorizontalAlign.Center) // 交叉轴(水平)居中
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }
}

参数说明

  • justifyContent:主轴对齐方式。Column 的主轴是垂直方向,传入 FlexAlign.Center 让子组件在垂直方向居中。
  • alignItems:交叉轴对齐方式。Column 的交叉轴是水平方向,传入 HorizontalAlign.Center 让子组件在水平方向居中。

注意事项

  • 内外两层都用 Column 是实现整个页面居中的常用写法。如果只在外层设置,子组件内部的内容可能不会居中。
  • justifyContentalignItems 是针对容器子组件的对齐,不是针对容器自身。

2. 等分布局 ------ Flex + flexGrow

等分布局在底部导航、主页选项卡、多列卡片中很常见。用固定宽度实现等分会导致适配困难,Flex 的弹性伸缩特性专门解决这个问题。

typescript 复制代码
// 三等分布局示例
@Entry
@Component
struct EqualDivideLayoutDemo {
  @State private tabNames: string[] = ['首页', '发现', '我的']

  build() {
    Column() {
      Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceEvenly }) {
        ForEach(this.tabNames, (item: string, index: number) => {
          Column() {
            Text(item)
              .fontSize(16)
              .fontColor(Color.Black)
              .lineHeight(40)
          }
          .width(80) // 固定宽度,不会伸缩
          .height(40)
          .backgroundColor(0xF5F5F5)
        })
      }
      .width('100%')
      .height(40)
      .margin({ bottom: 20 })

      // 正确的等分布局:使用 flexGrow
      Flex({ direction: FlexDirection.Row }) {
        ForEach(this.tabNames, (item: string) => {
          Column() {
            Text(item)
              .fontSize(16)
              .fontColor(Color.Black)
              .lineHeight(40)
          }
          .layoutWeight(1) // 使用 layoutWeight 实现均匀分配
          .height(40)
          .backgroundColor(0xE0E0E0)
        })
      }
      .width('100%')
      .height(40)
    }
    .width('100%')
    .padding(10)
  }
}

参数说明

  • layoutWeight:ArkUI 中实现等分更推荐用 layoutWeight 而不是 flexGrowlayoutWeight 是 ArkUI 封装的便捷属性,直接按比例分配剩余空间,比手动设置 flexGrow 更稳定。
  • SpaceEvenly:项目间的间距相等,且首尾也有间距。

为什么推荐 layoutWeight 而不是 flexGrow

  • 实际开发中发现,flexGrow 在嵌套 Flex 容器时,有时会出现计算误差,导致布局错位。layoutWeight 的实现更直接,没有额外的折行计算逻辑,稳定性更好。
  • flexShrink 类似,但实际项目中用到的情况较少,除非你明确知道子组件在不同屏幕下会溢出。

3. 层叠布局 ------ Stack

层叠布局用来实现浮层、水印、头像上的红点提示等效果。Stack 的核心能力是让子组件在 Z 轴方向上重叠。

typescript 复制代码
// 头像+红点提示的层叠布局
@Entry
@Component
struct StackLayoutDemo {
  @State private hasNewMessage: boolean = true

  build() {
    Column() {
      Stack() {
        // 底层:头像
        Column() {
          Text('U')
            .fontSize(40)
            .fontColor(Color.White)
        }
        .width(80)
        .height(80)
        .backgroundColor(0x317AFF)
        .borderRadius(40)
        .alignSelf(ItemAlign.Center)

        // 顶层:未读红点
        if (this.hasNewMessage) {
          Column() {
            Text('3')
              .fontSize(12)
              .fontColor(Color.White)
              .lineHeight(18)
              .textAlign(TextAlign.Center)
          }
          .width(18)
          .height(18)
          .backgroundColor(Color.Red)
          .borderRadius(9)
          .position({ x: 62, y: -2 }) // 定位到头像右上角
        }
      }
      .width(80)
      .height(80)
      .margin({ top: 50 })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Start)
    .alignItems(HorizontalAlign.Center)
  }
}

参数说明

  • position:设置子组件相对于 Stack 容器的偏移。x 和 y 分别表示水平和垂直偏移量。
  • 红点组件使用 if 条件渲染,状态变化时 UI 自动刷新。

注意事项

  • Stack 默认子组件按添加顺序层叠,后添加的在顶层。
  • position 是相对 Stack 容器的坐标,不是相对父组件。如果要实现"相对于某个子组件定位",需要在外层再包一层 Stack。
  • 红点数据用 @State 管理,状态变化时组件会重建,这是 ArkUI 的刷新机制决定的。

常见踩坑

坑1:justifyContentalignItems 在 Column 和 Row 中的方向混淆

现象 :在 Column 中设置 alignItems(HorizontalAlign.Center) 后,子组件水平方向居中了,但在 Row 中设置同样的属性,布局却完全不对。

原因 :ArkUI 的 justifyContentalignItems 方向是跟随容器主轴(main axis)和交叉轴(cross axis)变化的。Column 的主轴是垂直方向,交叉轴是水平方向;Row 正好相反。所以 Row 中要水平居中,需要设置 justifyContent(FlexAlign.Center) 而不是 alignItems

解法 :写布局前先确认容器的主轴方向。有一个简单的记忆方法:justifyContent 控制的是"排列方向"上的对齐,alignItems 控制的是"垂直排列方向"上的对齐。

坑2:layoutWeightwidth 同时设置时的优先级问题

现象 :给某个子组件同时设置 layoutWeight(1)width(80),子组件宽度固定为 80,不会均匀分配剩余空间。

原因layoutWeight 只会在子组件没有设置固定宽高(或 width/heightauto)的情况下才生效。一旦设置了具体数值,layoutWeight 会被忽略。

解法 :使用 layoutWeight 时,不要给子组件设置固定宽度。如果需要预设最小值,可以用 constraintSize 设置最小宽度,但宽度不能写死。

最佳实践

  1. 不要用 Column/Row 硬编码所有布局 。等分布局优先用 layoutWeight,层叠布局用 Stack,弹性的用 Flex。在 build() 里频繁使用固定宽高,后续屏幕适配会很痛苦。

  2. layoutWeightflexGrow 更稳定 。在嵌套 Flex 的场景下,flexGrow 有时会出现百分比计算误差,而 layoutWeight 直接按比例分配剩余空间,很少出问题。

  3. Stack 的 position 定位不要过度依赖绝对值 。设备分辨率差异大时,绝对值会偏移。如果需要相对定位(比如红点固定在头像右上角),可以考虑用 offset 配合百分比,或者在外层包一个 RelativeContainer(HarmonyOS 6.1.0 新增)。

Demo 入口

typescript 复制代码
// 完整布局示例入口
@Entry
@Component
struct LayoutDemoPage {
  build() {
    Scroll() {
      Column() {
        // 示例1:居中布局
        CenterLayoutDemo()
        Divider().height(2).margin(20)
        // 示例2:等分布局
        EqualDivideLayoutDemo()
        Divider().height(2).margin(20)
        // 示例3:层叠布局
        StackLayoutDemo()
      }
      .width('100%')
      .padding(10)
    }
  }
}

FAQ

Q:为什么 Column 里用 justifyContent(FlexAlign.Center) 子组件没有居中?

A:检查一下子组件的高度是否撑满了容器。如果子组件高度超过容器高度,居中不会有视觉上的居中的感觉。建议给容器设置固定高度或者 height('100%')

Q:layoutWeightflexGrow 效果一样吗?

A:从功能上讲,两者都用于分配剩余空间。但在 ArkUI 中,layoutWeight 是更直接的 ArkTS 属性,底层实现比 flexGrow 更稳定,推荐优先使用 layoutWeight

Q:Stack 里的子组件可以通过触摸事件区分层级吗?

A:可以。Stack 容器会按子组件的层叠顺序处理触摸事件。默认情况下,顶层组件会先响应触摸。如果需要底层的组件也能响应,可以通过 hitTestBehavior 属性调整,比如 HitTestMode.None 让组件不参与事件检测。

如果你在 ArkUI 布局中遇到其他奇怪的问题,可以重点检查容器的主轴方向,以及 alignSelfflexGrow 这些属性是否被正确覆盖。官方文档的描述偏向 API 层面,实际运行效果往往需要结合真机验证,不同设备的渲染表现可能会有细微差异。