HarmonyOS布局艺术:Flex、Stack、Grid等容器组件的使用技巧
布局是UI设计的核心,HarmonyOS提供了强大的容器组件系统。本文将深入讲解Flex、Stack、Grid等核心布局组件的使用技巧和最佳实践。
一、Flex弹性布局详解
1.1 Flex基础布局
@Component
struct FlexBasicExample {
@State direction: FlexDirection = FlexDirection.Row
@State justifyContent: FlexAlign = FlexAlign.Start
@State alignItems: ItemAlign = ItemAlign.Start
@State wrap: FlexWrap = FlexWrap.NoWrap
build() {
Column({ space: 20 }) {
// 控制面板
this.buildControlPanel()
// Flex布局演示区域
Text('Flex布局演示').fontSize(18).fontWeight(FontWeight.Bold)
Flex({
direction: this.direction,
justifyContent: this.justifyContent,
alignItems: this.alignItems,
wrap: this.wrap
}) {
ForEach([1, 2, 3, 4, 5, 6], (item: number) => {
Text(`Item ${item}`)
.width(60)
.height(60)
.backgroundColor(this.getItemColor(item))
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.fontSize(14)
})
}
.width('100%')
.height(200)
.padding(10)
.backgroundColor('#F8F9FA')
.border({ width: 1, color: '#E0E0E0' })
}
.width('100%')
.padding(20)
}
@Builder
buildControlPanel() {
Column({ space: 10 }) {
Text('Flex布局控制').fontSize(16).fontWeight(FontWeight.Bold)
// 方向控制
Row({ space: 5 }) {
Text('方向:').fontSize(14).width(60)
Button('Row').onClick(() => this.direction = FlexDirection.Row).stateEffect(this.direction === FlexDirection.Row)
Button('Column').onClick(() => this.direction = FlexDirection.Column).stateEffect(this.direction === FlexDirection.Column)
Button('RowReverse').onClick(() => this.direction = FlexDirection.RowReverse).stateEffect(this.direction === FlexDirection.RowReverse)
Button('ColumnReverse').onClick(() => this.direction = FlexDirection.ColumnReverse).stateEffect(this.direction === FlexDirection.ColumnReverse)
}
// 主轴对齐
Row({ space: 5 }) {
Text('主轴:').fontSize(14).width(60)
Button('Start').onClick(() => this.justifyContent = FlexAlign.Start).stateEffect(this.justifyContent === FlexAlign.Start)
Button('Center').onClick(() => this.justifyContent = FlexAlign.Center).stateEffect(this.justifyContent === FlexAlign.Center)
Button('End').onClick(() => this.justifyContent = FlexAlign.End).stateEffect(this.justifyContent === FlexAlign.End)
Button('SpaceBetween').onClick(() => this.justifyContent = FlexAlign.SpaceBetween).stateEffect(this.justifyContent === FlexAlign.SpaceBetween)
}
// 交叉轴对齐
Row({ space: 5 }) {
Text('交叉轴:').fontSize(14).width(60)
Button('Start').onClick(() => this.alignItems = ItemAlign.Start).stateEffect(this.alignItems === ItemAlign.Start)
Button('Center').onClick(() => this.alignItems = ItemAlign.Center).stateEffect(this.alignItems === ItemAlign.Center)
Button('End').onClick(() => this.alignItems = ItemAlign.End).stateEffect(this.alignItems === ItemAlign.End)
Button('Stretch').onClick(() => this.alignItems = ItemAlign.Stretch).stateEffect(this.alignItems === ItemAlign.Stretch)
}
}
.width('100%')
.padding(15)
.backgroundColor('#FFFFFF')
.borderRadius(8)
}
getItemColor(index: number): string {
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FECA57', '#FF9FF3']
return colors[index % colors.length]
}
}
1.2 Flex高级特性
@Component
struct FlexAdvancedExample {
@State items: any[] = [
{ id: 1, name: 'Item1', flexGrow: 1, flexShrink: 1, alignSelf: ItemAlign.Auto },
{ id: 2, name: 'Item2', flexGrow: 2, flexShrink: 1, alignSelf: ItemAlign.Center },
{ id: 3, name: 'Item3', flexGrow: 1, flexShrink: 2, alignSelf: ItemAlign.End }
]
build() {
Column({ space: 20 }) {
Text('Flex高级特性').fontSize(20).fontWeight(FontWeight.Bold)
// Flex容器
Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceAround }) {
ForEach(this.items, (item: any) => {
Text(item.name)
.flexGrow(item.flexGrow)
.flexShrink(item.flexShrink)
.alignSelf(item.alignSelf)
.minWidth(60)
.height(80)
.backgroundColor(this.getItemColor(item.id))
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.padding(10)
})
}
.width('100%')
.height(120)
.padding(10)
.backgroundColor('#F5F5F5')
// 控制面板
this.buildItemControls()
}
.width('100%')
.padding(20)
}
@Builder
buildItemControls() {
Column({ space: 15 }) {
ForEach(this.items, (item: any, index: number) => {
Column({ space: 8 }) {
Text(`${item.name} 控制`).fontSize(14).fontWeight(FontWeight.Medium)
Row({ space: 10 }) {
Text(`flexGrow: ${item.flexGrow}`).fontSize(12).width(80)
Slider({
value: item.flexGrow,
min: 0,
max: 3,
step: 1
}).onChange((value: number) => {
item.flexGrow = value
}).layoutWeight(1)
}
Row({ space: 10 }) {
Text(`flexShrink: ${item.flexShrink}`).fontSize(12).width(80)
Slider({
value: item.flexShrink,
min: 0,
max: 3,
step: 1
}).onChange((value: number) => {
item.flexShrink = value
}).layoutWeight(1)
}
Row({ space: 5 }) {
Text('alignSelf:').fontSize(12).width(80)
Button('Auto').onClick(() => item.alignSelf = ItemAlign.Auto).stateEffect(item.alignSelf === ItemAlign.Auto)
Button('Start').onClick(() => item.alignSelf = ItemAlign.Start).stateEffect(item.alignSelf === ItemAlign.Start)
Button('Center').onClick(() => item.alignSelf = ItemAlign.Center).stateEffect(item.alignSelf === ItemAlign.Center)
Button('End').onClick(() => item.alignSelf = ItemAlign.End).stateEffect(item.alignSelf === ItemAlign.End)
}
}
.width('100%')
.padding(10)
.backgroundColor('#FFFFFF')
.borderRadius(6)
})
}
}
getItemColor(id: number): string {
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1']
return colors[(id - 1) % colors.length]
}
}
二、Stack层叠布局
2.1 Stack基础应用
@Component
struct StackBasicExample {
@State alignment: Alignment = Alignment.Center
@State stackItems: any[] = [
{ id: 1, content: '底层', width: 200, height: 150, color: '#FF6B6B', zIndex: 1 },
{ id: 2, content: '中层', width: 160, height: 120, color: '#4ECDC4', zIndex: 2 },
{ id: 3, content: '顶层', width: 120, height: 90, color: '#45B7D1', zIndex: 3 }
]
build() {
Column({ space: 20 }) {
Text('Stack层叠布局').fontSize(20).fontWeight(FontWeight.Bold)
// Stack容器
Stack({ alignContent: this.alignment }) {
ForEach(this.stackItems, (item: any) => {
Text(item.content)
.width(item.width)
.height(item.height)
.backgroundColor(item.color)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.fontSize(16)
.zIndex(item.zIndex)
})
}
.width(300)
.height(250)
.backgroundColor('#F8F9FA')
.padding(10)
// 控制面板
this.buildStackControls()
}
.width('100%')
.padding(20)
.alignItems(HorizontalAlign.Center)
}
@Builder
buildStackControls() {
Column({ space: 15 }) {
Text('对齐方式控制').fontSize(16).fontWeight(FontWeight.Medium)
// 对齐方式按钮组
Grid() {
GridItem() {
Button('TopStart').onClick(() => this.alignment = Alignment.TopStart)
.stateEffect(this.alignment === Alignment.TopStart)
}
GridItem() {
Button('Top').onClick(() => this.alignment = Alignment.Top)
.stateEffect(this.alignment === Alignment.Top)
}
GridItem() {
Button('TopEnd').onClick(() => this.alignment = Alignment.TopEnd)
.stateEffect(this.alignment === Alignment.TopEnd)
}
GridItem() {
Button('Start').onClick(() => this.alignment = Alignment.Start)
.stateEffect(this.alignment === Alignment.Start)
}
GridItem() {
Button('Center').onClick(() => this.alignment = Alignment.Center)
.stateEffect(this.alignment === Alignment.Center)
}
GridItem() {
Button('End').onClick(() => this.alignment = Alignment.End)
.stateEffect(this.alignment === Alignment.End)
}
GridItem() {
Button('BottomStart').onClick(() => this.alignment = Alignment.BottomStart)
.stateEffect(this.alignment === Alignment.BottomStart)
}
GridItem() {
Button('Bottom').onClick(() => this.alignment = Alignment.Bottom)
.stateEffect(this.alignment === Alignment.Bottom)
}
GridItem() {
Button('BottomEnd').onClick(() => this.alignment = Alignment.BottomEnd)
.stateEffect(this.alignment === Alignment.BottomEnd)
}
}
.columnsTemplate('1fr 1fr 1fr')
.rowsTemplate('1fr 1fr 1fr')
.columnsGap(5)
.rowsGap(5)
.height(120)
// zIndex控制
Text('层级控制').fontSize(16).fontWeight(FontWeight.Medium)
ForEach(this.stackItems, (item: any) => {
Row({ space: 10 }) {
Text(`${item.content} zIndex:`).fontSize(14).width(100)
Slider({
value: item.zIndex,
min: 1,
max: 5,
step: 1
}).onChange((value: number) => {
item.zIndex = value
}).layoutWeight(1)
Text(item.zIndex.toString()).fontSize(14).width(30)
}
})
}
.width('100%')
.maxWidth(400)
.padding(15)
.backgroundColor('#FFFFFF')
.borderRadius(8)
}
}
2.2 Stack实战案例
@Component
struct StackPracticalExample {
@State progress: number = 60
@State badgeCount: number = 3
@State isOnline: boolean = true
build() {
Column({ space: 30 }) {
Text('Stack实战案例').fontSize(20).fontWeight(FontWeight.Bold)
// 案例1:用户头像带状态
this.buildUserAvatar()
// 案例2:进度条叠加
this.buildProgressStack()
// 案例3:卡片叠加效果
this.buildCardStack()
}
.width('100%')
.padding(20)
}
@Builder
buildUserAvatar() {
Column({ space: 10 }) {
Text('1. 用户头像带状态').fontSize(16).fontWeight(FontWeight.Medium)
Stack({ alignContent: Alignment.BottomEnd }) {
// 头像
Image($r('app.media.user_avatar'))
.width(80)
.height(80)
.borderRadius(40)
.border({ width: 3, color: Color.White })
// 在线状态
Circle({ width: 16, height: 16 })
.fill(this.isOnline ? '#34C759' : '#FF3B30')
.stroke({ width: 2, color: Color.White })
.offset({ x: -5, y: -5 })
}
.onClick(() => {
this.isOnline = !this.isOnline
})
}
.alignItems(HorizontalAlign.Center)
}
@Builder
buildProgressStack() {
Column({ space: 10 }) {
Text('2. 进度条叠加效果').fontSize(16).fontWeight(FontWeight.Medium)
Stack() {
// 背景条
Row()
.width(200)
.height(12)
.backgroundColor('#E5E5EA')
.borderRadius(6)
// 进度条
Row()
.width(`${this.progress}%`)
.height(12)
.backgroundColor('#007DFF')
.borderRadius(6)
// 进度文本
Text(`${this.progress}%`)
.fontSize(10)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
.offset({ x: this.progress * 2 - 10, y: 0 })
}
.width(200)
.height(20)
Slider({
value: this.progress,
min: 0,
max: 100,
step: 1
})
.width(200)
.onChange((value: number) => {
this.progress = value
})
}
.alignItems(HorizontalAlign.Center)
}
@Builder
buildCardStack() {
Column({ space: 10 }) {
Text('3. 卡片叠加效果').fontSize(16).fontWeight(FontWeight.Medium)
Stack({ alignContent: Alignment.Center }) {
// 底层卡片
Column({ space: 10 }) {
Text('卡片标题')
.fontSize(16)
.fontWeight(FontWeight.Bold)
Text('这是卡片内容描述信息')
.fontSize(12)
.fontColor('#666')
}
.width(180)
.height(120)
.padding(15)
.backgroundColor('#FFECB3')
.borderRadius(12)
.shadow({ radius: 8, color: '#40000000', offsetX: 0, offsetY: 4 })
.offset({ x: -5, y: -5 })
.zIndex(1)
// 中层卡片
Column({ space: 10 }) {
Text('卡片标题')
.fontSize(16)
.fontWeight(FontWeight.Bold)
Text('这是卡片内容描述信息')
.fontSize(12)
.fontColor('#666')
}
.width(180)
.height(120)
.padding(15)
.backgroundColor('#C8E6C9')
.borderRadius(12)
.shadow({ radius: 8, color: '#40000000', offsetX: 0, offsetY: 4 })
.offset({ x: 0, y: 0 })
.zIndex(2)
// 顶层卡片
Column({ space: 10 }) {
Text('卡片标题')
.fontSize(16)
.fontWeight(FontWeight.Bold)
Text('这是卡片内容描述信息')
.fontSize(12)
.fontColor('#666')
}
.width(180)
.height(120)
.padding(15)
.backgroundColor('#BBDEFB')
.borderRadius(12)
.shadow({ radius: 12, color: '#40000000', offsetX: 0, offsetY: 6 })
.offset({ x: 5, y: 5 })
.zIndex(3)
}
.height(140)
}
.alignItems(HorizontalAlign.Center)
}
}
三、Grid网格布局
3.1 Grid基础布局
@Component
struct GridBasicExample {
@State columnsTemplate: string = '1fr 1fr 1fr'
@State rowsTemplate: string = '1fr 1fr'
@State columnsGap: number = 10
@State rowsGap: number = 10
@State gridItems: number[] = [1, 2, 3, 4, 5, 6]
build() {
Column({ space: 20 }) {
Text('Grid网格布局').fontSize(20).fontWeight(FontWeight.Bold)
// Grid容器
Grid() {
ForEach(this.gridItems, (item: number) => {
GridItem() {
Text(`Item ${item}`)
.width('100%')
.height('100%')
.backgroundColor(this.getItemColor(item))
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.fontSize(14)
}
})
}
.columnsTemplate(this.columnsTemplate)
.rowsTemplate(this.rowsTemplate)
.columnsGap(this.columnsGap)
.rowsGap(this.rowsGap)
.width('100%')
.height(200)
.padding(10)
.backgroundColor('#F8F9FA')
// 控制面板
this.buildGridControls()
}
.width('100%')
.padding(20)
}
@Builder
buildGridControls() {
Column({ space: 15 }) {
Text('Grid布局控制').fontSize(16).fontWeight(FontWeight.Medium)
// 列模板控制
Row({ space: 10 }) {
Text('列模板:').fontSize(14).width(80)
TextInput({ placeholder: '例如: 1fr 1fr 1fr' })
.onChange((value: string) => {
this.columnsTemplate = value
})
.layoutWeight(1)
}
// 行模板控制
Row({ space: 10 }) {
Text('行模板:').fontSize(14).width(80)
TextInput({ placeholder: '例如: 1fr 1fr' })
.onChange((value: string) => {
this.rowsTemplate = value
})
.layoutWeight(1)
}
// 间距控制
Row({ space: 10 }) {
Text('列间距:').fontSize(14).width(80)
Slider({
value: this.columnsGap,
min: 0,
max: 30,
step: 1
}).onChange((value: number) => {
this.columnsGap = value
}).layoutWeight(1)
Text(this.columnsGap.toString()).fontSize(14).width(30)
}
Row({ space: 10 }) {
Text('行间距:').fontSize(14).width(80)
Slider({
value: this.rowsGap,
min: 0,
max: 30,
step: 1
}).onChange((value: number) => {
this.rowsGap = value
}).layoutWeight(1)
Text(this.rowsGap.toString()).fontSize(14).width(30)
}
// 预设模板
Text('预设模板:').fontSize(14)
Row({ space: 5 }) {
Button('2x2').onClick(() => {
this.columnsTemplate = '1fr 1fr'
this.rowsTemplate = '1fr 1fr'
})
Button('3x2').onClick(() => {
this.columnsTemplate = '1fr 1fr 1fr'
this.rowsTemplate = '1fr 1fr'
})
Button('4x3').onClick(() => {
this.columnsTemplate = '1fr 1fr 1fr 1fr'
this.rowsTemplate = '1fr 1fr 1fr'
})
Button('不规则').onClick(() => {
this.columnsTemplate = '2fr 1fr 1fr'
this.rowsTemplate = '100px 1fr'
})
}
}
.width('100%')
.padding(15)
.backgroundColor('#FFFFFF')
.borderRadius(8)
}
getItemColor(index: number): string {
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FECA57', '#FF9FF3']
return colors[index % colors.length]
}
}
3.2 Grid高级应用
@Component
struct GridAdvancedExample {
@State gridData: any[] = [
{ id: 1, name: '音乐', icon: '🎵', span: { column: 1, row: 1 } },
{ id: 2, name: '视频', icon: '🎬', span: { column: 1, row: 1 } },
{ id: 3, name: '游戏', icon: '🎮', span: { column: 2, row: 1 } },
{ id: 4, name: '阅读', icon: '📚', span: { column: 1, row: 2 } },
{ id: 5, name: '社交', icon: '👥', span: { column: 1, row: 1 } },
{ id: 6, name: '工具', icon: '🛠️', span: { column: 1, row: 1 } }
]
build() {
Column({ space: 20 }) {
Text('Grid高级应用 - 不规则网格').fontSize(20).fontWeight(FontWeight.Bold)
// 不规则Grid布局
Grid() {
ForEach(this.gridData, (item: any) => {
GridItem(item.span) {
Column({ space: 8 }) {
Text(item.icon)
.fontSize(24)
Text(item.name)
.fontSize(12)
.fontColor(Color.White)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.backgroundColor(this.getCategoryColor(item.id))
.borderRadius(8)
.onClick(() => {
console.log(`点击了: ${item.name}`)
})
}
})
}
.columnsTemplate('1fr 1fr 1fr')
.rowsTemplate('80px 80px 80px')
.columnsGap(10)
.rowsGap(10)
.width('100%')
.height(280)
.padding(10)
.backgroundColor('#F8F9FA')
// 网格项详细信息
this.buildGridInfo()
}
.width('100%')
.padding(20)
}
@Builder
buildGridInfo() {
Column({ space: 10 }) {
Text('网格项详细信息').fontSize(16).fontWeight(FontWeight.Medium)
Grid() {
ForEach(this.gridData, (item: any) => {
GridItem() {
Row({ space: 8 }) {
Text(item.icon).fontSize(16)
Column({ space: 2 }) {
Text(item.name).fontSize(12).fontWeight(FontWeight.Medium)
Text(`跨${item.span.column}列${item.span.row}行`).fontSize(10).fontColor('#666')
}
}
.width('100%')
.padding(8)
.backgroundColor('#FFFFFF')
.borderRadius(6)
}
})
}
.columnsTemplate('1fr 1fr')
.rowsTemplate('1fr 1fr 1fr')
.columnsGap(8)
.rowsGap(8)
.width('100%')
.height(120)
}
}
getCategoryColor(id: number): string {
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FECA57', '#FF9FF3']
return colors[(id - 1) % colors.length]
}
}
需要参加鸿蒙认证的请点击 鸿蒙认证链接

浙公网安备 33010602011771号