一、背景
在开发过程中,底部弹窗是一种常见的交互方式,下面总结如何实现高度根据内容动态调整的底部弹窗,并提供两种实现方案
常见场景:当弹窗内容由动态数据驱动时(比如商品详情、任务列表、评论区等),内容高度可能随数据量变化
- 数据少时弹窗矮一点
- 数据多时弹窗高一点(但不超过屏幕80%)
- 支持拖拽收起、点击空白关闭
- 头部/底部可能有固定高度的模块(如标题栏、操作按钮)
二、实现步骤
第一步:创建基础底部弹窗
推荐使用半模态弹窗,它自带点击空白处关闭和拖拽收起功能:
Button("底部弹窗动态高度1").height("40").width("100%")
.onClick(() => {
this.isShowSheet1 = true
})
.bindSheet(this.isShowSheet1, this.sheet1(), {
preferType: SheetType.BOTTOM,
maskColor: $r('app.color.black_alpha_50'),
showClose: false,
height: SheetSize.FIT_CONTENT,
radius: { topStart: LengthMetrics.vp(16), topEnd: LengthMetrics.vp(16) },
onWillDismiss: (dismissSheetAction: DismissSheetAction) => {
this.isShowSheet1 = false
},
onWillSpringBackWhenDismiss: ((springBackAction: SpringBackAction) => {
})
})
第二步:实现动态高度
根据需求选择以下两种方式之一:
方式1:固定头部/底部高度
适合头部和底部高度固定的场景:
@Builder
sheet1() {
Column() {
Text("头部").height(40)
List() {
ForEach(this.Sheet1Array, (item: string) => {
ListItem() {
Text(item).fontSize(14).width("100%")
}
})
}
// 关键:设置最大高度 = 弹窗最大高度 - 头部高度 - 底部高度
.constraintSize({
maxHeight: this.getScreenHeightVp() * 0.8 - 80
})
Text("底部").height(40)
}
// 设置弹窗整体最大高度(屏幕高度的80%)
.constraintSize({
maxHeight: this.getScreenHeightVp() * 0.8
})
}
方式1完整代码:
import { LengthMetrics } from '@kit.ArkUI'
import { ScreenUtils } from '@tbs/common'
//动态高度
@Component
export struct MinePage {
@State isShowSheet1: boolean = false
@State Sheet1Array: Array<string> =
['text0', 'text1', 'text2', 'text3', 'text4', 'text5', 'text6', 'text7', 'text8', 'text9', 'text10', 'text11',
'text12', 'text13', 'text14', 'text15', 'text16', 'text17', 'text18', 'text19', 'text20', 'text0', 'text1',
'text2', 'text3', 'text4', 'text5', 'text6', 'text7', 'text8', 'text9', 'text10', 'text11',
'text12', 'text13', 'text14', 'text15', 'text16', 'text17', 'text18', 'text19', 'text20','text0', 'text1', 'text2', 'text3', 'text4', 'text5', 'text6', 'text7', 'text8', 'text9', 'text10', 'text11',
'text12','text13','text14','text15','text16','text17','text18','text19','text20']
build() {
Button("底部弹窗动态高度1").height("40").width("100%")
.onClick(() => {
this.isShowSheet1 = true
})
.bindSheet(this.isShowSheet1, this.sheet1(), {
preferType: SheetType.BOTTOM,
maskColor: Color.Black,
showClose: false,
height: SheetSize.FIT_CONTENT,
radius: { topStart: LengthMetrics.vp(16), topEnd: LengthMetrics.vp(16) },
onWillDismiss: (dismissSheetAction: DismissSheetAction) => {
this.isShowSheet1 = false
},
onWillSpringBackWhenDismiss: ((springBackAction: SpringBackAction) => {
})
})
}
@Builder
sheet1() {
Column() {
Text("头部").height(40)
List() {
ForEach(this.Sheet1Array, (item: string) => {
ListItem() {
Text(item).fontSize(14).width("100%")
}
})
}.constraintSize({
maxHeight: ScreenUtils.getInstance().getScreenHeightVp() * 0.8 - 80
})
Text("底部").height(40)
}
.constraintSize({
maxHeight: ScreenUtils.getInstance().getScreenHeightVp() * 0.8
})
}
}
方式2:自适应高度
适合头部或底部高度不确定的场景:
@State sheetLayoutWeight: number = 0
// 创建组件观察器
content: inspector.ComponentObserver = this.getUIContext().getUIInspector().createComponentObserver('content')
aboutToAppear() {
// 监听布局变化
this.content.on('layout', () => {
const componentInfo = componentUtils.getRectangleById("content")
const height = px2vp(componentInfo.size.height)
// 如果内容高度超过屏幕80%,启用自适应布局
if (height > this.getScreenHeightVp() * 0.8) {
this.sheetLayoutWeight = 1
} else {
this.sheetLayoutWeight = 0
}
})
}
aboutToDisappear(): void {
// 清理观察器
this.content.off('layout')
}
@Builder
sheet2() {
Column() {
Text("头部").height(40)
List() {
ForEach(this.Sheet1Array, (item: string) => {
ListItem() {
Text(item).fontSize(14).width("100%")
}
})
}
.layoutWeight(this.sheetLayoutWeight)
Text("底部").height(40)
}.id("content")
.constraintSize({
maxHeight: this.getScreenHeightVp() * 0.8
})
}
方式2完整代码:
@Component
export struct MinePage {
@State isShowSheet1: boolean = false
@State sheetLayoutWeight: number = 0
content: inspector.ComponentObserver = this.getUIContext().getUIInspector().createComponentObserver('content')
@State Sheet1Array: Array<string> =
['text0', 'text1', 'text2', 'text3', 'text4', 'text5', 'text6', 'text7', 'text8', 'text9', 'text10', 'text11',
'text12', 'text13', 'text14', 'text15', 'text16', 'text17', 'text18', 'text19', 'text20', 'text0', 'text1',
'text2', 'text3', 'text4', 'text5', 'text6', 'text7', 'text8', 'text9', 'text10', 'text11',
'text12', 'text13', 'text14', 'text15', 'text16', 'text17', 'text18', 'text19', 'text20','text0', 'text1', 'text2', 'text3', 'text4', 'text5', 'text6', 'text7', 'text8', 'text9', 'text10', 'text11',
'text12','text13','text14','text15','text16','text17','text18','text19','text20']
aboutToAppear() {
this.content.on('layout', () => {
const componentInfo = componentUtils.getRectangleById("content")
const height = px2vp(componentInfo.size.height)
if (height > ScreenUtils.getInstance().getScreenHeightVp() * 0.8) {
this.sheetLayoutWeight = 1
} else {
this.sheetLayoutWeight = 0
}
})
}
aboutToDisappear(): void {
this.content.off('layout')
}
build() {
Button("底部弹窗动态高度1").height("40").width("100%")
.onClick(() => {
this.isShowSheet1 = true
})
.bindSheet(this.isShowSheet1, this.sheet2(), {
preferType: SheetType.BOTTOM,
maskColor: Color.Black,
showClose: false,
height: SheetSize.FIT_CONTENT,
radius: { topStart: LengthMetrics.vp(16), topEnd: LengthMetrics.vp(16) },
onWillDismiss: (dismissSheetAction: DismissSheetAction) => {
this.isShowSheet1 = false
},
onWillSpringBackWhenDismiss: ((springBackAction: SpringBackAction) => {
})
})
}
@Builder
sheet2() {
Column() {
Text("头部").height(40)
List() {
ForEach(this.Sheet1Array, (item: string) => {
ListItem() {
Text(item).fontSize(14).width("100%")
}
})
}
.layoutWeight(this.sheetLayoutWeight)
Text("底部").height(40)
}.id("content")
.constraintSize({
maxHeight: ScreenUtils.getInstance().getScreenHeightVp() * 0.8
})
}
}
三、实现效果
四、关键点说明
ScreenUtils.getScreenHeightVp()
:获取屏幕高度(vp单位),需自行实现(可通过@SystemScreen
接口获取)layoutConstraint({ maxHeight: ... })
:限制列表最大高度,超出自动滚动layoutWeight(1)
:让列表占满头部和底部之外的所有空间