本节目标
· 掌握 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 组件的页面栈管理、参数传递与返回值处理,以及页面转场动画的实现方式。