2、弹性伸缩布局:深入理解flexGrow、flexShrink、flexBasis属性,实现子组件按比例伸缩。

好,咱们接着聊弹性伸缩布局。

上一章我们讲了 Flex 的基本概念,说白了就是「怎么排」。这一章我们深入一点,聊聊「怎么变」。

你想想看,屏幕有大有小,内容有多有少。如果每个子组件都固定死宽度,那肯定不行。这时候就需要三个关键属性:flexGrowflexShrinkflexBasis。我刚开始接触鸿蒙开发时,也经常搞混它们。后来踩了几个坑,才真正理解。

2.1 flexBasis:组件的「理想尺寸」

flexBasis 是基础尺寸。你可以把它理解为「在没有伸缩之前,我希望这个组件占多大」。它有点像 width,但优先级更高。

核心要点:

  • 如果主轴是水平方向,flexBasis 控制宽度
  • 如果主轴是垂直方向,flexBasis 控制高度
  • 默认值是 auto,此时会参考组件的 widthheight
// 示例:设置 flexBasis
Flex({
  direction: FlexDirection.Row
}) {
  Text('A')
    .flexBasis(100)  // 理想宽度 100vp
    .height(50)
    .backgroundColor(Color.Red)

  Text('B')
    .flexBasis(200)  // 理想宽度 200vp
    .height(50)
    .backgroundColor(Color.Green)
}

嗯,这里要注意:flexBasis 只是「理想值」。如果容器空间不够或者有富余,它就会被 flexGrowflexShrink 调整。

2.2 flexGrow:空间多了,怎么分?

当所有子组件的 flexBasis 加起来,比容器小的时候,多余的空间怎么分配?flexGrow 说了算。

我在项目中遇到过这样一个场景:一个底部导航栏,三个按钮。我希望它们平分整个宽度。当时我直接给每个按钮设了 flexGrow(1),完美解决。

// 三个按钮平分空间
Flex({
  direction: FlexDirection.Row,
  justifyContent: FlexAlign.SpaceBetween
}) {
  Text('首页')
    .flexGrow(1)  // 占 1 份
    .height(50)
    .backgroundColor(Color.Blue)

  Text('发现')
    .flexGrow(1)  // 占 1 份
    .height(50)
    .backgroundColor(Color.Orange)

  Text('我的')
    .flexGrow(1)  // 占 1 份
    .height(50)
    .backgroundColor(Color.Pink)
}

为什么会这样?因为 flexGrow 的值代表「权重」。三个都是 1,那就各拿三分之一。如果我把「首页」改成 flexGrow(2),那它就拿走一半,剩下两个各拿四分之一。

我的习惯:

如果想让某个组件「自适应填充剩余空间」,给它设 flexGrow(1) 就够了。其他组件保持默认 flexGrow(0),它们就不会变大。

2.3 flexShrink:空间不够,谁先缩?

反过来,当所有子组件的 flexBasis 加起来,比容器大的时候,就需要压缩了。flexShrink 决定谁先缩、缩多少。

默认情况下,每个组件的 flexShrink 都是 1。也就是说,大家按比例一起缩。但有时候我们不想让某个组件被压缩。

我曾经做过一个聊天列表,左边是头像,右边是昵称。昵称可以很长,但我不想让头像被压扁。解决办法就是给头像设 flexShrink(0)

// 头像不压缩,昵称自适应
Flex({
  direction: FlexDirection.Row
}) {
  // 头像:固定大小,绝不压缩
  Image($r('app.media.avatar'))
    .width(40)
    .height(40)
    .flexShrink(0)

  // 昵称:可以压缩
  Text('这是一个非常非常长的用户昵称')
    .flexShrink(1)
    .maxLines(1)
    .textOverflow({ overflow: TextOverflow.Ellipsis })
}

注意:

flexShrink 的压缩比例是基于 flexBasis 的。如果一个组件 flexBasis 是 200,另一个是 100,且 flexShrink 都是 1,那么需要压缩 60vp 时,第一个组件会缩 40vp,第二个缩 20vp。说白了,基础大的缩得多。

2.4 三者配合:一个完整的例子

我们来看一个实际布局。一个卡片,左边是图标,中间是文字,右边是操作按钮。我希望:

  • 图标固定大小,不伸缩
  • 文字自适应,能撑就撑,能缩就缩
  • 按钮固定大小,不伸缩
Flex({
  direction: FlexDirection.Row,
  alignItems: ItemAlign.Center
}) {
  // 图标:固定 40vp
  Image($r('app.media.icon'))
    .width(40)
    .height(40)
    .flexShrink(0)
    .flexGrow(0)

  // 文字:自适应
  Text('这是一段可能会很长的描述文字')
    .flexGrow(1)
    .flexShrink(1)
    .flexBasis(0)  // 从 0 开始计算,完全由 grow/shrink 决定

  // 按钮:固定宽度
  Button('操作')
    .width(80)
    .flexShrink(0)
    .flexGrow(0)
}

你看,这里我给文字设了 flexBasis(0)。这意味着它的「理想尺寸」是 0,然后通过 flexGrow(1) 去占满剩余空间。如果空间不够,它也会通过 flexShrink(1) 去压缩。而图标和按钮完全不受影响。

2.5 避坑指南

最后,分享几个我踩过的坑:

  • flexBasis 和 width 同时设置?flexBasis 为准。我建议只用 flexBasis,别混用,容易乱。
  • flexGrow 和 flexShrink 同时为 0? 那这个组件就是「死尺寸」,不增不减。适合固定宽高的元素。
  • 子组件内容溢出? 记得配合 maxLinestextOverflow,否则文字会撑破容器。

一句话总结:

flexBasis 定基调,flexGrow 管分配,flexShrink 管压缩。三者配合,就能实现各种弹性伸缩效果。

下一章,我们会聊聊更复杂的场景——嵌套 Flex 和交叉轴对齐。到时候你会发现,掌握了这三个属性,很多布局问题都能迎刃而解。