鸿蒙工具学习二十八:openCustomDialog实现参数化菜单弹窗与状态监听
本文详细介绍了如何在HarmonyOS中使用openCustomDialog API实现高度自定义的参数化菜单弹窗。主要内容包括:1)API基础概念与核心参数解析;2)完整实现参数化菜单弹窗,涵盖数据结构设计、组件实现和控制器封装;3)高级功能扩展如搜索过滤、多选模式和动画效果;4)性能优化建议和常见问题解决方案。通过组件化设计和回调机制,实现了动态参数传递和状态监听,为开发者提供了灵活、高效的弹
引言:自定义弹窗在HarmonyOS开发中的重要性
在HarmonyOS应用开发中,弹窗是用户交互的重要组成部分。系统提供的标准弹窗虽然能满足基本需求,但在复杂业务场景下,开发者往往需要高度定制化的弹窗界面和交互逻辑。openCustomDialogAPI正是为此而生,它允许开发者创建完全自定义的弹窗组件,实现从UI布局到交互逻辑的全面控制。
本文将深入探讨如何使用openCustomDialog实现一个功能完整的参数化菜单弹窗,重点解决两个核心问题:如何从外部动态传递参数到弹窗,以及如何在外部监听菜单项的选中状态。通过完整的代码示例和最佳实践,帮助开发者掌握这一高级交互组件的实现技巧。
一、openCustomDialog基础概念
1.1 API概述
openCustomDialog是HarmonyOS提供的高级弹窗API,与简单的AlertDialog相比,它具有以下优势:
-
完全自定义UI:可以自由设计弹窗的布局、样式和动画
-
灵活的参数传递:支持通过构造函数或属性绑定传递数据
-
完整的事件处理:提供打开、关闭、交互等完整生命周期事件
-
性能优化:支持异步加载和渲染,避免阻塞主线程
1.2 核心参数解析
interface CustomDialogOptions {
builder: CustomDialogController; // 弹窗控制器
alignment?: DialogAlignment; // 对齐方式
offset?: Offset; // 偏移量
customStyle?: boolean; // 自定义样式
autoCancel?: boolean; // 点击外部是否关闭
maskColor?: ResourceColor; // 遮罩层颜色
maskOpacity?: number; // 遮罩层透明度
onWillDismiss?: () => void; // 即将关闭回调
onDismiss?: () => void; // 关闭完成回调
}
二、参数化菜单弹窗完整实现
2.1 需求分析与设计
我们要实现的菜单弹窗需要满足以下需求:
-
参数化配置:从外部传入菜单项数据、标题、样式等参数
-
状态监听:外部能够监听菜单项的选中事件
-
灵活布局:支持垂直列表、网格布局等多种展示方式
-
动画效果:提供平滑的打开/关闭动画
-
样式定制:支持自定义颜色、字体、间距等样式属性
2.2 数据结构设计
首先定义弹窗需要的数据结构:
// 菜单项数据模型
export interface MenuItem {
id: string; // 唯一标识
title: string; // 显示标题
icon?: Resource; // 图标资源
disabled?: boolean; // 是否禁用
selected?: boolean; // 是否选中
badge?: string | number; // 角标
extraData?: any; // 额外数据
}
// 弹窗配置参数
export interface MenuDialogConfig {
title?: string; // 弹窗标题
items: MenuItem[]; // 菜单项列表
layoutType?: 'list' | 'grid'; // 布局类型
columns?: number; // 网格布局列数
maxHeight?: string; // 最大高度
showCancel?: boolean; // 是否显示取消按钮
cancelText?: string; // 取消按钮文本
theme?: 'light' | 'dark'; // 主题样式
animationType?: 'fade' | 'slide' | 'scale'; // 动画类型
}
// 选中事件回调
export interface MenuSelectEvent {
item: MenuItem; // 选中的菜单项
index: number; // 选中索引
dialogController: CustomDialogController; // 弹窗控制器
}
2.3 核心组件实现
2.3.1 菜单弹窗组件
@Component
export struct MenuDialog {
// 接收外部参数
@Prop config: MenuDialogConfig;
@Prop onItemSelect?: (event: MenuSelectEvent) => void;
@Prop onDialogDismiss?: () => void;
// 弹窗控制器
@Link dialogController: CustomDialogController;
// 内部状态
@State private selectedIndex: number = -1;
@State private filteredItems: MenuItem[] = [];
aboutToAppear() {
// 初始化数据
this.filteredItems = [...this.config.items];
// 查找默认选中项
const defaultSelected = this.config.items.findIndex(item => item.selected);
if (defaultSelected >= 0) {
this.selectedIndex = defaultSelected;
}
}
build() {
Column({ space: 0 }) {
// 标题区域
if (this.config.title) {
this.buildTitle()
}
// 菜单内容区域
this.buildMenuContent()
// 取消按钮区域
if (this.config.showCancel !== false) {
this.buildCancelButton()
}
}
.width('100%')
.backgroundColor(this.getBackgroundColor())
.borderRadius(20)
.padding({ top: 24, bottom: 12, left: 16, right: 16 })
.constraintSize({
minWidth: '280vp',
maxWidth: '90%',
maxHeight: this.config.maxHeight || '70%'
})
}
// 构建标题
@Builder
private buildTitle() {
Column() {
Text(this.config.title!)
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor(this.getTextColor())
.margin({ bottom: 16 })
Divider()
.strokeWidth(1)
.color(this.getDividerColor())
.margin({ bottom: 16 })
}
}
// 构建菜单内容
@Builder
private buildMenuContent() {
if (this.config.layoutType === 'grid') {
this.buildGridMenu()
} else {
this.buildListMenu()
}
}
// 构建列表式菜单
@Builder
private buildListMenu() {
List({ space: 8 }) {
ForEach(this.filteredItems, (item: MenuItem, index: number) => {
ListItem() {
this.buildMenuItem(item, index)
}
.backgroundColor(Color.Transparent)
.enabled(!item.disabled)
.onClick(() => {
if (!item.disabled) {
this.handleItemSelect(item, index)
}
})
})
}
.width('100%')
.divider({
strokeWidth: 1,
color: this.getDividerColor(),
startMargin: 0,
endMargin: 0
})
.scrollBar(BarState.Off)
}
// 构建网格菜单
@Builder
private buildGridMenu() {
const columns = this.config.columns || 3;
GridRow({ columns: { sm: columns, md: columns, lg: columns } }) {
ForEach(this.filteredItems, (item: MenuItem, index: number) => {
GridCol({ span: { sm: 1, md: 1, lg: 1 } }) {
Column({ space: 4 }) {
if (item.icon) {
Image(item.icon)
.width(24)
.height(24)
.objectFit(ImageFit.Contain)
.margin({ bottom: 8 })
}
Text(item.title)
.fontSize(14)
.fontColor(this.getItemTextColor(item, index))
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.width('100%')
.padding(12)
.backgroundColor(this.getItemBackgroundColor(item, index))
.borderRadius(8)
.onClick(() => {
if (!item.disabled) {
this.handleItemSelect(item, index)
}
})
}
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
// 构建单个菜单项
@Builder
private buildMenuItem(item: MenuItem, index: number) {
Row({ space: 12 }) {
// 图标区域
if (item.icon) {
Image(item.icon)
.width(24)
.height(24)
.objectFit(ImageFit.Contain)
}
// 文本区域
Column({ space: 2 }) {
Text(item.title)
.fontSize(16)
.fontColor(this.getItemTextColor(item, index))
.fontWeight(this.selectedIndex === index ? FontWeight.Medium : FontWeight.Normal)
if (item.badge) {
Text(item.badge.toString())
.fontSize(12)
.fontColor('#FFFFFF')
.backgroundColor('#FF3B30')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(10)
.margin({ top: 2 })
}
}
.layoutWeight(1)
// 选中状态指示器
if (this.selectedIndex === index) {
Image($r('app.media.ic_check'))
.width(20)
.height(20)
.tintColor('#007DFF')
}
}
.width('100%')
.padding({ top: 12, bottom: 12, left: 16, right: 16 })
.backgroundColor(this.getItemBackgroundColor(item, index))
.borderRadius(8)
}
// 构建取消按钮
@Builder
private buildCancelButton() {
Column() {
Divider()
.strokeWidth(1)
.color(this.getDividerColor())
.margin({ top: 16, bottom: 12 })
Text(this.config.cancelText || '取消')
.fontSize(16)
.fontColor('#007DFF')
.fontWeight(FontWeight.Medium)
.width('100%')
.textAlign(TextAlign.Center)
.padding(12)
.backgroundColor(Color.Transparent)
.onClick(() => {
this.dialogController.close()
this.onDialogDismiss?.()
})
}
}
// 处理菜单项选中
private handleItemSelect(item: MenuItem, index: number): void {
// 更新选中状态
this.selectedIndex = index;
// 触发外部回调
if (this.onItemSelect) {
this.onItemSelect({
item: { ...item, selected: true },
index,
dialogController: this.dialogController
});
}
// 延迟关闭弹窗(可选)
setTimeout(() => {
this.dialogController.close();
this.onDialogDismiss?.();
}, 150);
}
// 样式辅助方法
private getBackgroundColor(): ResourceColor {
return this.config.theme === 'dark' ? '#1C1C1E' : '#FFFFFF';
}
private getTextColor(): ResourceColor {
return this.config.theme === 'dark' ? '#FFFFFF' : '#000000';
}
private getDividerColor(): ResourceColor {
return this.config.theme === 'dark' ? '#38383A' : '#E5E5EA';
}
private getItemTextColor(item: MenuItem, index: number): ResourceColor {
if (item.disabled) {
return this.config.theme === 'dark' ? '#8E8E93' : '#C7C7CC';
}
return this.selectedIndex === index ? '#007DFF' : this.getTextColor();
}
private getItemBackgroundColor(item: MenuItem, index: number): ResourceColor {
if (item.disabled) {
return Color.Transparent;
}
return this.selectedIndex === index ?
(this.config.theme === 'dark' ? '#2C2C2E' : '#F2F2F7') :
Color.Transparent;
}
}
2.3.2 弹窗控制器封装
export class MenuDialogController {
private controller: CustomDialogController | null = null;
private config: MenuDialogConfig;
private onItemSelect?: (event: MenuSelectEvent) => void;
private onDialogDismiss?: () => void;
constructor(config: MenuDialogConfig) {
this.config = config;
}
// 设置选中回调
setOnItemSelect(callback: (event: MenuSelectEvent) => void): this {
this.onItemSelect = callback;
return this;
}
// 设置关闭回调
setOnDismiss(callback: () => void): this {
this.onDialogDismiss = callback;
return this;
}
// 打开弹窗
open(): void {
if (this.controller) {
this.controller.close();
}
this.controller = new CustomDialogController({
builder: () => {
return MenuDialog({
config: this.config,
onItemSelect: this.onItemSelect,
onDialogDismiss: this.onDialogDismiss,
dialogController: this.controller!
});
},
alignment: DialogAlignment.Bottom,
offset: { dx: 0, dy: 0 },
customStyle: true,
autoCancel: true,
maskColor: '#00000040',
maskOpacity: 0.6
});
this.controller.open();
}
// 关闭弹窗
close(): void {
if (this.controller) {
this.controller.close();
}
}
// 更新配置
updateConfig(newConfig: Partial<MenuDialogConfig>): void {
this.config = { ...this.config, ...newConfig };
}
// 更新菜单项
updateItems(items: MenuItem[]): void {
this.config.items = items;
}
// 获取当前配置
getConfig(): MenuDialogConfig {
return { ...this.config };
}
}
2.4 使用示例
2.4.1 基本使用
@Entry
@Component
struct MenuDialogExample {
@State menuItems: MenuItem[] = [
{ id: '1', title: '选项一', icon: $r('app.media.ic_option1') },
{ id: '2', title: '选项二', icon: $r('app.media.ic_option2'), badge: 'New' },
{ id: '3', title: '选项三', icon: $r('app.media.ic_option3'), disabled: true },
{ id: '4', title: '选项四', icon: $r('app.media.ic_option4') },
{ id: '5', title: '选项五', icon: $r('app.media.ic_option5') }
];
@State selectedItem: MenuItem | null = null;
private dialogController: MenuDialogController;
aboutToAppear() {
// 初始化弹窗控制器
this.dialogController = new MenuDialogController({
title: '请选择操作',
items: this.menuItems,
layoutType: 'list',
showCancel: true,
cancelText: '取消',
theme: 'light',
animationType: 'slide'
});
// 设置选中回调
this.dialogController.setOnItemSelect((event) => {
this.selectedItem = event.item;
console.log(`选中了: ${event.item.title}, 索引: ${event.index}`);
// 可以在这里处理业务逻辑
this.handleMenuSelect(event);
});
// 设置关闭回调
this.dialogController.setOnDismiss(() => {
console.log('弹窗已关闭');
});
}
build() {
Column({ space: 20 }) {
// 显示当前选中项
if (this.selectedItem) {
Text(`当前选中: ${this.selectedItem.title}`)
.fontSize(18)
.fontColor('#007DFF')
.margin({ top: 30 });
}
// 打开弹窗按钮
Button('打开菜单弹窗')
.width('80%')
.height(50)
.backgroundColor('#007DFF')
.fontColor(Color.White)
.onClick(() => {
this.dialogController.open();
})
.margin({ top: 50 });
// 动态更新菜单项按钮
Button('更新菜单项')
.width('80%')
.height(50)
.backgroundColor('#34C759')
.fontColor(Color.White)
.onClick(() => {
this.updateMenuItems();
});
// 切换主题按钮
Button('切换暗色主题')
.width('80%')
.height(50)
.backgroundColor('#5856D6')
.fontColor(Color.White)
.onClick(() => {
this.toggleTheme();
});
}
.width('100%')
.height('100%')
.padding(20)
.backgroundColor('#F2F2F7')
.justifyContent(FlexAlign.Start);
}
// 处理菜单选中
private handleMenuSelect(event: MenuSelectEvent): void {
// 根据不同的菜单项执行不同的操作
switch (event.item.id) {
case '1':
// 执行选项一的操作
this.showToast('执行了选项一的操作');
break;
case '2':
// 执行选项二的操作
this.showToast('执行了选项二的操作');
break;
// ... 其他选项的处理
}
}
// 更新菜单项
private updateMenuItems(): void {
const newItems: MenuItem[] = [
{ id: 'a', title: '新选项A', icon: $r('app.media.ic_new_a') },
{ id: 'b', title: '新选项B', icon: $r('app.media.ic_new_b'), selected: true },
{ id: 'c', title: '新选项C', icon: $r('app.media.ic_new_c') },
{ id: 'd', title: '新选项D', icon: $r('app.media.ic_new_d'), badge: 3 }
];
this.menuItems = newItems;
this.dialogController.updateItems(newItems);
this.showToast('菜单项已更新');
}
// 切换主题
private toggleTheme(): void {
const currentTheme = this.dialogController.getConfig().theme;
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
this.dialogController.updateConfig({ theme: newTheme });
this.showToast(`已切换到${newTheme === 'dark' ? '暗色' : '亮色'}主题`);
}
// 显示提示
private showToast(message: string): void {
// 这里可以使用Toast组件显示提示
prompt.showToast({ message, duration: 2000 });
}
}
2.4.2 网格布局示例
@Component
struct GridMenuExample {
@State menuItems: MenuItem[] = [
{ id: 'photo', title: '照片', icon: $r('app.media.ic_photo') },
{ id: 'camera', title: '相机', icon: $r('app.media.ic_camera') },
{ id: 'file', title: '文件', icon: $r('app.media.ic_file') },
{ id: 'location', title: '位置', icon: $r('app.media.ic_location') },
{ id: 'contact', title: '联系人', icon: $r('app.media.ic_contact') },
{ id: 'audio', title: '音频', icon: $r('app.media.ic_audio') }
];
private dialogController: MenuDialogController;
aboutToAppear() {
this.dialogController = new MenuDialogController({
title: '选择分享方式',
items: this.menuItems,
layoutType: 'grid',
columns: 3,
maxHeight: '50%',
showCancel: true,
theme: 'light'
});
this.dialogController.setOnItemSelect((event) => {
console.log(`选择了: ${event.item.title}`);
// 处理分享逻辑
});
}
build() {
Column() {
Button('打开网格菜单')
.onClick(() => {
this.dialogController.open();
})
}
}
}
三、高级功能扩展
3.1 搜索过滤功能
@Component
export struct SearchableMenuDialog extends MenuDialog {
@State private searchText: string = '';
@Builder
private buildSearchBar() {
Row({ space: 8 }) {
TextInput({ placeholder: '搜索菜单项' })
.placeholderFont({ size: 14 })
.fontSize(14)
.width('100%')
.height(40)
.backgroundColor(this.config.theme === 'dark' ? '#2C2C2E' : '#F2F2F7')
.borderRadius(8)
.padding({ left: 12, right: 12 })
.onChange((value: string) => {
this.searchText = value;
this.filterItems();
})
if (this.searchText) {
Image($r('app.media.ic_clear'))
.width(20)
.height(20)
.onClick(() => {
this.searchText = '';
this.filterItems();
})
}
}
.width('100%')
.margin({ bottom: 16 })
}
private filterItems(): void {
if (!this.searchText.trim()) {
this.filteredItems = [...this.config.items];
return;
}
const keyword = this.searchText.toLowerCase();
this.filteredItems = this.config.items.filter(item =>
item.title.toLowerCase().includes(keyword)
);
}
build() {
Column({ space: 0 }) {
// 搜索栏
this.buildSearchBar()
// 原有内容
super.build()
}
}
}
3.2 多选模式支持
export interface MultiSelectMenuDialogConfig extends MenuDialogConfig {
maxSelection?: number; // 最大选择数量
minSelection?: number; // 最小选择数量
confirmText?: string; // 确认按钮文本
}
@Component
export struct MultiSelectMenuDialog {
@Prop config: MultiSelectMenuDialogConfig;
@Prop onItemsSelect?: (selectedItems: MenuItem[]) => void;
@Link dialogController: CustomDialogController;
@State private selectedIndices: number[] = [];
private handleItemSelect(item: MenuItem, index: number): void {
const currentIndex = this.selectedIndices.indexOf(index);
if (currentIndex >= 0) {
// 取消选中
this.selectedIndices.splice(currentIndex, 1);
} else {
// 检查最大选择限制
if (this.config.maxSelection && this.selectedIndices.length >= this.config.maxSelection) {
// 达到最大选择数量,提示用户
prompt.showToast({
message: `最多只能选择${this.config.maxSelection}项`,
duration: 2000
});
return;
}
this.selectedIndices.push(index);
}
// 更新选中状态
this.config.items.forEach((item, i) => {
item.selected = this.selectedIndices.includes(i);
});
}
@Builder
private buildConfirmButton() {
const isEnabled = !this.config.minSelection ||
this.selectedIndices.length >= this.config.minSelection;
Text(this.config.confirmText || '确定')
.fontSize(16)
.fontColor(isEnabled ? '#007DFF' : '#C7C7CC')
.fontWeight(FontWeight.Medium)
.width('100%')
.textAlign(TextAlign.Center)
.padding(12)
.backgroundColor(Color.Transparent)
.onClick(() => {
if (isEnabled && this.onItemsSelect) {
const selectedItems = this.selectedIndices.map(i => this.config.items[i]);
this.onItemsSelect(selectedItems);
this.dialogController.close();
}
})
}
}
3.3 动画效果增强
export class AnimatedMenuDialogController extends MenuDialogController {
private animationType: 'fade' | 'slide' | 'scale' | 'bounce' = 'slide';
setAnimationType(type: 'fade' | 'slide' | 'scale' | 'bounce'): this {
this.animationType = type;
return this;
}
open(): void {
this.controller = new CustomDialogController({
builder: () => {
return Column() {
// 使用动画包装器
this.buildAnimatedDialog()
}
},
// 根据动画类型设置不同的参数
...this.getAnimationOptions()
});
this.controller.open();
}
private getAnimationOptions(): Partial<CustomDialogOptions> {
switch (this.animationType) {
case 'fade':
return {
alignment: DialogAlignment.Center,
transition: {
type: TransitionType.Fade,
opacity: 0
}
};
case 'slide':
return {
alignment: DialogAlignment.Bottom,
offset: { dx: 0, dy: 100 },
transition: {
type: TransitionType.Slide,
slideEffect: SlideEffect.Bottom
}
};
case 'scale':
return {
alignment: DialogAlignment.Center,
transition: {
type: TransitionType.Scale,
scale: { x: 0.5, y: 0.5 }
}
};
case 'bounce':
return {
alignment: DialogAlignment.Center,
transition: {
type: TransitionType.Bounce,
bounce: { x: 0, y: -50 }
}
};
default:
return {};
}
}
@Builder
private buildAnimatedDialog() {
// 使用动画组件包装菜单内容
Transition({
type: TransitionType.Fade,
opacity: 0,
duration: 300
}) {
MenuDialog({
config: this.config,
onItemSelect: this.onItemSelect,
onDialogDismiss: this.onDialogDismiss,
dialogController: this.controller!
})
}
}
}
四、最佳实践与性能优化
4.1 性能优化建议
-
数据懒加载:对于大量菜单项,使用LazyForEach进行懒加载
-
图片优化:使用合适的图片尺寸和格式,避免内存占用过大
-
事件防抖:搜索功能添加防抖处理,避免频繁重渲染
-
组件复用:提取可复用的子组件,减少重复渲染
4.2 内存管理
@Component
struct OptimizedMenuDialog {
@Prop config: MenuDialogConfig;
@Link dialogController: CustomDialogController;
// 使用@ObjectLink优化对象监听
@ObjectLink items: MenuItem[];
// 使用@Watch监听配置变化
@Watch('onConfigChange')
@State configVersion: number = 0;
onConfigChange() {
// 配置变化时更新版本号,触发重新渲染
this.configVersion++;
}
aboutToDisappear() {
// 清理资源
this.cleanupResources();
}
private cleanupResources(): void {
// 清理事件监听器、定时器等资源
}
build() {
// 使用条件渲染避免不必要的计算
if (this.config.items.length === 0) {
return this.buildEmptyView();
}
return this.buildMenuContent();
}
}
4.3 错误处理与边界情况
-
空数据状态:处理菜单项为空的情况
-
网络异常:异步加载数据时的错误处理
-
内存警告:监听系统内存警告,及时释放资源
-
横竖屏适配:处理屏幕方向变化
private handleErrors(): void {
try {
// 业务逻辑
} catch (error) {
console.error('菜单弹窗错误:', error);
// 显示错误提示
prompt.showToast({
message: '加载菜单失败,请重试',
duration: 3000
});
// 关闭弹窗
this.dialogController?.close();
}
}
五、常见问题与解决方案
Q1:弹窗打开时页面卡顿怎么办?
解决方案:
-
检查菜单项数据量,过多数据考虑分页加载
-
使用
LazyForEach替代ForEach进行列表渲染 -
优化图片资源,使用合适的尺寸和格式
-
将复杂计算移到异步任务中
Q2:如何实现弹窗的拖拽关闭?
实现方案:
@Builder
private buildDraggableDialog() {
PanGesture({ distance: 5 })
.onActionStart(() => {
// 记录开始位置
})
.onActionUpdate((event: GestureEvent) => {
// 更新位置
})
.onActionEnd(() => {
// 判断是否达到关闭阈值
if (/* 达到关闭条件 */) {
this.dialogController.close();
}
}) {
// 弹窗内容
}
}
Q3:如何适配不同屏幕尺寸?
适配策略:
-
使用相对单位(vp、fp)而非绝对像素
-
根据屏幕宽度动态计算列数
-
使用媒体查询响应式布局
-
提供横竖屏不同的布局方案
Q4:如何实现无障碍访问?
无障碍支持:
-
为每个菜单项添加accessibilityLabel
-
支持键盘导航和屏幕阅读器
-
提供足够大的点击区域
-
确保颜色对比度符合无障碍标准
六、总结
通过本文的详细讲解,我们全面掌握了使用openCustomDialog实现参数化菜单弹窗的核心技术。关键要点总结如下:
-
参数传递机制:通过@Prop装饰器接收外部参数,支持动态更新
-
状态监听实现:使用回调函数和事件对象实现外部状态监听
-
组件化设计:将弹窗封装为可复用的独立组件
-
扩展性考虑:支持搜索、多选、动画等高级功能
-
性能优化:采用懒加载、内存管理等优化策略
在实际开发中,建议根据具体业务需求选择合适的实现方案。对于简单的菜单场景,可以使用基础版本;对于复杂交互,可以基于本文提供的扩展方案进行定制开发。
openCustomDialog的强大之处在于其灵活性和可定制性,通过合理的架构设计,可以创建出既美观又实用的菜单弹窗组件,显著提升应用的用户体验。随着HarmonyOS生态的不断发展,掌握这些高级UI组件的实现技巧,将帮助开发者在应用开发中游刃有余。
更多推荐



所有评论(0)