Flex 弹性布局是 ArkUI 里最常用的容器,也是最容易"感觉会了"的一个。健康菜谱助手 0.9 版本重构时,我在一个菜谱卡片里叠了四层 Flex,界面看着没问题,真机上滚动帧率却掉了三成;另一处用了Flex 嵌套 Scroll 的写法,内容稍多直接把页面撑到无法滚动。这一篇把 Flex 嵌套相关的四个真实问题完整记录下来:测量二连跳、自适应高度失效、嵌套滚动冲突、以及 Flex 之外的替代选择。所有结论都在 API 26 真机上验证过。

Flex嵌套问题全景

一、问题一:四层嵌套 Flex 的测量二连跳

菜谱卡片的原始结构是这样的:外层 Flex 做横向排布,左边封面图,右边一个纵向 Flex 装菜名和标签,标签行又是一个横向 Flex,标签行里的每个标签自己还是一个小 Flex。四层嵌套,每层都写着 alignSelf 和 flexShrink。界面渲染结果是对的,但滚动列表时帧率异常。用 DevEco 的 ArkUI Inspector 看布局树才发现问题:每次滚动触发重新布局,四层 Flex 逐层做测量,子组件的尺寸变化又反向影响父层,同一帧内出现两次完整的测量传递——这就是帧率下降的主因。

测量二连跳的本质是 Flex 的自适应特性与嵌套的组合反应:每层 Flex 都要等子组件量完才知道自己多大,层数一多,测量链路指数变长。修复不是消灭 Flex,而是砍层级:标签行的每个标签其实不需要单独的 Flex,Text 组件直接放进行级 Flex 即可;纵向 Flex 与外层横向 Flex 之间的中间层,改用固定宽度约束,让测量不再依赖子组件回传。

结构嵌套层数滑动帧率
原始四层 Flex4掉帧明显
砍掉标签独立 Flex3接近满帧
中间层改固定宽2满帧稳定

二、嵌套层数的判断标准:两问自查

写 Flex 之前先问自己两个问题。第一问:这一层有没有做"排布"以外的事?如果一层 Flex 只是为了给子组件加个对齐或者间距,大概率可以用 padding、margin 或容器属性替代——替代之后少一层测量。第二问:这一层的尺寸是否依赖子组件回传?凡是写着 adaptHeight 或者没有显式宽高的 Flex,都会把测量链拉长一层;能确定尺寸就显式给尺寸,让这一层从测量链里退出来。我的经验值:同一条分支上 Flex 不超过两层,超过两层一定有一层可以合并或者替换。

// 反例:三层嵌套,每层都在等子组件
Flex() {
  Flex({ direction: FlexDirection.Column }) {
    Flex({ direction: FlexDirection.Row }) { Text(name); Text(tag) }
    Text(desc)
  }
}
// 正例:一层 Flex + 固定约束
Flex({ direction: FlexDirection.Row }) {
  Image(cover).width(96).height(96)
  Column() {
    Text(name)
    Text(tag)
  }.layoutWeight(1)
  Text(desc).width(120)
}.height(120)

三、问题二:自适应高度在某些机型失效

菜谱详情页的简介区域用了"不设高度的纵向 Flex 包多行 Text",预期是文字多高容器就多高。开发机上一切正常,某台旧测试机上简介只显示了两行,下面被截断。排查一天,结论与 Flex 本身无关:旧系统的 Text 在无约束父容器里测量返回的高度偏小,父 Flex 拿到偏小的值后就地截断。规避方法反而简单——给这一层 Flex 加上 constraintSize 的最小高度下限,或者干脆换成 Column:Column 对子组件的测量策略更直接,不依赖二次回传。这个案例让我确立了本项目的布局选型基调:能用 Column/Row 的场景不用 Flex,Flex 只在需要权重分配(flexGrow/flexShrink)或者主轴对齐时出场。

// 失效场景的规避:显式最小高度
Flex({ direction: FlexDirection.Column }) {
  Text(intro).fontSize(14).lineHeight(22)
}.constraintSize({ minHeight: 66 })

// 更稳妥的替代
Column() {
  Text(intro).fontSize(14).lineHeight(22)
}.alignItems(HorizontalAlign.Start)

四、问题三:Flex 嵌套 Scroll 的滚动冲突

收录页顶部有一个横向 Flex 的分类条,页面主体是纵向 Scroll,分类条想在自己的范围内横向滚动。最初的写法把横向 Flex 直接套了 Scroll 组件,真机上一划,纵向页面跟着晃,横向列表抖动,两个滚动手势打架。解法分两步:横向分类条不套 Scroll,换成带 scrollable 属性的 List——List 对子项的复用和手势隔离都做了处理,横向滚动天然独立;再给 List 显式设置 nestedScroll 选项,声明它与外层 Scroll 的嵌套关系,让手势优先级有了明确约定。改完之后两个方向各自顺畅,没有再出现过抢手势。

Scroll() {
  Column() {
    List({ space: 8 }) {
      ForEach(cats, (c) => {
        ListItem() { Text(c).padding(12) }
      }, (c) => c)
    }.listDirection(Axis.Horizontal).scrollBar(BarState.Off)
    // 主体内容...
  }
}.scrollBar(BarState.Auto)

嵌套滚动手势隔离

五、Flex 与 Row/Column 的选型对照

三个问题解决后,我把 Flex 与 Row/Column 的适用边界整理成一张表,贴在项目 README 里。核心结论:Row 和 Column 是"定好方向的线性排布",测量路径短、行为可预期;Flex 是"弹性空间分配器",它的 flexGrow、flexShrink、justifyContent 是不可替代的,但每多一层嵌套就多一份测量成本。选型的判断式很简单:需要权重或两端对齐用 Flex,其余一律 Row/Column。按照这个标准重构整个应用后,布局相关的性能问题归零。

需求推荐容器理由
权重分配(1:2:1)Flex + layoutWeightRow/Column 无权重语义
主轴两端对齐Flex + justifyContent语义明确
普通纵向列表项Column测量路径短
普通横向排布Row行为可预期
可滚动分类条List(横向)手势隔离与复用

六、间距的写法:space 与 margin 的性能差

Flex 有专用的 space 参数分配主轴间距,比给每个子组件写 margin 少一层内边距计算。改造时我把所有"子组件逐个 margin"的标签行换成了 Flex 的 space,列表滑动时的布局耗时有了可感知的下降。这里有个细节:space 只管主轴,交叉轴间距仍需要 margin 配合;两者混用时先在 Flex 层统一主轴间距,再对个别组件做交叉轴微调,顺序反了会出现间距叠加。

Flex({ wrap: FlexWrap.Wrap, space: { main: LengthMetrics.vp(8), cross: LengthMetrics.vp(8) } }) {
  ForEach(tags, (t) => {
    Text(t).fontSize(12).padding({ left: 10, right: 10, top: 4, bottom: 4 })
      .backgroundColor('#F2F3F5').borderRadius(10)
  }, (t) => t)
}

七、wrap 换行的测量代价与行数控制

标签行需要自动换行,Flex 的 wrap 参数直接支持。但 wrap 模式下 Flex 无法预知行数,测量开销比单行模式高,数据里标签多的菜谱(十几个标签)滑动帧率又下来了。最终方案是业务侧限制:标签展示最多两行,超出的收纳进"展开"按钮,点开后整行展示。限制行数不是性能妥协的权宜之计——两行以内是卡片的信息密度上限,十个标签本来就超出了用户在一屏里处理的范围。性能优化和产品设计在这里是同一件事。

八、验证方法:布局树的三个观察点

所有结论都要能被验证。ArkUI Inspector 里看布局树,我固定看三个点:一是组件实例总数,同样一屏内容,改造前后实例数应该明显下降;二是测量层级,Flex 嵌套层数直接数;三是布局耗时,Inspector 的时间线里每帧的 measure+layout 段落长度。三个点看完,布局问题有没有修好不是凭感觉说的。这三个观察点后来成了团队代码评审的固定动作,新同事提的布局代码先过一遍布局树再合入。

布局树观察要点

九、总结:Flex 使用的四条军规

把这篇的结论压缩成四条军规:同分支 Flex 不超过两层;能用 Column/Row 不用 Flex,Flex 留给权重和对齐;可滚动的横条用 List 不用 Flex 套 Scroll;间距优先用 space 而不是逐个 margin。四条军规执行后,布局层的性能问题在后续两个版本里都没有再出现。布局是最容易"看着没问题"的领域——界面对了不代表代价合理,Inspector 里的数字才是裁决者。每写一层 Flex 前多问一句"这层有必要吗",比事后优化省十倍功夫。

参考资料:

  • 华为开发者:Flex 弹性布局
  • 华为开发者:线性布局 Row/Column
  • 华为开发者:ArkUI Inspector 使用指南

#鸿蒙 #HarmonyOS 7(API 26)

十、真实案例复盘:一次由 Flex 引发的显示错位

除了性能,嵌套还带来过一次显示 bug。菜谱详情页的食材清单是纵向 Flex 套横向 Flex 的两列结构,某次数据更新后,第二列的整体位置向下漂了半个行高。排查发现是第一列某个食材名包含英文长单词,交叉轴对齐方式让这一行的基线发生了变化,两列没有用统一的基线约定。修复时给两列统一加了 alignItems 并把长词所在的 Text 加了 maxLines 和文本自适应缩放。这个案例说明嵌套结构的对齐问题会随内容变化暴露:数据可控时看不出问题,真实数据千奇百怪,每一层嵌套的对齐语义都要显式声明,不能依赖默认值。

Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Start }) {
  Column() { ForEach(left, (f) => Text(f).maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })) }.layoutWeight(1)
  Column() { ForEach(right, (f) => Text(f)) }.layoutWeight(1)
}

处理完这个 bug 之后,我给布局代码加了一条评审规则:凡是有 alignItems 或 justifyContent 的容器,评审时必须说明选择该对齐方式的业务原因,说不出原因就统一成项目默认值。默认值的存在让九成对齐场景不再需要决策,剩下需要特殊对齐的地方反而清晰醒目。约定优于配置,在布局这件事上同样成立。

十一、写给正在踩坑的你:排查 Flex 问题的固定动作

把遇到 Flex 问题时我的固定排查动作列出来,照做能省掉大部分弯路。第一步,打开 ArkUI Inspector 截图布局树,数一数 Flex 的层数和组件实例数,性能问题在这一步就能定位;第二步,给可疑的 Flex 层显式加上背景色,真机跑一遍,哪一层的高度和宽度不符合预期一眼可见,比看代码快得多;第三步,检查每一层的主轴对齐与交叉轴对齐是否显式声明,把依赖默认值的地方改成显式配置;第四步,如果涉及滚动,确认嵌套滚动的两层各自的 scrollable 与 nestedScroll 配置。四步走完仍未解决,再考虑用最小复现工程隔离问题——绝大多数情况走不到这一步。

这一篇写完,Flex 嵌套的坑基本盘完了:两个性能问题、一个失效场景、一个显示错位,外加一套验证方法与四条军规。布局知识的特殊之处在于它太基础,基础到大家都觉得自己会,而真实的差距恰恰藏在每个人都以为会的地方。愿这份记录能让你的布局少走几步弯路。

补充一个容易忽略的细节:Flex 的 direction 参数在不同场景下的默认值并不相同,部分容器包一层之后方向语义会发生细微变化。我的做法是所有 Flex 都显式写 direction,哪怕值与默认一致——显式声明的代码在三个月后回看仍然能一眼判断意图,依赖记忆和默认值的代码每次都要重新推理。这个习惯让我的布局代码在多次重构中保持了可读性,也建议每个写布局的同学都采纳。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐