#我的鸿蒙开发手记# 实现一个自定义提醒弹窗 原创 精华

RandomBoolean
发布于 2025-5-7 20:58
浏览
0收藏

当前正在开发一个运动健康类的鸿蒙应用。在实现消息提醒功能时,我发现系统原生的弹窗样式与产品设计图相差甚远。于是,我踏上了探索鸿蒙自定义弹窗的旅程。这篇手记就记录了这个过程中遇到的挑战和最终的解决方案。


一、需求与设计

我们需要一个支持如下特性的提醒组件:

  1. 支持自定义标题和内容
  2. 可配置确定/取消按钮
  3. 带有弹性动画效果
  4. 支持用户输入反馈

设计要求的圆角渐变弹窗,系统默认的样式并不能满足。所以尝试一下自定义实现这个组件。

二、技术方案选择

通过对鸿蒙开发文档的研究,发现ArkUI提供了两种实现方式:

  1. 使用​​CustomDialogController​​控制器
  2. 基于​​@CustomDialog​​装饰器

考虑到需要灵活控制动画效果,我选择了第一种方案。核心思路是通过状态管理控制弹窗显隐,结合属性动画实现弹性效果。同时利用父子组件通信机制传递用户输入数据。


三、开发实战记录

3.1 基础弹窗构建

// CustomDialog.ets
@Component
struct CustomDialog {
  @Link title: string;
  @Link content: string;
  @State inputText: string = '';

  controller: CustomDialogController = new CustomDialogController({
    builder: this,
    cancel: this.closeDialog,
    autoCancel: true
  });

  closeDialog() {
    this.controller.close();
  }

  build() {
    Column() {
      Text(this.title)
        .fontSize(20)
        .fontColor(Color.Black)

      Text(this.content)
        .margin({top:10})

      TextInput({ text: this.inputText })
        .onChange((value) => {
          this.inputText = value;
        })

      Flex({ direction: FlexDirection.Row }) {
        Button('确定')
          .onClick(() => {
            // 提交逻辑
            this.closeDialog();
          })
        Button('取消')
          .onClick(this.closeDialog)
      }
    }
    .padding(20)
    .backgroundColor(Color.White)
    .borderRadius(16)
    .width('60%')
  }
}

这个基础版本存在两个问题:缺乏动画效果,父子组件间无法传递输入数据。

3.2 添加弹性动画

通过研究动画API,为弹窗添加了入场动画:

@Extend(Column) function popupAnimation() {
  .scale({ x: 0.8, y: 0.8 })
  .opacity(0)
  .animate({
    duration: 300,
    curve: Curve.Spring
  })
}

// 在build方法中应用
.build() {
  Column() {
    // ...原有内容
  }
  .popupAnimation()
}

这里使用了Spring曲线实现弹性效果,scale结合opacity让弹窗有"弹出"的视觉感受。


3.3 实现数据通信

为传递用户输入,采用事件回调机制:

@CustomDialog
struct CustomDialog {
  @Consume onSubmit: (text: string) => void;

  // 确定按钮事件修改为
  Button('确定')
    .onClick(() => {
      this.onSubmit(this.inputText);
      this.closeDialog();
    })
}

// 父组件使用
@Component
struct HomePage {
  @State showDialog: boolean = false;
  
  handleSubmit(input: string) {
    // 处理提交数据
  }

  build() {
    Column() {
      Button('打开弹窗')
        .onClick(() => this.showDialog = true)
      
      if (this.showDialog) {
        CustomDialog({
          title: '健康提醒',
          content: '请输入今天的目标步数:',
          onSubmit: this.handleSubmit.bind(this)
        })
      }
    }
  }
}

这就实现了完整的输入数据流:用户输入 -> 弹窗提交 -> 父组件处理。

四、完整示例代码

请确保在entry>src>main>ets>components下创建这两个文件:

​SmartDialog.ets​​ - 弹窗组件

@CustomDialog
export struct SmartDialog {
  @Link title: string;
  @Link placeholder: string;
  @Consume onSubmit: (value: string) => void;
  
  @State private inputValue: string = '';
  controller: CustomDialogController;

  @Extend(Column) static dialogStyle() {
    .width('70%')
    .padding(24)
    .backgroundColor(Color.White)
    .borderRadius(24)
    .shadow({ radius: 16, color: Color.Gray })
  }

  build() {
    Column() {
      Text(this.title)
        .fontSize(24)
        .fontWeight(FontWeight.Bold)

      TextInput({ placeholder: this.placeholder })
        .width('100%')
        .margin(16)
        .onChange((value) => this.inputValue = value)

      Flex({ justifyContent: FlexAlign.SpaceAround }) {
        Button('取消', { type: ButtonType.Normal })
          .onClick(() => this.controller.close())

        Button('确认', { type: ButtonType.Capsule })
          .backgroundColor('#4CAF50')
          .onClick(() => {
            this.onSubmit(this.inputValue);
            this.controller.close();
          })
      }
    }
    .smartDialogStyle()
    .enterAnimation({ duration: 300, curve: Curve.EaseOut })
  }
}

​MainPage.ets​ - 示例页面

import { SmartDialog } from './SmartDialog'

@Entry
@Component
struct MainScreen {
  @State targetSteps: string = '';
  @State dialogVisible: boolean = false;

  build() {
    Column() {
      Text(`今日目标:${this.targetSteps}步`)
        .margin(24)

      Button('设置步数目标')
        .type(ButtonType.Capsule)
        .onClick(() => this.dialogVisible = true)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)

    if (this.dialogVisible) {
      SmartDialog({
        title: '运动目标设置',
        placeholder: '请输入步数(建议8000以上)',
        onSubmit: (value) => {
          if (Number(value) > 0) {
            this.targetSteps = value;
          }
          this.dialogVisible = false;
        }
      })
    }
  }
}



©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
分类
标签
收藏
回复
举报
回复
    相关推荐