基于HarmonyOS的折叠式动画菜单开发与跨设备同步实现 原创

进修的泡芙
发布于 2025-6-18 21:11
浏览
0收藏

基于HarmonyOS的折叠式动画菜单开发与跨设备同步实现

一、项目概述

本项目基于HarmonyOS的ArkUI框架和动画能力,开发一个支持跨设备同步的折叠式动画菜单组件。参考《鸿蒙跨端U同步:同一局游戏中多设备玩家昵称/头像显示》中的分布式数据同步技术,实现菜单状态在多设备间的实时同步。

!https://example.com/foldable-menu-arch.png
图1:折叠菜单架构(包含UI层、动画逻辑层和分布式数据同步层)

二、核心功能实现
菜单数据模型与状态管理(ArkTS)

// 菜单项模型
class MenuItem {
id: string;
title: string;
icon: Resource;
subItems: MenuSubItem[];
expanded: boolean = false;
deviceId: string = ‘’;

constructor(title: string, icon: Resource, subItems: MenuSubItem[] = []) {
this.id = generateUUID();
this.title = title;
this.icon = icon;
this.subItems = subItems;
this.deviceId = deviceInfo.deviceId;
}

// 子菜单项模型
class MenuSubItem {
id: string;
title: string;
action: () => void;

constructor(title: string, action: () => void) {
this.id = generateUUID();
this.title = title;
this.action = action;
}

// 菜单状态管理器
class MenuStateManager {
private static instance: MenuStateManager;
private menuItems: MenuItem[] = [];
private distObject: distributedDataObject.DataObject;

static getInstance(): MenuStateManager {
if (!MenuStateManager.instance) {
MenuStateManager.instance = new MenuStateManager();
return MenuStateManager.instance;

constructor() {

// 初始化菜单数据
this.initializeMenu();

// 初始化分布式数据对象
this.distObject = distributedDataObject.create({
  menuState: []
});

// 监听数据变化
this.distObject.on('change', (fields: string[]) => {
  if (fields.includes('menuState')) {
    this.handleMenuStateUpdate();

});

// 初始化菜单数据

private initializeMenu() {
this.menuItems = [
new MenuItem(‘首页’, $r(‘app.media.ic_home’), [
new MenuSubItem(‘仪表盘’, () => navigateTo(‘dashboard’)),
new MenuSubItem(‘通知’, () => navigateTo(‘notifications’))
]),
new MenuItem(‘通讯录’, $r(‘app.media.ic_contacts’), [
new MenuSubItem(‘个人’, () => navigateTo(‘personal’)),
new MenuSubItem(‘群组’, () => navigateTo(‘groups’))
]),
new MenuItem(‘设置’, $r(‘app.media.ic_settings’), [
new MenuSubItem(‘账户’, () => navigateTo(‘account’)),
new MenuSubItem(‘隐私’, () => navigateTo(‘privacy’)),
new MenuSubItem(‘关于’, () => navigateTo(‘about’))
])
];
// 获取菜单项

getMenuItems(): MenuItem[] {
return […this.menuItems];
// 切换菜单展开状态

toggleMenuItem(id: string) {
const item = this.menuItems.find(item => item.id === id);
if (item) {
item.expanded = !item.expanded;
item.deviceId = deviceInfo.deviceId;
this.syncMenuState();
}

// 同步菜单状态
private syncMenuState() {
this.distObject.menuState = this.menuItems.map(item => ({
id: item.id,
expanded: item.expanded,
deviceId: item.deviceId
}));

// 同步到已连接设备
const targetDevices = deviceManager.getConnectedDevices()
  .map(d => d.deviceId)
  .filter(id => id !== deviceInfo.deviceId);

if (targetDevices.length > 0) {
  this.distObject.setDistributed(targetDevices);

}

// 处理菜单状态更新
private handleMenuStateUpdate() {
const remoteState = this.distObject.menuState as Array<{
id: string;
expanded: boolean;
deviceId: string;
}>;

remoteState.forEach(remoteItem => {
  const localItem = this.menuItems.find(item => item.id === remoteItem.id);
  if (localItem && remoteItem.deviceId !== deviceInfo.deviceId) {
    localItem.expanded = remoteItem.expanded;
    localItem.deviceId = remoteItem.deviceId;

});

this.notifyListeners();

// 监听器相关代码省略…

折叠菜单组件实现(ArkTS)

// 折叠菜单组件
@Component
export struct FoldableMenu {
@State menuItems: MenuItem[] = [];
private menuManager = MenuStateManager.getInstance();

aboutToAppear() {
this.menuItems = this.menuManager.getMenuItems();
this.menuManager.addListener(() => {
this.menuItems = this.menuManager.getMenuItems();
});
build() {

Column() {
  ForEach(this.menuItems, (item: MenuItem) => {
    MenuItemView({ item: item })
  })

.width(‘100%’)

.backgroundColor('#FFFFFF')
.padding(12)

}

// 菜单项组件
@Component
struct MenuItemView {
@Prop item: MenuItem;
@State private animator: Animator = new Animator();
private menuManager = MenuStateManager.getInstance();

build() {
Column() {
// 主菜单项
Row() {
Image(this.item.icon)
.width(24)
.height(24)
.margin({ right: 12 })

    Text(this.item.title)
      .fontSize(16)
      .layoutWeight(1)
    
    Image($r('app.media.ic_arrow'))
      .width(16)
      .height(16)
      .rotate({ angle: this.item.expanded ? 180 : 0 })
      .animation({ duration: 300, curve: Curve.EaseInOut })

.width(‘100%’)

  .height(48)
  .onClick(() => {
    this.menuManager.toggleMenuItem(this.item.id);
  })
  
  // 子菜单项
  if (this.item.expanded) {
    Column() {
      ForEach(this.item.subItems, (subItem: MenuSubItem) => {
        SubMenuItemView({ subItem: subItem })
      })

.width(‘100%’)

    .margin({ left: 36 })
    .transition({ type: TransitionType.Insert, opacity: 0, translate: { y: -20 } })
    .transition({ type: TransitionType.Delete, opacity: 0, translate: { y: -20 } })

}

.width('100%')
.margin({ bottom: 8 })
.borderRadius(8)
.backgroundColor(this.item.expanded ? '#F5F5F5' : '#FFFFFF')
.animation({ duration: 300, curve: Curve.EaseInOut })

}

// 子菜单项组件
@Component
struct SubMenuItemView {
@Prop subItem: MenuSubItem;

build() {
Row() {
Text(this.subItem.title)
.fontSize(14)
.fontColor(‘#666666’)
.width(‘100%’)

.height(40)
.padding({ left: 12 })
.onClick(() => {
  this.subItem.action();
})

}

动画效果增强实现

// 高级动画控制器
class MenuAnimationController {
private static instance: MenuAnimationController;
private animators: Map<string, Animator> = new Map();

static getInstance(): MenuAnimationController {
if (!MenuAnimationController.instance) {
MenuAnimationController.instance = new MenuAnimationController();
return MenuAnimationController.instance;

// 注册菜单项动画

registerAnimation(menuId: string, animator: Animator) {
this.animators.set(menuId, animator);
// 执行展开动画

playExpandAnimation(menuId: string) {
const animator = this.animators.get(menuId);
if (animator) {
animator.play({
duration: 300,
curve: Curve.EaseOut,
onFinish: () => {
// 动画完成回调
});

}

// 执行折叠动画
playCollapseAnimation(menuId: string) {
const animator = this.animators.get(menuId);
if (animator) {
animator.play({
duration: 300,
curve: Curve.EaseIn,
onFinish: () => {
// 动画完成回调
});

}

分布式同步增强实现

// 增强型菜单同步服务
class EnhancedMenuSync {
private static instance: EnhancedMenuSync;
private distObject: distributedDataObject.DataObject;
private operationQueue: Array<{
type: ‘expand’ | ‘collapse’;
menuId: string;
deviceId: string;
timestamp: number;
}> = [];

static getInstance(): EnhancedMenuSync {
if (!EnhancedMenuSync.instance) {
EnhancedMenuSync.instance = new EnhancedMenuSync();
return EnhancedMenuSync.instance;

constructor() {

this.distObject = distributedDataObject.create({
  operations: []
});

// 监听设备连接变化
deviceManager.on('deviceStateChange', () => {
  this.syncPendingOperations();
});

// 监听数据变化
this.distObject.on('change', (fields: string[]) => {
  if (fields.includes('operations')) {
    this.handleOperations();

});

// 同步菜单操作

syncMenuOperation(type: ‘expand’ | ‘collapse’, menuId: string) {
const operation = {
type,
menuId,
deviceId: deviceInfo.deviceId,
timestamp: Date.now()
};

this.operationQueue.push(operation);
this.syncPendingOperations();

// 同步待处理操作

private syncPendingOperations() {
const connectedDevices = deviceManager.getConnectedDevices();
if (connectedDevices.length === 0) return;

this.distObject.operations = [...this.operationQueue];
this.distObject.setDistributed(
  connectedDevices.map(d => d.deviceId)
    .filter(id => id !== deviceInfo.deviceId)
);

this.operationQueue = [];

// 处理远程操作

private handleOperations() {
const operations = this.distObject.operations as Array<{
type: ‘expand’ | ‘collapse’;
menuId: string;
deviceId: string;
timestamp: number;
}>;

operations.forEach(op => {
  if (op.deviceId !== deviceInfo.deviceId) {
    const menuManager = MenuStateManager.getInstance();
    const menuItem = menuManager.getMenuItems().find(item => item.id === op.menuId);
    
    if (menuItem) {
      // 只处理最新的操作
      if (menuItem.deviceId !== op.deviceId || menuItem.timestamp < op.timestamp) {
        menuItem.expanded = op.type === 'expand';
        menuItem.deviceId = op.deviceId;
        
        // 执行动画
        const animController = MenuAnimationController.getInstance();
        if (op.type === 'expand') {
          animController.playExpandAnimation(op.menuId);

else {

          animController.playCollapseAnimation(op.menuId);

}

}

});

}

三、关键功能说明
动画实现原理

// 动画配置示例
.animation({
duration: 300, // 动画持续时间(ms)
curve: Curve.EaseInOut, // 动画曲线
delay: 0, // 延迟时间
iterations: 1, // 重复次数
playMode: PlayMode.Normal // 播放模式
})

// 转场动画示例
.transition({
type: TransitionType.Insert, // 插入动画
opacity: 0, // 透明度变化
translate: { y: -20 } // Y轴位移
})

分布式同步策略

同步方式 实现机制 优点 适用场景

状态同步 同步整个菜单状态 数据一致性强 菜单项较少时
操作同步 同步展开/折叠操作 传输数据量小 菜单项较多时
混合模式 状态+操作结合 平衡性能与一致性 通用场景

组件通信流程

sequenceDiagram
participant UI
participant MenuManager
participant SyncService

UI->>MenuManager: 用户点击菜单项
MenuManager->>SyncService: 同步菜单状态
SyncService->>其他设备: 分发状态更新
其他设备->>其他设备UI: 更新菜单显示

四、项目扩展与优化
性能优化建议

动画性能优化:

  .animation({ duration: 300, curve: Curve.Linear })

数据同步优化:

添加操作节流

实现差异比对

使用二进制协议
功能扩展建议

多级嵌套菜单:

  interface MenuItem {
 subItems: MenuItem[]; // 支持嵌套

菜单主题定制:

  enum MenuTheme {
 LIGHT = 'light',
 DARK = 'dark'

手势操作支持:

  .gesture(
 PanGesture({ direction: PanDirection.Vertical })
   .onActionStart(() => {})
   .onActionUpdate(() => {})
   .onActionEnd(() => {})

)

五、测试方案
测试用例设计

测试类型 测试场景 验证点

功能测试 点击菜单项 正确展开/折叠
动画测试 切换菜单状态 动画流畅无卡顿
同步测试 多设备操作 状态同步一致
性能测试 快速操作 无动画丢帧
兼容测试 不同设备 显示效果一致

自动化测试示例

// 折叠菜单测试
describe(‘FoldableMenu Tests’, () => {
let menuManager: MenuStateManager;

before(() => {
menuManager = MenuStateManager.getInstance();
});

it(‘should toggle menu item state’, () => {
const menuId = menuManager.getMenuItems()[0].id;
const initialState = menuManager.getMenuItems()[0].expanded;

menuManager.toggleMenuItem(menuId);
expect(menuManager.getMenuItems()[0].expanded).toBe(!initialState);

});

it(‘should sync menu state across devices’, () => {
const syncService = EnhancedMenuSync.getInstance();
const menuId = menuManager.getMenuItems()[0].id;

syncService.syncMenuOperation('expand', menuId);
expect(syncService['distObject'].operations.length).toBe(1);

});
});

六、总结

本项目基于HarmonyOS实现了具有以下特点的折叠式菜单:
流畅的动画效果:支持展开/折叠过渡动画

灵活的数据结构:支持多级菜单嵌套

实时的状态同步:基于分布式能力实现多设备协同

良好的可扩展性:易于添加新功能和定制样式

通过参考《鸿蒙跨端U同步:同一局游戏中多设备玩家昵称/头像显示》的技术方案,我们验证了HarmonyOS在UI动画和分布式协同方面的强大能力,为开发者提供了构建现代化交互组件的实践参考。

注意事项:
实际开发中需要处理菜单项动态加载

考虑添加无障碍访问支持

生产环境需要更完善的错误处理

可根据具体需求调整动画参数和同步策略

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