HarmonyOS《柚兔学伴》项目实战10-半模态与弹窗交互

第10篇:半模态与弹窗交互

在 HarmonyOS 应用开发中,半模态(Half Modal)和自定义弹窗是两种常见的高频交互形态。半模态从页面底部滑出,不会完全遮挡底层内容,适合表单填写、筛选等场景;自定义弹窗则用于确认、提醒等强反馈交互。本文结合"柚兔学伴"项目中 TodoView.ets 的真实代码,详解这两种交互的实现方式。


一、bindSheet 半模态绑定

HarmonyOS 提供了 bindSheet 修饰符,可以将任意自定义 Builder 以半模态面板的形式绑定到组件上。当绑定的状态变量为 true 时,面板从底部弹出;为 false 时自动收起。

1.1 基本用法

在项目中,TodoView 通过 isShowTask 状态变量控制任务添加面板的显示与隐藏:

typescript 复制代码
@State isShowTask: boolean = false
@State sheetHeight: number = 300;

点击添加按钮时,将 isShowTask 置为 true

typescript 复制代码
Image($r('app.media.ic_add')).width(20)
  .onClick(() => {
    this.isShowTask = true
  })

然后在 Scroll 组件上绑定半模态:

typescript 复制代码
Scroll(this.scroller) {
  // ...页面内容
}
.bindSheet($$this.isShowTask, this.todoAddBuilder(), {
  blurStyle: BlurStyle.Thick,
  dragBar: true,
  title: this.DialogTitle('今日任务', '添加', async () => {
    // 添加逻辑
  }),
  height: this.sheetHeight,
  keyboardAvoidMode: SheetKeyboardAvoidMode.TRANSLATE_AND_RESIZE
})

1.2 关键参数解析

参数 说明
$$this.isShowTask 双向绑定状态变量,$$ 语法确保半模态内部(如拖拽关闭)也能同步修改状态
this.todoAddBuilder() 自定义 Builder,作为半模态的内容区
blurStyle 底层页面的模糊样式,BlurStyle.Thick 表示深度模糊
dragBar 是否显示顶部拖拽条,方便用户下拉关闭
title 半模态标题栏,支持自定义 Builder
height 半模态高度,可动态设置
keyboardAvoidMode 键盘避让模式

其中 keyboardAvoidMode: SheetKeyboardAvoidMode.TRANSLATE_AND_RESIZE 是表单场景的关键配置------当软键盘弹出时,半模态面板会同时平移和缩放,确保输入框不被遮挡。


二、自定义 Sheet 标题栏

半模态的 title 参数不仅支持纯文本,还支持传入自定义 Builder。项目中实现了一个 DialogTitle Builder,左侧显示标题文字,右侧放置操作按钮:

typescript 复制代码
@Builder
DialogTitle(title: string, btnText: string, onClick: () => void) {
  Row() {
    Text(title).fontWeight(FontWeight.Bold)
    Button(btnText)
      .backgroundColor(Color.Blue)
      .fontColor(Color.White)
      .onClick(onClick)
  }.width('100%').justifyContent(FlexAlign.SpaceBetween)
}

bindSheet 中使用:

typescript 复制代码
title: this.DialogTitle('今日任务', '添加', async () => {
  if (StrUtil.isEmpty(this.value)) {
    ToastUtil.showToast("任务内容不能为空")
    return
  }
  if (StrUtil.isEmpty(this.startTime)) {
    ToastUtil.showToast("开始时间不能为空")
    return
  }
  if (StrUtil.isEmpty(this.stopTime)) {
    ToastUtil.showToast("结束时间不能为空")
    return
  }
  // 验证通过,添加待办事项
  const newTodo: TodoItem = {
    date: DateUtil.getTodayStr('yyyy-MM-dd'),
    dayOfWeek: '周' + DateUtil.getWeekDay(DateUtil.getTodayStr('HH-mm-ss')),
    time: `${this.startTime}-${this.stopTime}`,
    content: this.value,
    isCompleted: false
  };
  const todoId = await this.todoDb.addTodo(newTodo);
  this.arr = await this.todoDb.getAllTodos();
  this.startTime = this.stopTime = ''
  this.isShowTask = false
  this.isTodoEmpty = false
})

这种设计将表单验证逻辑集中在标题栏按钮的回调中,验证通过后关闭面板(isShowTask = false),验证失败则通过 Toast 提示用户。


三、IBestField 输入组件

项目使用了 @ibestservices/ibest-ui 三方库提供的 IBestField 组件作为输入框。它内置了标签、边框、圆角等样式配置:

typescript 复制代码
IBestField({
  label: '任务名称',
  value: $value,
  showLabel: false,
  placeholder: "请输入今日任务",
  hasBorder: false,
  radius: 12,
  onChange: (input) => {
    this.value = input.toString()
  }
}).width('100%')
  • value: $value:通过 $ 语法进行双向绑定,输入内容自动同步到 this.value
  • showLabel: false:隐藏左侧标签,因为半模态空间有限
  • hasBorder: false:去除默认边框,与面板整体风格统一
  • onChange:内容变化时的回调

四、IBestCell + showDialogTime 时间选择

时间选择采用 IBestCell(单元格)+ showDialogTime(时间弹窗)的组合模式。IBestCell 以列表项的形式展示当前值,点击后弹出时间选择器:

typescript 复制代码
IBestCell({
  title: "开始时间",
  value: this.startTime ? this.startTime : '请选择开始时间',
  isLink: true,
  onCellClick: (() => {
    showDialogTime({
      titleBarAttribute: {
        titleText: "请选择开始时间",
        backgroundColor: $r('app.color.color_card')
      },
      timeAttribute: {
        timeType: TimeDialogType.HM,
        startHours: 6,
        startMinutes: 0,
        endHours: 22,
        endMinutes: 59,
      },
      backgroundColor: $r('app.color.color_card'),
      timeConfirmClick: (date) => {
        let formatter = new Intl.DateTimeFormat('zh-CN', {
          hour: '2-digit',
          minute: '2-digit',
          hour12: false
        });
        let result: string = formatter.format(date);
        this.startTime = result
      },
      confirmClick: (value, index) => {
        console.log("===内容结果:" + value + "====" + index)
      }
    })
  })
})

4.1 TimeDialogType

@abner/dialog 库的 showDialogTime 支持 TimeDialogType 枚举:

  • TimeDialogType.HM:仅选择时和分(小时:分钟),适合任务时间选择
  • TimeDialogType.HMS:选择时、分、秒,适合倒计时精确设置

4.2 Intl.DateTimeFormat 时间格式化

项目使用 JavaScript 内置的 Intl.DateTimeFormat API 进行时间格式化:

typescript 复制代码
let formatter = new Intl.DateTimeFormat('zh-CN', {
  hour: '2-digit',
  minute: '2-digit',
  hour12: false
});
let result: string = formatter.format(date);
this.startTime = result
  • 'zh-CN':中文 locale,确保格式符合中国用户习惯
  • hour12: false:强制 24 小时制,避免上下午歧义
  • '2-digit':两位数显示,如 08:00 而非 8:0

五、IBestDialog 自定义弹窗

IBestDialog@ibestservices/ibest-ui 提供的自定义弹窗组件,支持通过 $$ 双向绑定控制可见性,并通过 defaultBuilder 自定义内容区。

5.1 倒计时结束弹窗

typescript 复制代码
@State alarmVisible: boolean = false

IBestDialog({
  visible: $alarmVisible,
  defaultBuilder: (): void => this.alarmBuild(),
  onConfirm: (() => {
    this.alarmVisible = false
    this.timerController.reset()
  })
})

弹窗内容通过 alarmBuild Builder 定义,包含闹钟动画和提示文字:

typescript 复制代码
@Builder
alarmBuild() {
  Column({ space: 15 }) {
    Lottie({
      controller: this.alarmController,
      animationPath: 'lottie/lottie_alarm.json',
      autoPlay: true,
      loop: true,
    })
      .width(80)
      .height(80)

    Text('时间到').fontColor($r('app.color.app_primary')).fontWeight(FontWeight.Bold)
  }.padding(15)
}

5.2 任务完成弹窗

typescript 复制代码
@State finishVisible: boolean = false

IBestDialog({
  visible: $finishVisible,
  defaultBuilder: (): void => this.finishBuild(),
  onConfirm: (() => {
    this.finishVisible = false
    this.timerController.reset()
  })
})

两个弹窗的触发时机不同:

  • alarmVisible:倒计时归零时设为 true
  • finishVisible:所有任务完成时设为 true
typescript 复制代码
// 倒计时结束
onTimerFinished = async () => {
  this.alarmVisible = true
}

// 任务切换时检查
onToggleChange: (value) => {
  item.isCompleted = value;
  this.finishVisible = this.arr.every(item => item.isCompleted === true);
}

六、表单验证模式

项目使用 @pura/harmony-utils 提供的 StrUtil.isEmpty 方法进行非空校验,形成三级验证链:

typescript 复制代码
if (StrUtil.isEmpty(this.value)) {
  ToastUtil.showToast("任务内容不能为空")
  return
}
if (StrUtil.isEmpty(this.startTime)) {
  ToastUtil.showToast("开始时间不能为空")
  return
}
if (StrUtil.isEmpty(this.stopTime)) {
  ToastUtil.showToast("结束时间不能为空")
  return
}

这种逐项验证 + 提前返回的模式,确保用户能明确知道哪个字段未填写,而非一次性弹出所有错误。


七、小结

交互方式 适用场景 核心API
半模态 bindSheet 表单填写、筛选 bindSheet($$state, builder, options)
时间弹窗 showDialogTime 时间选择 TimeDialogType.HM / HMS
自定义弹窗 IBestDialog 确认、提醒、庆祝 visible: $$state, defaultBuilder
表单验证 数据完整性 StrUtil.isEmpty + ToastUtil

关键设计要点:

  1. **双向绑定∗∗:半模态和弹窗的'visible'都需要使用' 双向绑定**:半模态和弹窗的 `visible` 都需要使用 `双向绑定∗∗:半模态和弹窗的'visible'都需要使用'` 语法,确保组件内部的关闭操作能同步到外部状态
  2. 键盘避让 :半模态内包含输入框时,务必设置 keyboardAvoidMode
  3. 验证前置:在关闭半模态之前完成验证,避免无效数据入库
  4. 弹窗分层:根据业务含义(闹钟提醒 vs 完成庆祝)使用不同的弹窗实例和动画内容
相关推荐
不羁的木木1 小时前
HarmonyOS APP实战-基于Image Kit的图像处理APP - 第8篇:图片编码与保存
图像处理·华为·harmonyos
春卷同学1 小时前
HarmonyOS掌上记账APP开发实践第2篇:从零搭建记账应用 — MoneyTrack 工程全景解析![
华为·harmonyos
春卷同学1 小时前
HarmonyOS掌上记账APP开发实践第1篇:HarmonyOS 多模块 HAR 架构最佳实践 — MoneyTrack 多模块拆分之道
华为·架构·harmonyos
g_z_x_3 小时前
鸿蒙 ArkTS 实战:Quiz Arena 从状态建模到交互闭环完整解析
华为·交互·harmonyos
用户0934077735143 小时前
HarmonyOS WPS Open SDK 实践:最小打开闭环与 enableEdit 分支处理
typescript·harmonyos
绝世番茄3 小时前
鸿蒙原生 ArkTS 布局方式之 Stack 悬浮按钮布局(HarmonyOS NEXT / API 24)
华为·harmonyos·鸿蒙
李白你好3 小时前
一款面向攻防演练、红蓝对抗和安全研究场景的高交互智能欺骗蜜罐平台
安全·交互
2301_768103493 小时前
HarmonyOS趣味相机实战第5篇:CameraKit资源释放、PixelMap生命周期与预览黑屏排查
性能优化·harmonyos·arkts·camerakit·pixelmap
花开彼岸天~4 小时前
鸿蒙应用开发实战【01】— 号码助手项目概述与开发环境搭建
华为·harmonyos·鸿蒙系统