【HarmonyOS NEXT】使用半模态实现动态高度底部弹窗

一、背景

在开发过程中,底部弹窗是一种常见的交互方式,下面总结如何实现高度根据内容动态调整的底部弹窗,并提供两种实现方案

常见场景:当弹窗内容由动态数据驱动时(比如商品详情、任务列表、评论区等),内容高度可能随数据量变化

  • 数据少时弹窗矮一点
  • 数据多时弹窗高一点(但不超过屏幕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 =
    ['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 =
      ['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
    })
  }
}

三、实现效果

【HarmonyOS NEXT】使用半模态实现动态高度底部弹窗_第1张图片

四、关键点说明

  • ScreenUtils.getScreenHeightVp():获取屏幕高度(vp单位),需自行实现(可通过@SystemScreen接口获取)
  • layoutConstraint({ maxHeight: ... }):限制列表最大高度,超出自动滚动
  • layoutWeight(1):让列表占满头部和底部之外的所有空间

你可能感兴趣的:(鸿蒙,HarmonyOS,windows,linux,服务器)