
回复
当前正在开发一个运动健康类的鸿蒙应用。在实现消息提醒功能时,我发现系统原生的弹窗样式与产品设计图相差甚远。于是,我踏上了探索鸿蒙自定义弹窗的旅程。这篇手记就记录了这个过程中遇到的挑战和最终的解决方案。
我们需要一个支持如下特性的提醒组件:
设计要求的圆角渐变弹窗,系统默认的样式并不能满足。所以尝试一下自定义实现这个组件。
通过对鸿蒙开发文档的研究,发现ArkUI提供了两种实现方式:
CustomDialogController
控制器@CustomDialog
装饰器考虑到需要灵活控制动画效果,我选择了第一种方案。核心思路是通过状态管理控制弹窗显隐,结合属性动画实现弹性效果。同时利用父子组件通信机制传递用户输入数据。
// 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%')
}
}
这个基础版本存在两个问题:缺乏动画效果,父子组件间无法传递输入数据。
通过研究动画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让弹窗有"弹出"的视觉感受。
为传递用户输入,采用事件回调机制:
@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;
}
})
}
}
}