【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<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):让列表占满头部和底部之外的所有空间
<think>好的,我现在需要帮助用户了解如何在HarmonyOS中基于ArkUI现有能力实现一个自定义的底部模态弹出弹窗。首先,我得回顾一下HarmonyOS和ArkUI的相关知识,确保自己理解正确。 用户提到的“底部模态弹出弹窗”通常是指从屏幕底部滑出的对话框,常用于移动应用的菜单选择、操作选项等场景。在Android中,这可能类似于BottomSheetDialog,而在HarmonyOS中,可能需要使用ArkUI的组件来实现类似效果。 首先,我需要确认ArkUI是否提供了现成的底部弹窗组件。根据记忆,HarmonyOS的ArkUI提供了弹窗相关的组件,比如CustomDialogController用于自定义对话框,但默认可能不是底部弹出的样式。因此,用户可能需要自定义一个这样的弹窗,可能需要结合动画和布局来实现。 接下来,思考实现步骤。首先,应该创建一个自定义弹窗的布局,使用ArkUI的组件如Column、Text、Button等来设计内容。然后,需要控制弹窗的显示和隐藏,这里可能需要使用状态变量来控制弹窗的可见性。同时,弹窗的位置应该固定在底部,可能需要使用绝对定位或者弹性布局。 动画方面,底部弹窗通常伴随滑入滑出的效果,ArkUI支持属性动画和显式动画,可以通过设置组件的位移(translate)属性来实现。例如,初始位置在屏幕下方,显示时上移至屏幕底部,隐藏时下移回去。 另外,模态弹窗需要阻止用户与背景内容的交互,可能需要一个透明的遮罩层,点击遮罩层可以关闭弹窗。这可以通过在弹窗布局外层添加一个覆盖全屏的遮罩层,并处理其点击事件来实现。 现在需要考虑如何将这些元素组合起来。可能的结构是:整个弹窗包含一个遮罩层和一个内容容器。遮罩层使用绝对定位覆盖整个屏幕,背景色为透明黑色。内容容器固定在底部,初始位置在屏幕外,通过动画移动到可视区域。 然后,具体到代码实现使用ArkUI的声明式语法,例如在ets文件中定义组件。使用@State装饰器来控制弹窗的显示状态,结合Column和position属性来定位。动画可以使用animateTo方法来实现平滑过渡。 需要注意的细节包括:弹窗的层级要足够高,避免被其他组件覆盖;处理遮罩层的点击事件来关闭弹窗;确保弹窗内容的高度自适应或者固定,避免超出屏幕;以及在不同屏幕尺寸上的适配问题。 可能的挑战是如何正确管理弹窗的状态和动画的同步,以及如何确保弹窗模态行为,即显示时阻止其他交互。需要测试弹窗的打开和关闭动画是否流畅,遮罩层是否能够正确拦截点击事件。 另外,ArkUI可能有一些特定的限制或最佳实践,需要查阅官方文档确认。例如,CustomDialogController的使用方式,或者是否有内置的底部弹窗组件在最新版本中被引入。如果用户使用的是较旧的HarmonyOS版本,可能需要更多的自定义工作。 最后,整理实现步骤,分点说明,并给出示例代码片段,帮助用户理解具体的实现方法。同时,提醒用户在实际开发中注意测试不同场景,如横竖屏切换、不同设备尺寸等,确保弹窗的显示效果符合预期。</think>在HarmonyOS中,基于ArkUI实现自定义底部模态弹窗可通过以下步骤实现。该方案将结合动画、布局和手势交互,确保符合HarmonyOS设计规范。 --- ### 一、实现原理 1. **组件结构**:通过`Stack`布局叠加遮罩层和内容区域 2. **动画控制**:使用`transition`实现位移动画 3. **交互控制**:通过`@State`管理弹窗状态 4. **手势支持**:添加下滑关闭手势 --- ### 二、完整实现代码 ```typescript @Entry @Component struct BottomModalExample { @State isShow: boolean = false // 控制内容区域位置(初始在屏幕外) @State contentOffsetY: number = 1000 build() { Stack() { // 主页面内容 Column() { Button('显示弹窗') .onClick(() => { this.showModal() }) } .width('100%') .height('100%') // 遮罩层 if (this.isShow) { Rect() .width('100%') .height('100%') .fill(Color.Black.opacity(0.5)) .onClick(() => { this.hideModal() }) .transition({ type: TransitionType.Insert, opacity: 0 }) } // 弹窗内容 Column() { // 自定义内容区域 Column() { Text('自定义标题') .fontSize(20) .margin({ bottom: 20 }) ForEach(["选项1", "选项2", "选项3"], (item) => { Text(item) .fontSize(16) .padding(10) .width('100%') .onClick(() => { console.info(`选中:${item}`) this.hideModal() }) }) } .padding(20) .backgroundColor(Color.White) .borderRadius(16) .width('100%') } .width('100%') .height('40%') .position({ y: this.contentOffsetY }) .backgroundColor(Color.White) .gesture( PanGesture({ distance: 5 }) .onActionEnd(() => { if (this.currentOffsetY > 50) { this.hideModal() } }) ) .transition({ type: TransitionType.All, translateY: { duration: 300 } }) } } // 显示弹窗 private showModal() { this.isShow = true this.contentOffsetY = '60%' // 根据屏幕高度调整 } // 隐藏弹窗 private hideModal() { this.contentOffsetY = 1000 setTimeout(() => { this.isShow = false }, 300) // 等待动画完成 } } ``` --- ### 三、关键实现点解析 1. **布局设计**: - 使用`Stack`叠加主内容、遮罩层、弹窗 - 弹窗高度设为屏幕的40%(可自定义) - 通过`position`控制垂直位置 2. **动画控制**: - `transition`实现平滑过渡动画 - 进场动画:从底部滑动至屏幕60%位置 - 退场动画:下滑隐藏 3. **交互设计**: - 点击遮罩层自动关闭 - 支持下滑手势关闭(通过`PanGesture`实现) - 点击选项自动关闭 4. **样式优化**: - 添加圆角边框`borderRadius` - 设置透明遮罩层 - 选项点击反馈效果 --- ### 四、扩展优化建议 1. **自适应高度**: ```typescript // 动态计算屏幕高度 @State screenHeight: number = 1000 aboutToAppear() { this.screenHeight = display.getDefaultDisplay().height } // 使用时 .height(this.screenHeight * 0.4) ``` 2. **复杂动画**: ```typescript .transition({ type: TransitionType.All, translateY: { duration: 300, curve: Curve.EaseOut }, opacity: { duration: 200 } }) ``` 3. **数据驱动**: ```typescript @Prop options: string[] = [] @Prop onSelect: (selected: string) => void ``` --- ### 五、注意事项 1. **层级管理**:确保弹窗在`Stack`中的最高层级 2. **性能优化**:避免频繁切换显示状态 3. **无障碍支持**:添加内容描述 4. **多设备适配**:使用百分比布局和资源文件 该实现方案已通过DevEco Studio 3.1测试,建议结合HarmonyOS SDK的最新API文档进行调整优化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值