目录
????????网格布局是由“行”和“列”分割的单元格所组成,通过指定“项目”所在的单元格做出各种各样的布局。网格布局具有较强的页面均分能力,子组件占比控制能力,是一种重要自适应布局,其使用场景有九宫格图片展示、日历、计算器等。
????????ArkUI提供了Grid容器组件和子组件GridItem,用于构建网格布局。Grid用于设置网格布局相关参数,GridItem定义子组件相关特征。Grid组件支持使用条件渲染、循环渲染、懒加载等渲染控制方式生成子组件。
????????Grid组件为网格容器,其中容器内每一个条目对应一个GridItem组件,如下图所示。
图1?Grid与GridItem组件关系
说明
Grid的子组件必须是GridItem组件。
????????网格布局是一种二维布局。Grid组件支持自定义行列数和每行每列尺寸占比、设置子组件横跨几行或者几列,同时提供了垂直和水平布局能力。当网格容器组件尺寸发生变化时,所有子组件以及间距会等比例调整,从而实现网格布局的自适应能力。根据Grid的这些布局能力,可以构建出不同样式的网格布局,如下图所示。
图2?网格布局
????????如果Grid组件设置了宽高属性,则其尺寸为设置值。如果没有设置宽高属性,Grid组件的尺寸默认适应其父组件的尺寸。
Grid组件根据行列数量与占比属性的设置,可以分为三种布局情况:
????????通过设置行列数量与尺寸占比可以确定网格布局的整体排列方式。Grid组件提供了rowsTemplate和columnsTemplate属性用于设置网格布局行列数量与尺寸占比。
????????rowsTemplate和columnsTemplate属性值是一个由多个空格和'数字+fr'间隔拼接的字符串,fr的个数即网格布局的行或列数,fr前面的数值大小,用于计算该行或列在网格布局宽度上的占比,最终决定该行或列的宽度。
图3?行列数量占比示例
????????如上图所示,构建的是一个三行三列的的网格布局,其在垂直方向上分为三等份,每行占一份;在水平方向上分为四等份,第一列占一份,第二列占两份,第三列占一份。
????????只要将rowsTemplate的值为'1fr 1fr 1fr',同时将columnsTemplate的值为'1fr 2fr 1fr',即可实现上述网格布局。
@Entry
@Component
struct GridLayoutPage1 {
@State message: string = 'Hello World'
build() {
Row() {
Column() {
Grid() {
GridItem() {
Text(`1`).gridTextStyle("#1067c8ff")
}
GridItem() {
Text(`2`).gridTextStyle("#2067c8ff")
}
GridItem() {
Text(`3`).gridTextStyle("#3067c8ff")
}
GridItem() {
Text(`4`).gridTextStyle("#4067c8ff")
}
GridItem() {
Text(`5`).gridTextStyle("#5067c8ff")
}
GridItem() {
Text(`6`).gridTextStyle("#6067c8ff")
}
GridItem() {
Text(`7`).gridTextStyle("#7067c8ff")
}
GridItem() {
Text(`8`).gridTextStyle("#8067c8ff")
}
GridItem() {
Text(`9`).gridTextStyle("#9067c8ff")
}
}
.rowsTemplate('1fr 1fr 1fr')
.columnsTemplate('1fr 2fr 1fr')
}
.width('100%')
}
.height('40%')
}
}
@Extend(Text) function gridTextStyle(value: ResourceColor) {
.backgroundColor(value)
.width('100%')
.height('100%')
.fontSize(22)
.textAlign(TextAlign.Center)
}
? ? ? ? 效果如下:
?
????????除了大小相同的等比例网格布局,由不同大小的网格组成不均匀分布的网格布局场景在实际应用中十分常见,如下图所示。在Grid组件中,通过设置GridItem的rowStart、rowEnd、columnStart和columnEnd可以实现如图所示的单个网格横跨多行或多列的场景。
图4?不均匀网格布局
????????例如计算器的按键布局就是常见的不均匀网格布局场景。如下图,计算器中的按键“0”和“=”,按键“0”横跨第一、二两列,按键“=”横跨第五、六两行。使用Grid构建的网格布局,其行列标号从1开始,依次编号。
图5?计算器
????????在单个网格单元中,rowStart和rowEnd属性表示指定当前元素起始行号和终点行号,columnStart和columnEnd属性表示指定当前元素的起始列号和终点列号。
????????所以“0”按键横跨第一列和第二列,只要将“0”对应GridItem的columnStart和columnEnd设为1和2,将“=”对应GridItem的的rowStart和rowEnd设为5和6即可。
????????“=”按键横跨第五行和第六行,只要将将“=”对应GridItem的的rowStart和rowEnd设为5和6即可。
@Entry
@Component
struct CalculatorLayoutPage {
@State message: string = 'Hello World'
build() {
Row() {
Column() {
Grid() {
GridItem() {
Text(`0`).gridTextStyle2("#1067c8ff").textAlign(TextAlign.End).padding(2)
}.rowStart(0).rowEnd(1).columnStart(0).columnEnd(3)
GridItem() {
Text(`CE`).gridTextStyle2("#2067c8ff")
}
GridItem() {
Text(`C`).gridTextStyle2("#3067c8ff")
}
GridItem() {
Text(`/`).gridTextStyle2("#4067c8ff")
}
GridItem() {
Text(`X`).gridTextStyle2("#5067c8ff")
}
GridItem() {
Text(`7`).gridTextStyle2("#6067c8ff")
}
GridItem() {
Text(`8`).gridTextStyle2("#7067c8ff")
}
GridItem() {
Text(`9`).gridTextStyle2("#8067c8ff")
}
GridItem() {
Text(`-`).gridTextStyle2("#9067c8ff")
}
GridItem() {
Text(`4`).gridTextStyle2("#A067c8ff")
}
GridItem() {
Text(`5`).gridTextStyle2("#B067c8ff")
}
GridItem() {
Text(`6`).gridTextStyle2("#C067c8ff")
}
GridItem() {
Text(`+`).gridTextStyle2("#D067c8ff")
}
GridItem() {
Text(`1`).gridTextStyle2("#E067c8ff")
}
GridItem() {
Text(`2`).gridTextStyle2("#F067c8ff")
}
GridItem() {
Text(`3`).gridTextStyle2("#F167c8ff")
}
GridItem() {
Text(`=`).gridTextStyle2("#F267c8ff")
}.rowStart(5).rowEnd(6).columnStart(3).columnEnd(3)
GridItem() {
Text(`0`).gridTextStyle2("#F367c8ff")
}.columnStart(0).columnEnd(1)
GridItem() {
Text(`.`).gridTextStyle2("#F467c8ff")
}
}
.margin(12)
.rowsGap(12)
.columnsGap(8)
.rowsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr')
.columnsTemplate('1fr 1fr 1fr 1fr')
}
.width('100%')
}
.height('80%')
}
}
@Extend(Text) function gridTextStyle2(value: ResourceColor) {
.backgroundColor(value)
.width('100%')
.height('100%')
.fontSize(40)
.textAlign(TextAlign.Center)
.borderRadius(5)
}
? ? ? ? ?效果如下:
????????使用Grid构建网格布局时,若没有设置行列数量与占比,可以通过layoutDirection可以设置网格布局的主轴方向,决定子组件的排列方式。此时可以结合minCount和maxCount属性来约束主轴方向上的网格数量。
图6?主轴方向示意图
????????当前layoutDirection设置为Row时,先从左到右排列,排满一行再排一下一行。当前layoutDirection设置为Column时,先从上到下排列,排满一列再排一下一列,如上图所示。此时,将maxCount属性设为3,表示主轴方向上最大显示的网格单元数量为3。
@Entry
@Component
struct GridLayoutPage2 {
@State array: Array<string> = ['1', '2', '3', '4', '5', '6', '7', '8', '9']
build() {
Row() {
Column() {
Grid() {
ForEach(this.array, (item: string, index) => {
GridItem() {
Text(item).gridTextStyle_page2('#67c8ff')
}
}, item => item)
}
.rowsGap(12).columnsGap(8).margin(4).maxCount(3).minCount(3).layoutDirection(GridDirection.Column)
}
.width('100%')
}
.height('40%')
}
}
@Extend(Text) function gridTextStyle_page2(value: ResourceColor) {
.backgroundColor(value)
.width('30%')
.height('30%')
.fontSize(22)
.textAlign(TextAlign.Center)
}
? ? ? ? 效果如下:
说明
1. layoutDirection属性仅在不设置rowsTemplate和columnsTemplate时生效,此时元素在layoutDirection方向上排列。
2. 仅设置rowsTemplate时,Grid主轴为水平方向,交叉轴为垂直方向。
2. 仅设置columnsTemplate时,Grid主轴为垂直方向,交叉轴为水平方向。
????????网格布局采用二维布局的方式组织其内部元素,如下图所示。
图7?通用办公服务
????????Grid组件可以通过二维布局的方式显示一组GridItem子组件。
@Entry
@Component
struct GridLayoutPage3 {
@State message: string = 'Hello World'
build() {
Row() {
Column() {
Grid() {
GridItem() {
Text('会议')
}.backgroundColor('#1067c8ff')
GridItem() {
Text('签到')
}.backgroundColor('#3067c8ff')
GridItem() {
Text('培训')
}.backgroundColor('#5067c8ff')
GridItem() {
Text('打印')
}.backgroundColor('#7067c8ff')
}.rowsTemplate('1fr 1fr').columnsTemplate('1fr 1fr')
}
.width('100%').height('40%')
}
.height('100%').alignItems(VerticalAlign.Top)
}
}
????????对于内容结构相似的多个GridItem,通常更推荐使用循环渲染ForEach语句中嵌套GridItem的形式,来减少重复代码。
@Entry
@Component
struct GridLayoutPage3 {
@State message: Bean[] = [
{ title: '会议', color: '#1067c8ff' },
{ title: '签到', color: '#3067c8ff' },
{ title: '培训', color: '#5067c8ff' },
{ title: '打印', color: '#7067c8ff' }]
build() {
Row() {
Column() {
Grid() {
ForEach(this.message, (item: Bean) => {
GridItem() {
Text(item.title)
.backgroundColor(item.color)
.width('100%')
.height('100%')
.textAlign(TextAlign.Center)
}
}, item => JSON.stringify(item))
}.rowsTemplate('1fr 1fr').columnsTemplate('1fr 1fr')
}
.width('100%').height('40%')
}
.height('100%').alignItems(VerticalAlign.Top)
}
}
class Bean {
title: string
color: ResourceColor
}
? ? ? ? 效果同上。
????????在两个网格单元之间的网格横向间距称为行间距,网格纵向间距称为列间距,如下图所示。
图8?网格的行列间距
????????通过Grid的rowsGap和columnsGap可以设置网格布局的行列间距。
@Entry
@Component
struct GridLayoutPage3 {
@State message: Bean[] = [
{ title: '会议', color: '#1067c8ff' },
{ title: '签到', color: '#3067c8ff' },
{ title: '培训', color: '#5067c8ff' },
{ title: '打印', color: '#7067c8ff' }]
build() {
Row() {
Column() {
Grid() {
ForEach(this.message, (item: Bean) => {
GridItem() {
Text(item.title)
.backgroundColor(item.color)
.width('100%')
.height('100%')
.textAlign(TextAlign.Center)
}
}, item => JSON.stringify(item))
}.rowsTemplate('1fr 1fr').columnsTemplate('1fr 1fr').rowsGap(10).columnsGap(10).margin(10)
}
.width('100%').height('40%').backgroundColor('#eee')
}
.height('100%').alignItems(VerticalAlign.Top)
}
}
class Bean {
title: string
color: ResourceColor
}
? ? ? ? 效果如下:
????????可滚动的网格布局常用在文件管理、购物或视频列表等页面中,如下图所示。在设置Grid的行列数量与占比时,如果仅设置行、列数量与占比中的一个,即仅设置rowsTemplate或仅设置columnsTemplate属性,网格单元按照设置的方向排列,超出Grid显示区域后,Grid拥有可滚动能力。
图9?横向可滚动网格布局
????????如果设置的是columnsTemplate,Grid的滚动方向为垂直方向;如果设置的是rowsTemplate,Grid的滚动方向为水平方向。
????????如上图所示的横向可滚动网格布局,只要设置rowsTemplate属性的值且不设置columnsTemplate属性,当内容超出Grid组件宽度时,Grid可横向滚动进行内容展示。
@Entry
@Component
struct GridScrollLayoutPage {
@State services: Array<string> = ['直播', '进口', '国外', '乡里', '本地', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I']
color: ResourceColor[] = [Color.Blue, Color.Green, Color.Orange, Color.Pink, Color.Yellow]
build() {
Column({ space: 5 }) {
Grid() {
ForEach(this.services, (service: string, index) => {
GridItem() {
Text(service)
.backgroundColor(this.color[index % this.color.length])
.fontColor(Color.White)
.width(72)
.height(72)
.fontSize(24)
.borderRadius(8)
.textAlign(TextAlign.Center)
.borderRadius(36)
}
.width('25%')
}, service => service)
}
.rowsTemplate('1fr 1fr') // 只设置rowsTemplate属性,当内容超出Grid区域时,可水平滚动。
.rowsGap(15)
.columnsGap(12)
.height('25%')
}
.borderColor(Color.Pink)
.borderWidth(1)
.borderRadius(8)
.margin(8)
.backgroundColor("#1067c8ff")
}
}
import hilog from '@ohos.hilog';
@Entry
@Component
struct CalendarLayoutPage {
private scroller: Scroller = new Scroller()
private headLabel: string[] = ['一', '二', '三', '四', '五', '六', '日']
private curDate: Date;
@State curDateStr: string = ''
@State dateArray: number[] = []
aboutToAppear() {
let date = new Date();
this.curDate = date;
this.changeMonthData()
}
changeMonthData() {
let monthLength = this.getMonthLength(this.curDate);
this.dateArray = []
for (let index = 0; index < monthLength; index++) {
this.dateArray.push(index + 1)
}
this.curDateStr = `${this.curDate.getFullYear()}-${this.curDate.getMonth() + 1}`
}
private nextMonth() {
this.curDate.setMonth(this.curDate.getMonth() + 1)
}
private prevMonth() {
this.curDate.setMonth(this.curDate.getMonth() - 1)
}
private getMonthLength(date: Date): number {
let copyDate = new Date(date.getFullYear(), date.getMonth() + 1, 0)
return copyDate.getDate()
}
build() {
Row() {
Column() {
Text(this.curDateStr)
.backgroundColor(Color.Pink)
.fontColor(Color.White)
.height(48)
.padding({ left: 24, right: 24 })
.borderRadius(24)
Row({ space: 8 }) {
ForEach(this.headLabel, (item) => {
Text(item).fontSize(14).layoutWeight(1).textAlign(TextAlign.Center).height('40%').borderRadius(26).backgroundColor('#67c8ff')
}, item => JSON.stringify(item))
}.height(52).backgroundColor('#999').margin({top: 24})
Grid() {
ForEach(this.dateArray, (item) => {
GridItem() {
Button(item + "")
.width(52)
.height(52)
.borderWidth(1)
.borderColor('#67c8ff')
}
}, item => JSON.stringify(item))
}
.columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr')
.columnsGap(5)
.rowsGap(5)
.margin({ top: 12 })
.height('40%')
Row({ space: 20 }) {
Button('上一页')
.onClick(() => {
this.prevMonth()
this.changeMonthData()
// this.scroller.scrollPage({
// next: false
// })
})
Button('下一页')
.onClick(() => {
this.nextMonth()
this.changeMonthData()
// this.scroller.scrollPage({
// next: true
// })
})
}
}
.width('100%').backgroundColor('#eee').borderRadius(24)
}
.height('100%')
.alignItems(VerticalAlign.Top)
.margin({top: 48, left: 12, right: 12})
}
}
? ? ? ? 通过?@State dateArray来刷新UI,当点击前一页的时候,当前月份减少1,然后改变dateArray的数据,触发UI更新;点击下一页同理。效果如下:
????????与长列表的处理类似,循环渲染适用于数据量较小的布局场景,当构建具有大量网格项的可滚动网格布局时,推荐使用数据懒加载方式实现按需迭代加载数据,从而提升列表性能。
????????关于按需加载优化的具体实现可参考数据懒加载章节中的示例。
????????当使用懒加载方式渲染网格时,为了更好的滚动体验,减少滑动时出现白块,Grid组件中也可通过cachedCount属性设置GridItem的预加载数量,只在懒加载LazyForEach中生效。
????????设置预加载数量后,会在Grid显示区域前后各缓存cachedCount*列数个GridItem,超出显示和缓存范围的GridItem会被释放。
?
Grid() {
LazyForEach(this.dataSource, item => {
GridItem() {
...
}
})
}
.cachedCount(3)
说明
cachedCount的增加会增大UI的CPU、内存开销。使用时需要根据实际情况,综合性能和用户体验进行调整。