网格布局概要
网格布局是由行和列分割的单元格组成,通过指定项目所在的单元格做出各种各样的布局。网格布局具有较强的页面均分能力,子组件占比控制能力,是一种重要的自适应布局组件,其使用场景有九宫格图片展示、日历、计算器等。
ArkUI提供了Grid容器组件和子组件GridItem,用于构建网格布局。Grid用于设置网格布局相关参数,GridItem定义子组件相关特征。Grid组件支持使用条件渲染、循环渲染、懒加载等方式生成子组件。
布局与约束
Grid组件为网格容器,其中容器内各条目对应一个GridItem组件,如下图所示。
Grid与GridItem组件的关系
说明:
Grid的子组件必须是GridItem组件。
网格布局是一种二维布局。Grid组件支持自定义行列数和每行每列尺寸占比、设置子组件横跨几行或者几列,同时提供了垂直和水平布局能力。当网格容器组件尺寸发生变化时,所有子组件以及间距会等比例调整,从而实现网格布局的自适应能力。根据Grid的这些布局能力,可以构建出不同样式的网格布局,如下图所示。
网格布局
如果Grid组件设置了宽高属性,则其尺寸为设置值。如果没有设置宽高属性,Grid组件的尺寸默认适应其父组件的尺寸。
Grid组件根据行列数量与占比属性的设置,可以分为三种布局情况:
-
行、列数量与占比同时设置:Grid只展示固定行列数的元素,其余元素不展示,且Grid不可滚动。(推荐使用该种布局方式)
-
只设置行、列数量与占比中的一个:元素按照设置的方向进行排布,超出的元素可通过滚动的方式展示。
-
行列数量与占比都不设置:元素在布局方向上排布,其行列数由布局方向、单个网格的宽高等多个属性共同决定。超出行列容纳范围的元素不展示,且Grid不可滚动。
设置排列方式
设置行列数量与占比
通过设置行列数量与尺寸占比可以确定网格布局的整体排列方式。Grid组件提供了rowsTemplate和columnsTemplate属性用于设置网格布局行列数量与尺寸占比。
rowsTemplate和columnsTemplate属性值是一个由多个空格和'数字+fr'间隔拼接的字符串,fr的个数即网格布局的行或列数,fr前面的数值大小,用于计算该行或列在网格布局宽度上的占比,最终决定该行或列宽度。
行列数量占比示例
如上图所示,构建的是一个三行三列的网格布局,其在垂直方向上分为三等份,每行占一份;在水平方向上分为四等份,第一列占一份,第二列占两份,第三列占一份。
只要将rowsTemplate的值为'1fr 1fr 1fr',同时将columnsTemplate的值为'1fr 2fr 1fr',即可实现上述网格布局。
// 声明背景色数组
let colors: Color[] = [
Color.Black, Color.Blue, Color.Brown,
Color.Gray, Color.Green, Color.Grey,
Color.Orange, Color.Pink, Color.Red
]
@Entry
@Component
struct GridSample {
build() {
// 网格布局组件
Grid() {
ForEach(colors, (color: Color, index: number) => {
// 网格布局单元组件
GridItem().backgroundColor(color)
}, (index: number) => index.toString())
}
// 三行等高划分
.rowsTemplate('1fr 1fr 1fr')
// 第一列和第三列占据一个单位的宽度,第二列占据两个单位的宽度
.columnsTemplate('1fr 2fr 1fr')
}
}
显示效果如下图所示:
说明:
当Grid组件设置了rowsTemplate或columnsTemplate时,Grid的layoutDirection、maxCount、minCount、cellLength属性不生效。
设置子组件所占行列数
除了大小相同的等比例网格布局,由不同大小的网格组成不均匀分布的网格布局场景在实际应用中也十分常见,如下图所示。
在Grid组件中,可以通过在创建Grid时传入合适的 GridLayoutOptions
实现如下图所示的单个网格横跨多行或多列的场景。其中,irregularIndexes
和 onGetIrregularSizeByIndex
用于仅设置rowsTemplate或columnsTemplate的Grid,onGetRectByIndex
用于同时设置rowsTemplate和columnsTemplate的Grid。
不均匀网格布局
例如计算器的按键布局就是常见的不均匀网格布局场景。例如下图中的计算器,按键“0”和“=”,按键“0”横跨第一、二两列,按键“=”横跨第五、六两行。使用Grid构建的网格布局,其行列标号从0开始,依次编号。
在网格中,可以通过onGetRectByIndex返回的[rowStart,columnStart,rowSpan,columnSpan]来实现跨行跨列布局,其中rowStart和columnStart属性表示指定当前元素起始行号和起始列号,rowSpan和columnSpan属性表示指定当前元素的占用行数和占用列数。
所以“0”按键横跨第一列和第二列,“=”按键横跨第五行和第六行,只要将“0”对应onGetRectByIndex的rowStart和columnStart设为5和0,rowSpan和columnSpan设为1和2,将“=”对应onGetRectByIndex的rowStart和columnStart设为4和3,rowSpan和columnSpan设为2和1即可。
代码如下:
@Entry
@Component
struct GridSample {
// 布局选项
layoutOptions: GridLayoutOptions = {
regularSize: [1, 1], // 绝大多数网格的尺寸,通常是占据1行1列
onGetRectByIndex: (index: number) => {
switch (index) {
// 返回值格式:[<rowStart>, <columnStart>, <rowSpan>, <columnSpan>]
case 1: return [1, 0, 1, 1] // 从[1, 0]开始,跨行1,跨列1
case 2: return [1, 1, 1, 1] // 从[1, 1]开始,跨行1,跨列1
case 3: return [1, 2, 1, 1] // 从[1, 2]开始,跨行1,跨列1
case 4: return [1, 3, 1, 1] // 从[1, 3]开始,跨行1,跨列1
case 5: return [2, 0, 1, 1] // 从[2, 0]开始,跨行1,跨列1
case 6: return [2, 1, 1, 1] // 从[2, 1]开始,跨行1,跨列1
case 7: return [2, 2, 1, 1] // 从[2, 2]开始,跨行1,跨列1
case 8: return [2, 3, 1, 1] // 从[2, 3]开始,跨行1,跨列1
case 9: return [3, 0, 1, 1] // 从[3, 0]开始,跨行1,跨列1
case 10: return [3, 1, 1, 1] // 从[3, 1]开始,跨行1,跨列1
case 11: return [3, 2, 1, 1] // 从[3, 2]开始,跨行1,跨列1
case 12: return [3, 3, 1, 1] // 从[3, 3]开始,跨行1,跨列1
case 13: return [4, 0, 1, 1] // 从[4, 0]开始,跨行1,跨列1
case 14: return [4, 1, 1, 1] // 从[4, 1]开始,跨行1,跨列1
case 15: return [4, 2, 1, 1] // 从[4, 2]开始,跨行1,跨列1
case 16: return [4, 3, 2, 1] // 从[4, 3]开始,跨行2,跨列1
case 17: return [5, 0, 1, 2] // 从[5, 0]开始,跨行1,跨列1
case 18: return [5, 2, 1, 1] // 从[5, 2]开始,跨行1,跨列1
default: return [0, 0, 1, 4] // 从[0, 0]开始,跨行1,跨列4
}
}
}
// 显示的文本数组
texts: string[] = [
'CE', 'C', '/', 'X', '7', '8', '9', '-', '4', '5', '6', '+',
'1', '2', '3', '=', '0', '.'
]
build() {
// 设置使用指定的布局选项layoutOptions
Grid(undefined, this.layoutOptions) {
GridItem() {
Text('0').width('100%').height('100%').backgroundColor('#cccccc')
.padding(5).fontSize(30).textAlign(TextAlign.End).borderRadius(5)
}.padding(5)
ForEach(this.texts, (text: string) => {
GridItem() {
Button(text).type(ButtonType.Normal).height('100%').width('100%')
.backgroundColor('#aaaaaa').borderRadius(5).fontSize(20)
}.padding(5)
}, (index: number) => index.toString())
}
// 设置4列等宽
.columnsTemplate("1fr 1fr 1fr 1fr")
// 设置6行,第一行占据两个单位高度,其余一个单位高度
.rowsTemplate("2fr 1fr 1fr 1fr 1fr 1fr")
.height('70%').width('100%').backgroundColor('#eeeeee').padding(5)
}
}
设置主轴方向
使用Grid构建网格布局时,若没有设置行列数量与占比,可以通过layoutDirection设置网格布局的主轴方向,决定子组件的排列方式。此时可以结合minCount和maxCount属性来约束主轴方向上的网格数量。
主轴方向示意图
当前layoutDirection设置为Row时,先从左到右排列,排满一行再排下一行。当前layoutDirection设置为Column时,先从上到下排列,排满一列再排下一列,如上图所示。此时,将maxCount属性设为3,表示主轴方向上最大显示的网格单元数量为3。
实例代码如下所示:
@Entry
@Component
struct GridSample {
// 显示的文本数组
texts: string[] = [
'1', '2', '3', '4', '5', '6', '7', '8', '9'
]
build() {
Row() {
Grid() {
ForEach(this.texts, (text: string) => {
GridItem({}) {
Text(text).height(100).width(100)
.backgroundColor('#aaaaaa').borderRadius(5).fontSize(20)
.fontColor(Color.White).textAlign(TextAlign.Center)
}.padding(5)
}, (index: number) => index.toString())
}
// 注意,如果设置了多个layoutDirection,则最后一次设置的生效
// 设置主轴方向为行的从左向右
.layoutDirection(GridDirection.Row)
// 设置主轴方向为从上向下
.layoutDirection(GridDirection.Column)
// 设置主轴方向为从右向左
.layoutDirection(GridDirection.RowReverse)
// 设置主轴方向为从下向上
.layoutDirection(GridDirection.ColumnReverse)
// 设置主轴方向最多显示3个网格单元,最少3个网格单元
.maxCount(3)
}
.justifyContent(FlexAlign.Center)
.backgroundColor('#eeeeee')
.width('100%').height('50%')
}
}
说明:
-
layoutDirection属性仅在不设置rowsTemplate和columnsTemplate时生效,此时元素在layoutDirection方向上排列。
-
仅设置rowsTemplate时,Grid主轴为水平方向,交叉轴为垂直方向。
-
仅设置columnsTemplate时,Grid主轴为垂直方向,交叉轴为水平方向。
在网格布局中显示数据
网格布局采用二维布局的方式组织其内部元素,如下图所示。
通用办公服务
Grid组件可以通过二维布局的方式显示一组GridItem子组件。
@Entry
@Component
struct GridSample {
texts: string[] = [
'会议', '签到', '投票', '打印'
]
build() {
Row() {
Grid() {
ForEach(this.texts, (text: string) => {
GridItem({}) {
Text(text).height('100%').width('100%')
.backgroundColor('#aaaaaa').borderRadius(10).fontSize(30)
.fontColor(Color.White).textAlign(TextAlign.Center)
}.padding(10)
}, (index: number) => index.toString())
}
.padding(10)
.rowsTemplate('1fr 1fr')
.columnsTemplate('1fr 1fr')
}
.justifyContent(FlexAlign.Center)
.backgroundColor('#eeeeee')
.width('100%').height('50%')
}
}
对于内容结构相似的多个GridItem,通常更推荐使用ForEach语句中嵌套GridItem的形式,来减少重复代码。
设置行列间距
在两个网格单元之间的网格横向间距称为行间距,网格纵向间距称为列间距,如下图所示。
注意,下图中与父组件边框交界的位置使用了父组件的内边距设置,我们需要注意的是列间距和行间距。
网格的行列间距
通过Grid的rowsGap和columnsGap可以设置网格布局的行列间距。
@Entry
@Component
struct GridSample {
build() {
Row() {
Grid() {
GridItem().backgroundColor('#999999')
GridItem().backgroundColor('#999999')
GridItem().backgroundColor('#999999')
GridItem().backgroundColor('#999999')
GridItem().backgroundColor('#999999')
GridItem().backgroundColor('#999999')
GridItem().backgroundColor('#999999')
GridItem().backgroundColor('#999999')
GridItem().backgroundColor('#999999')
GridItem().backgroundColor('#999999')
GridItem().backgroundColor('#999999')
GridItem().backgroundColor('#999999')
}.rowsTemplate('1fr 1fr 1fr 1fr').columnsTemplate('1fr 1fr 1fr')
// 设置列间距为10vp
.columnsGap(10)
// 设置行间距为10vp
.rowsGap(10)
}
// 设置内边距为10vp
.padding(10)
// 子组件水平方向居中显示
.justifyContent(FlexAlign.Center)
.backgroundColor('#eeeeee')
.width('100%').height('65%')
}
}
构建可滚动的网格布局
可滚动的网格布局常用在文件管理、购物或视频列表等页面中,如下图所示。在设置Grid的行列数量与占比时,如果仅设置行、列数量与占比中的一个,即仅设置rowsTemplate或仅设置columnsTemplate属性,网格单元按照设置的方向排列,超出Grid显示区域后,Grid拥有可滚动能力。
横向可滚动网格布局
如果设置的是columnsTemplate,Grid的滚动方向为垂直方向;如果设置的是rowsTemplate,Grid的滚动方向为水平方向。
如上图所示的横向可滚动网格布局,只要设置rowsTemplate属性的值且不设置columnsTemplate属性,当内容超出Grid组件宽度时,Grid可横向滚动进行内容展示。
案例代码如下:
@Entry
@Component
struct GridSample {
@State services: Array<string> = [
'直播', '分类', '领券', '会员', '积分',
'进口', '充值', '抽奖', '收藏', '更多'
]
build() {
Column({ space: 5 }) {
Grid() {
ForEach(this.services, (service: string, index) => {
GridItem() {
Button(service).type(ButtonType.Circle).backgroundColor('#aaaaaa')
.height('90%').width('90%').stateEffect(false)
}.width('25%').height('50%')
}, (service:string):string => service)
}
// 只设置rowsTemplate属性,当内容超出Grid区域时,可水平滚动。
.rowsTemplate('1fr 1fr')
.rowsGap(15)
.scrollBar(BarState.Off)
}.height(185).backgroundColor('#dddddd')
}
}
控制滚动位置
与新闻列表的返回顶部场景类似,控制滚动位置功能在网格布局中也很常用,例如下图所示日历的翻页功能。
日历翻页如下图所示:
日历翻页
Grid组件初始化时,可以绑定一个Scroller对象,用于进行滚动控制,例如通过Scroller对象的scrollPage方法进行翻页。
在日历页面中,用户在点击“下一页”按钮时,应用响应点击事件,通过指定scrollPage方法的参数next为true,滚动到下一页。
案例代码如下所示:
@Entry
@Component
struct GridSample {
// 通过Scroller对象的scrollPage方法进行翻页
private scroller: Scroller = new Scroller()
months: number[][] = [
[
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30
], [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30
], [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30
], [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30
], [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30
], [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30
]
]
build() {
Column({ space: 5 }) {
// 设置滚动控制器
Grid(this.scroller) {
ForEach(this.months, (days: number[]) => {
ForEach(days, (day: number[]) => {
GridItem() {
Text(day.toString()).width('100%').height(50)
.backgroundColor('#aaaaaa').textAlign(TextAlign.Center)
.fontColor(Color.White).fontWeight(FontWeight.Bold)
}.padding({ left: 5, top: 5 })
}, (dayIndex: number) => Math.random().toString() + dayIndex)
}, (monthIndex: number) => Math.random().toString() + monthIndex)
}.columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr')
.scrollBar(BarState.Off)
.height(220)
Row({ space: 20 }) {
Button('上一页')
.onClick(() => {
// 调用滚动控制器的方法向下翻页
this.scroller.scrollPage({ next: false })
})
Button('下一页')
.onClick(() => {
// 调用滚动控制器的方法向上翻页
this.scroller.scrollPage({ next: true })
})
}
}.backgroundColor('#eeeeee').height('100%')
}
}
性能优化
与长列表的处理类似,循环渲染适用于数据量较小的布局场景,当构建具有大量网格项的可滚动网格布局时,推荐使用数据懒加载的方式实现按需迭代加载数据,从而提升列表性能。
当使用懒加载方式渲染网格时,为了更好的滚动体验,减少滑动时出现白块,Grid组件中也可通过cachedCount属性设置GridItem的预加载数量,只在懒加载LazyForEach中生效。
设置预加载数量后,会在Grid显示区域前后各缓存 cachedCount × 列数个GridItem,超出显示和缓存范围的GridItem会被释放。
Grid() {
LazyForEach(this.dataSource, () => {
GridItem() {
}
})
}.cachedCount(3)
说明:
cachedCount的增加会增大UI的CPU、内存开销。使用时需要根据实际情况,综合性能和用户体验进行调整。