【ArkUI 练中学】第4课:高级布局与列表性能优化

本节目标

· 掌握 Flex 弹性布局的核心属性 flexBasis、flexGrow、flexShrink 及 FlexWrap 换行布局的用法

· 掌握 Grid 网格布局的 columnsTemplate / rowsTemplate 行列设置方法,理解 fr 单位与 GridItem 的配合

· 掌握 Stack 层叠布局的 alignContent 对齐方式与 zIndex Z 序控制

· 掌握 List 列表布局的基础用法,理解 ListItem 与 ListItemGroup 的组织方式

· 理解长列表性能问题,掌握 LazyForEach 懒加载、cachedCount 预加载、@Reusable 组件复用三大优化手段

· 能根据实际场景选择合适的布局容器,并针对长列表进行性能优化

一、Flex 弹性布局

1.1 Flex 的定位与核心属性

Flex 布局提供灵活的组件排列和对齐能力,可以动态分配容器内的子组件空间,使元素根据可用空间自动扩展或收缩。适用于响应式 UI 布局、动态内容布局、复杂布局实现等场景,能解决传统布局在多设备适配困难、内容变化导致布局错位、复杂对齐需求难以实现等问题。

Flex 布局的核心在于三个属性,它们共同决定了子组件在主轴方向上的尺寸分配行为:

flexBasis:设置组件的基准尺寸,作为布局的初始参考值,优先级高于 width / height。默认值为 'auto',表示组件在主轴方向上的基准尺寸为组件原本的大小。仅作为 Flex、Column、Row 和 DynamicLayout 容器的子组件时支持设置此属性。

flexGrow:定义组件在父容器有剩余空间时的扩展比例,剩余空间按各组件 flexGrow 比例分配。当父容器宽度大于所有子组件总宽度时,超出部分按比例分配给设置了 flexGrow 的子组件。

flexShrink:定义组件在父容器空间不足时的压缩比例,超出的尺寸按各组件 flexShrink 比例分摊。当父容器宽度小于所有子组件总宽度时,超出部分按比例从各子组件中扣除。

三者关系可以概括为:flexBasis 设定基准尺寸,flexGrow 控制扩展行为,flexShrink 控制压缩行为,可单独使用或组合使用。

typescript 复制代码
Flex({ direction: FlexDirection.Row }) {
  Text('固定宽度')
    .width(80)
    .height(50)
    .backgroundColor('#e1dede')

  Text('弹性扩展')
    .flexGrow(1)       // 占满剩余空间
    .height(50)
    .backgroundColor('#c1cbac')

  Text('按比例扩展')
    .flexGrow(2)       // 占剩余空间的2份
    .flexBasis(60)     // 基准尺寸60vp
    .height(50)
    .backgroundColor('#d2cab3')
}
.width('100%')

1.2 FlexWrap 换行布局

Flex 布局支持通过 FlexWrap 属性控制子组件是否换行。FlexWrap.NoWrap 为默认值,不换行;FlexWrap.Wrap 允许换行,从左到右从上到下排列;FlexWrap.WrapReverse 允许换行,但从下到上排列。

FlexWrap 是实现流式布局的关键属性------当标签数量不固定时,无需提前计算一行放几个,设置 FlexWrap.Wrap 即可自动换行。

typescript 复制代码
Flex({
  wrap: FlexWrap.Wrap,
  space: { main: LengthMetrics.px(10), cross: LengthMetrics.px(10) }
}) {
  ForEach(this.tags, (tag: string) => {
    Text(tag)
      .padding({ left: 12, right: 12, top: 6, bottom: 6 })
      .backgroundColor('#f0f0f0')
      .borderRadius(16)
  })
}
.width('100%')

注意:当 Flex 组件参数 wrap 设置为 FlexWrap.Wrap 或 FlexWrap.WrapReverse 时,主轴长度 auto 的自适应布局会失效,默认撑满父容器。如果需要自适应内容宽度,可以根据内容是否超过一行动态切换换行规则。

二、Grid 网格布局

2.1 Grid 的定位与结构

网格布局由"行"和"列"分割的单元格组成,通过指定"项目"所在的单元格可以做出各种各样的布局。网格布局具有较强的页面均分能力和子组件占比控制能力,是一种重要的自适应布局,其使用场景有九宫格图片展示、日历、计算器等。

ArkUI 提供了 Grid 容器组件和子组件 GridItem 用于构建网格布局。Grid 用于设置网格布局相关参数,GridItem 定义子组件相关特征。Grid 的子组件必须是 GridItem 组件。

2.2 行列数量与占比设置

Grid 组件提供了 rowsTemplate 和 columnsTemplate 属性用于设置网格布局的行列数量与尺寸占比。属性值是一个由多个空格和 '数字+fr' 间隔拼接的字符串,fr 的个数即网格布局的行或列数,fr 前面的数值大小用于计算该行或列在网格布局宽度上的占比,最终决定该行或列的宽度。

例如,columnsTemplate('1fr 1fr 1fr') 表示将水平方向划分为 3 等列,每列宽度相等。columnsTemplate('1fr 2fr') 表示两列,第二列宽度是第一列的 2 倍。

根据行列数量与占比属性的设置,Grid 可以分为三种布局情况:

· 行、列数量与占比同时设置:Grid 只展示固定行列数的元素,其余元素不展示,且 Grid 不可滚动(推荐使用该种布局方式)

· 只设置行或列数量与占比中的一个:元素按照设置的方向进行排布,超出的元素可通过滚动方式展示

· 行列数量与占比都不设置:元素在布局方向上排布,其行列数由布局方向、单个网格的宽高等多个属性共同决定

typescript 复制代码
Grid() {
  ForEach(this.items, (item: string, index: number) => {
    GridItem() {
      Text(item)
        .fontSize(16)
        .fontColor(Color.White)
    }
    .backgroundColor(this.colors[index % this.colors.length])
    .borderRadius(8)
    .rowStart(index * 2)      // 从第 index*2 行开始
    .rowEnd(index * 2 + 1)    // 到第 index*2+1 行结束
  })
}
.columnsTemplate('1fr 1fr 1fr')   // 3列等宽
.rowsTemplate('1fr 1fr')          // 2行等高
.columnsGap(10)                   // 列间距
.rowsGap(10)                      // 行间距
.width('100%')
.height(300)

2.3 子组件横跨行列

GridItem 支持通过 rowStart / rowEnd / columnStart / columnEnd 属性设置子组件所占的行列数,实现合并单元格的效果。此外,还可以直接使用 rowStart(0) 配合 rowEnd 来跨行,或 columnStart / columnEnd 来跨列。

三、Stack 层叠布局

3.1 Stack 的定位与基本用法

层叠布局(StackLayout)用于在屏幕上预留一块区域来显示组件中的元素,提供元素可以重叠的布局。容器中的子元素依次入栈,后一个子元素覆盖前一个子元素,子元素可以叠加,也可以设置位置。层叠布局具有较强的页面层叠、位置定位能力,其使用场景有广告、卡片层叠效果等。

Stack 容器内的子元素默认进行居中堆叠,子元素被约束在 Stack 下进行自己的样式定义以及排列。

typescript 复制代码
Stack({ alignContent: Alignment.BottomEnd }) {
  Image($r('app.media.background'))
    .width('100%')
    .height(200)

  Text('限时优惠')
    .fontSize(14)
    .fontColor(Color.White)
    .backgroundColor(Color.Red)
    .padding({ left: 8, right: 8, top: 4, bottom: 4 })
    .margin(10)
}
.width('100%')
.height(200)

3.2 对齐方式与 Z 序控制

Stack 组件通过 alignContent 参数实现位置的相对移动,支持九种对齐方式(TopStart、Top、TopEnd、Start、Center、End、BottomStart、Bottom、BottomEnd)。当 alignContent 和通用属性 align 同时设置时,后设置的生效。

Stack 容器中兄弟组件的显示层级关系可以通过 Z 序控制的 zIndex 属性改变。zIndex 值越大,显示层级越高,即 zIndex 值大的组件会覆盖在 zIndex 值小的组件上方。默认情况下,后入栈的子组件覆盖先入栈的子组件。在层叠布局中,如果后面子元素尺寸大于前面子元素尺寸,则前面子元素会被完全隐藏。

typescript 复制代码
Stack({ alignContent: Alignment.Center }) {
  Text('底层')
    .width('80%')
    .height('80%')
    .backgroundColor('#e1dede')
    .zIndex(1)

  Text('上层')
    .width('50%')
    .height('50%')
    .backgroundColor('#c1cbac')
    .zIndex(2)   // 层级更高,显示在"底层"上方
}
.width('100%')
.height(200)

四、List 列表布局

4.1 List 的定位与基本用法

List 是 ArkUI 中的列表容器组件,用于呈现连续、多行或多列的同类数据,例如图片和文本,支持垂直或水平滚动。List 适用于消息列表、商品列表、设置页面等场景,支持单列/多列布局、分组列表、吸顶吸底等能力。

List 的子组件应为 ListItem 或 ListItemGroup。ListItem 表示单个列表项,ListItemGroup 用于分组。

typescript 复制代码
List({ space: 10 }) {
  ForEach(this.messages, (msg: Message) => {
    ListItem() {
      Row() {
        Image(msg.avatar).width(40).height(40).borderRadius(20)
        Column() {
          Text(msg.name).fontSize(16).fontWeight(FontWeight.Bold)
          Text(msg.content).fontSize(14).fontColor(Color.Gray)
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
      }
      .width('100%')
      .padding(12)
    }
  }, (msg: Message) => msg.id.toString())
}
.width('100%')
.padding(16)
.divider({ strokeWidth: 1, color: '#e0e0e0' })   // 分割线

4.2 分组列表

当列表数据需要按类别分组时,可以使用 ListItemGroup 将同类数据组织在一起,并为其设置分组标题(header)。

typescript 复制代码
List() {
  ForEach(this.groups, (group: Group) => {
    ListItemGroup({ header: this.groupHeader(group.title) }) {
      ForEach(group.items, (item: string) => {
        ListItem() {
          Text(item).fontSize(16).padding(12)
        }
      })
    }
  })
}
.sticky(StickyStyle.Header)   // 分组标题吸顶

五、长列表性能优化

5.1 为什么需要性能优化?

在处理大量子组件时,如果使用普通的 ForEach 循环渲染,List 会一次性创建所有的子组件,在需要的时候布局和渲染屏幕范围内的节点。这意味着如果列表有上千条数据,即使屏幕上只显示十几条,框架也会创建全部上千个组件节点,导致启动速度慢、内存占用高。

为了解决这个问题,ArkUI 提供了三大优化手段:懒加载、缓存列表项、组件复用。

5.2 LazyForEach 懒加载

LazyForEach 从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。当在滚动容器中使用了 LazyForEach,框架会根据滚动容器可视区域按需创建组件,当组件滑出可视区域外时,框架会进行组件销毁回收以降低内存占用。

与 ForEach 的核心区别在于:ForEach 一次性创建全部子组件,划出屏幕范围的节点不会下树销毁;而 LazyForEach 只会创建屏幕范围内的节点,划出屏幕范围的节点会下树销毁。

使用 LazyForEach 需要先实现 IDataSource 接口,这是懒加载的核心。

typescript 复制代码
// 实现 IDataSource 接口
class BasicDataSource implements IDataSource {
  private listeners: DataChangeListener[] = []
  private originDataArray: string[] = []

  totalCount(): number {
    return this.originDataArray.length
  }

  getData(index: number): string {
    return this.originDataArray[index]
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      this.listeners.push(listener)
    }
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener)
    if (pos >= 0) {
      this.listeners.splice(pos, 1)
    }
  }

  // 数据操作方法...
  pushData(data: string): void {
    this.originDataArray.push(data)
    this.listeners.forEach(listener => {
      listener.onDataAdd(this.originDataArray.length - 1)
    })
  }
}
typescript 复制代码
// 在 List 中使用 LazyForEach
List() {
  LazyForEach(this.dataSource, (item: string, index: number) => {
    ListItem() {
      Text(item).fontSize(16).padding(12)
    }
  }, (item: string) => item)   // key 必须唯一且稳定
}
.cachedCount(5)   // 预加载屏幕外上下各5项

5.3 cachedCount 预加载

List 支持通过 cachedCount 设置预加载的数量。默认会预加载显示区域上下各一屏子组件(最大预加载 16 行子组件)。预加载是指除了加载显示区域内可见的子组件外,还支持在空闲时隙提前加载部分显示区域外不可见的子组件,使用预加载可以减少滚动丢帧,提升流畅性。cachedCount 只在 LazyForEach 中生效,即 cachedCount 只能与 LazyForEach 一起使用。

cachedCount 的增加会增大 UI 的 CPU 和内存开销,使用时需要根据实际情况调整。一般建议:中等长度列表设置 10 左右,长列表快速滚动场景可适当增大到 20。

5.4 @Reusable 组件复用

为了降低反复创建销毁自定义组件带来的性能开销,可以使用 @Reusable 装饰 @Component 装饰的自定义组件,实现组件复用。标记为 @Reusable 的组件从组件树上被移除时,组件和其对应的 JSView 对象会被放入复用缓存中,再次需要时优先从缓存池中取出同结构节点并回调 aboutToReuse 更新数据,省去 build() 重建成本。

typescript 复制代码
@Reusable
@Component
struct MessageItem {
  @State message: Message = new Message()

  aboutToReuse(params: Record<string, Object>): void {
    this.message = params.message as Message
  }

  build() {
    Row() {
      Image(this.message.avatar).width(40).height(40).borderRadius(20)
      Column() {
        Text(this.message.name).fontSize(16).fontWeight(FontWeight.Bold)
        Text(this.message.content).fontSize(14).fontColor(Color.Gray)
      }
    }
    .padding(12)
  }
}

性能优化总结:LazyForEach 负责按需创建视口内组件节点,@Reusable 负责将滑出屏幕的组件缓存复用而非销毁重建,两者配合可以显著降低内存占用和滑动丢帧率。cachedCount 则在此基础上预加载屏幕外的列表项,让组件提前创建好,滑入时直接渲染。

六、多元化习题

习题 1(判断题)

题目:在 Flex 布局中,flexGrow 用于定义组件在父容器空间不足时的压缩比例。

答案:错误

解读:flexGrow 定义的是组件在父容器有剩余空间时的扩展比例,剩余空间按各组件 flexGrow 比例分配。flexShrink 才是用于定义父容器空间不足时的压缩比例。

习题 2(单选题)

题目:以下 Grid 的 columnsTemplate 设置中,表示将水平方向划分为 3 等列的是( )

A. columnsTemplate('3fr')

B. columnsTemplate('1fr 1fr 1fr')

C. columnsTemplate('1 1 1')

D. columnsTemplate('3')

答案:B

解读:columnsTemplate 属性值是由多个空格和 '数字+fr' 间隔拼接的字符串,fr 的个数即列数,fr 前面的数值大小用于计算该列在网格布局宽度上的占比。'1fr 1fr 1fr' 表示 3 列等宽,每列占比相同。

习题 3(多选题)

题目:关于 Stack 层叠布局,以下说法正确的有(多选):

A. Stack 容器中的子元素依次入栈,后一个子元素覆盖前一个子元素

B. alignContent 支持九种对齐方式

C. zIndex 值越小,显示层级越高

D. Stack 适用于广告、卡片层叠效果等场景

答案:A、B、D

解读:Stack 容器中的子元素依次入栈,后入栈的组件覆盖先入栈的组件,选项 A 正确。alignContent 支持 TopStart、Top、TopEnd、Start、Center、End、BottomStart、Bottom、BottomEnd 共九种对齐方式,选项 B 正确。zIndex 值越大,显示层级越高,选项 C 的描述正好相反,错误。Stack 适用于广告、卡片层叠效果等场景,选项 D 正确。

习题 4(代码填空题)

题目:请补全以下代码,使 Grid 实现 4 列等宽的网格布局,每列占比相同。

typescript 复制代码
Grid() {
  ForEach(this.items, (item: string) => {
    GridItem() {
      Text(item).fontSize(16)
    }
  })
}
.______________('1fr 1fr 1fr 1fr')
.columnsGap(10)
.rowsGap(10)

答案:columnsTemplate

解读:columnsTemplate 用于设置 Grid 的列数和每列尺寸占比。'1fr 1fr 1fr 1fr' 表示 4 列等宽。rowsTemplate 用于设置行数和每行尺寸占比,但题目要求的是列设置。

习题 5(代码改错题)

题目:以下代码存在性能问题,请指出问题并修正。

typescript 复制代码
List() {
  ForEach(this.largeDataSource, (item: string) => {
    ListItem() {
      Text(item).fontSize(16).padding(12)
    }
  })
}
.width('100%')

答案:该代码使用 ForEach 渲染长列表,会一次性创建所有子组件,导致启动速度慢、内存占用高。应改为使用 LazyForEach 按需创建组件:

typescript 复制代码
List() {
  LazyForEach(this.dataSource, (item: string) => {
    ListItem() {
      Text(item).fontSize(16).padding(12)
    }
  }, (item: string) => item)
}
.cachedCount(10)
.width('100%')

解读:当 List 和 ForEach 结合时,会一次性创建所有的子组件,在需要的时候布局和渲染屏幕范围内的节点。对于长列表场景,应使用 LazyForEach 替代 ForEach,框架会根据滚动容器可视区域按需创建组件,组件滑出可视区域外时进行销毁回收,从而降低内存占用并提升启动速度。

习题 6(简答题)

题目:简述 LazyForEach、cachedCount、@Reusable 三者在长列表性能优化中的各自职责与配合关系。

答案:LazyForEach 负责按需迭代数据,框架根据滚动容器可视区域创建组件,组件滑出可视区域外时销毁回收,避免一次性创建大量组件节点。cachedCount 负责预加载,在空闲时隙提前创建屏幕外的组件,减少滚动时的丢帧,提升流畅性,只在配合 LazyForEach 时生效。@Reusable 负责组件复用,将滑出屏幕的组件实例放入复用缓存池而非销毁,再次需要时优先从缓存池中取出同结构节点并回调 aboutToReuse 更新数据,省去 build() 重建成本。三者配合可以显著降低内存占用和滑动丢帧率。

解读:LazyForEach 解决的是"一次性创建大量组件"的问题,cachedCount 解决的是"滚动时临时创建组件导致卡顿"的问题,@Reusable 解决的是"组件频繁创建销毁"的问题。三者各司其职,共同构成了 ArkUI 长列表性能优化的完整方案。

七、本节知识点总结

Flex 弹性布局

通过 flexBasis 设定基准尺寸、flexGrow 控制扩展、flexShrink 控制压缩,三者可单独或组合使用。FlexWrap.Wrap 实现自动换行流式布局。

Grid 网格布局

通过 columnsTemplate 和 rowsTemplate 设置行列数量与尺寸占比,fr 单位决定各行列占比。Grid 子组件必须是 GridItem。子组件可通过 rowStart / rowEnd / columnStart / columnEnd 横跨行列。

Stack 层叠布局

子元素依次入栈,后入栈者覆盖先入栈者。alignContent 支持九种对齐方式,zIndex 控制显示层级,值越大层级越高。适用于广告、卡片层叠、悬浮按钮等场景。

List 列表布局

适用于连续多行同类数据的展示,支持垂直/水平滚动、分组列表、吸顶吸底。子组件为 ListItem 或 ListItemGroup。

长列表性能优化

LazyForEach 实现按需创建和销毁,需实现 IDataSource 接口;cachedCount 预加载屏幕外列表项,仅在 LazyForEach 中生效;@Reusable 实现组件复用,配合 aboutToReuse 回调更新数据。三者配合可显著降低内存占用和滑动丢帧率。

布局选型建议

需要一维线性排列时使用 Row/Column;需要弹性伸缩和换行时使用 Flex;需要二维网格时使用 Grid;需要元素重叠时使用 Stack;需要长列表滚动时使用 List 并配合 LazyForEach 优化。

下节预告

第5课将深入讲解 ArkUI 中的路由导航与页面跳转,包括 router 路由的基本用法、Navigation 组件的页面栈管理、参数传递与返回值处理,以及页面转场动画的实现方式。

相关推荐
dtq04241 小时前
数据结构 - 栈
c语言·数据结构·学习
idanzk1 小时前
鸿蒙HarmonyOS——1.编译器下载
华为·harmonyos
威哥爱编程2 小时前
HarmonyOS 7 应用 Skill 化实战:从"被打开"到"被调用",把功能递进系统意图分发池
华为·harmonyos·arkts
威哥爱编程2 小时前
HarmonyOS 应用上架审核避坑:驳回五类地图与提交前自检清单
华为·harmonyos·arkts
威哥爱编程2 小时前
HarmonyOS 一多布局实战:先懂分层再碰断点,一份代码喂饱五个端
华为·harmonyos·arkts
Thomas.Sir2 小时前
第10课:PyTorch|学习率调度策略全面精讲【训练过程的“自适应巡航”】
人工智能·pytorch·学习
一起努力啊~2 小时前
算法题打卡力扣第1658题:将 x 减到 0 的最小操作数(mid)
学习·leetcode
传奇开心果编程2 小时前
【ArkUI 练中学】第1课:从零开始
学习·华为·前端框架
j7~2 小时前
【C++微服务项目开发脚手架】(准备篇)从 0 到跑通:Docker 容器开发环境 + 宿主机用户 + Trae 远程连接 全链路踩坑实战
linux·c++·学习·xshell·docker容器·trae