OpenHarmony Launcher 桌面动画体系详解

桌面动画是 Launcher 用户体验的灵魂。本文详解从动画框架选型、页面切换动画、图标交互动画到性能优化的完整链路,并梳理核心数据结构与源码模块位置。

flowchart LR
 A[动画框架选型
ArkUI动画引擎] --> B[页面切换动画
PageView滑动]
    B --> C[图标交互动画
点击/长按/拖拽]
 C --> D[文件夹动画
展开/收起]
    B -.动画参数.-> E[(动画配置表)]
 C -.物理引擎.-> F[弹簧/阻尼曲线] 

一、概述

桌面动画决定了 Launcher ”手感”。流畅的动画让用户感觉操作跟手、过渡自然;卡顿的动画则直接影响体验。OpenHarmony Launcher 基于 ArkUI 动画引擎构建了一套完整的桌面动画体系。你可以把动画体系看成桌面的”运动神经系统”。

整个流程围绕一个核心组件展开——动画管理器(AnimationManager。它统一管理页面切换、图标交互、文件夹展开、拖拽跟随等各类动画,确保动画之间的协调与不冲突。

二、整体阶段划分

桌面动画体系分为 5 个阶段:

阶段

名称

作用

1

动画框架选型(Framework

选择 ArkUI 提供的动画能力

2

页面切换动画(PageTransition

桌面分页滑动切换动画

3

图标交互动画(IconInteraction

点击、长按、拖拽等图标动画

4

文件夹动画(FolderAnimation

文件夹展开与收起动画

5

性能优化(Optimization

帧率保障与内存优化

下面逐个展开。

三、动画框架选型阶段(Framework

ArkUI 提供了多种动画能力,Launcher 根据场景选择不同的动画方案:

3.1 ArkUI 动画能力一览

动画类型

API

适用场景

特点

属性动画

animateTo

位置、大小、透明度变化

声明式,自动插值

显式动画

animation

组件属性绑定动画

链式调用,细粒度控制

转场动画

Transition

组件出现/消失

入场+出场动画

共享元素动画

sharedTransition

图标展开到大图

跨页面元素过渡

粒子动画

particle

特效场景

高级视觉效果

3.2 Launcher 动画方案选型

flowchart TB
 A[Launcher 动画需求] --> B{动画类型}
    B -->|页面切换| C[animateTo + 手势驱动]
 B -->|图标点击| D[animation 属性动画]
    B -->|长按抖动| E[animateTo 循环动画]
 B -->|拖拽跟随| F[实时位置更新 无插值]
    B -->|文件夹展开| G[sharedTransition 共享元素]
 B -->|应用启动| H[缩放+淡出 转场动画]

3.3 动画配置数据结构

interface AnimationConfig {
  duration: number;          // 动画时长(ms)
  delay: number;             // 延迟时间(ms)
  curve: CurveType;          // 缓动曲线
  iterations: number;        // 重复次数(-1为无限)
  playMode: PlayMode;        // 播放模式(正向/逆向/交替)
  onFinish: () => void;      // 完成回调
}

enum CurveType {
  LINEAR = 0,         // 线性
  EASE_IN = 1,        // 先慢后快
  EASE_OUT = 2,       // 先快后慢
  EASE_IN_OUT = 3,    // 两端慢中间快
  SPRING = 4,         // 弹簧物理曲线
  CUSTOM = 5          // 自定义贝塞尔曲线
}

四、页面切换动画阶段(PageTransition

桌面分页滑动是最核心的动画体验:

4.1 手势驱动滑动

用户手指滑动时,页面实时跟随手指移动:

@Component
struct LauncherPageView {
  @State offsetX: number = 0;
  @State currentPage: number = 0;
  private startX: number = 0;

  build() {
    Stack() {
      // 页面内容
      Row() {
        ForEach(this.pages, (page, index) => {
          PageItem({ page: page })
        })
      }
      .offset({ x: this.offsetX })
      .gesture(
        PanGesture()
          .onActionStart((event) => {
            this.startX = event.offsetX;
          })
          .onActionUpdate((event) => {
            // 实时跟随手指
            this.offsetX = -this.currentPage * this.screenWidth 
                          + (event.offsetX - this.startX);
          })
          .onActionEnd((event) => {
            this.onSwipeEnd(event);
          })
      )
    }
  }
}

4.2 松手后的自动滚动

松手后根据滑动距离和速度决定翻页还是回弹:

onSwipeEnd(event: GestureEvent) {
  let deltaX = event.offsetX - this.startX;
  let velocity = event.velocity;
  let threshold = this.screenWidth * 0.3;  // 滑动超过30%翻页

  let shouldChangePage = false;
  let targetPage = this.currentPage;

  if (Math.abs(deltaX) > threshold || Math.abs(velocity) > 500) {
    shouldChangePage = true;
    targetPage = deltaX < 0 
      ? Math.min(this.currentPage + 1, this.pageCount - 1)
      : Math.max(this.currentPage - 1, 0);
  }

  // 使用弹簧曲线滚动到目标位置
  animateTo({
    duration: 350,
    curve: curves.springMotion(0.34, 0.84),  // 弹簧曲线
    onFinish: () => {
      this.currentPage = targetPage;
    }
  }, () => {
    this.offsetX = -targetPage * this.screenWidth;
  });
}

参数

默认值

说明

翻页阈值

30% 屏幕宽度

滑动超过此距离则翻页

速度阈值

500 px/s

快速滑动即使距离不够也翻页

动画时长

350ms

松手后滚动动画时长

缓动曲线

spring(0.34, 0.84)

弹簧物理曲线,自然减速

过冲

5%

弹簧轻微过冲后回弹

4.3 页面指示器联动

底部页面指示器(小圆点)随页面切换同步动画:

当前页:  ● ○ ○ ○ ○
滑动中:    ● → ○ ○ ○ ○  (圆点平滑移动)
完成:    ○ ● ○ ○ ○

// 指示器圆点位置随滑动比例插值
let indicatorOffset = this.currentPage * indicatorSpacing
                     + (deltaX / this.screenWidth) * indicatorSpacing;

五、图标交互动画阶段(IconInteraction

5.1 点击反馈动画

用户点击图标时的按压反馈:

@Component
struct AppIcon {
  @State scale: number = 1.0;
  @State opacity: number = 1.0;

  build() {
    Image(this.icon)
      .scale({ x: this.scale, y: this.scale })
      .opacity(this.opacity)
      .onClick(() => {
        // 点击缩小
        animateTo({ duration: 100, curve: Curve.EaseIn }, () => {
          this.scale = 0.85;
          this.opacity = 0.7;
        });
        // 启动应用
        this.launchApp();
      })
      .onTouch((event) => {
        if (event.type === TouchType.Up) {
          // 松手回弹
          animateTo({ duration: 200, curve: curves.springMotion() }, () => {
            this.scale = 1.0;
            this.opacity = 1.0;
          });
        }
      })
  }
}

动画状态

scale

opacity

时长

曲线

默认

1.0

1.0

-

-

按下

0.85

0.7

100ms

EaseIn

松手

1.0

1.0

200ms

Spring

5.2 长按抖动动画

长按图标进入编辑模式,所有图标开始轻微抖动:

@Component
struct AppIcon {
  @State isShaking: boolean = false;
  @State shakeAngle: number = 0;

  startShake() {
    this.isShaking = true;
    this.shakeLoop();
  }

  shakeLoop() {
    if (!this.isShaking) return;
    animateTo({
      duration: 120,
      curve: Curve.Linear,
      onFinish: () => {
        animateTo({
          duration: 120,
          curve: Curve.Linear,
          onFinish: () => {
            this.shakeLoop();  // 循环抖动
          }
        }, () => {
          this.shakeAngle = 2;  // 向右倾斜2
        });
      }
    }, () => {
      this.shakeAngle = -2;  // 向左倾斜2
    });
  }

  build() {
    Image(this.icon)
      .rotate({ angle: this.shakeAngle })
  }
}

抖动参数

说明

倾斜角度

±2°

左右各倾斜2

单次时长

120ms

单方向倾斜时长

总周期

240ms

一个完整左右摇摆周期

曲线

Linear

线性匀速摆动

各图标错位

随机延迟 0~120ms

避免整齐划一的机械感

5.3 拖拽跟随动画

拖拽过程中图标实时跟随手指,不做插值动画:

// 拖拽中实时更新位置 —— 无动画,追求最低延迟
onDragUpdate(event: GestureEvent) {
  this.dragIconX = event.x;
  this.dragIconY = event.y;
  // 直接赋值,不使用 animateTo
  // 因为动画插值会引入延迟,拖拽必须1:1跟手
}

交互动画类型

是否使用动画插值

原因

拖拽跟随

必须1:1跟手,任何插值都会产生延迟感

松手归位

需要平滑过渡到目标位置

拖拽时其他图标让位

被挤开的图标使用弹簧动画平滑移动

拖到边缘翻页

页面切换使用 350ms 弹簧动画

5.4 应用启动动画

点击图标后,图标放大淡出,应用从图标位置展开:

launchApp() {
  animateTo({
    duration: 250,
    curve: Curve.EaseOut,
    onFinish: () => {
      // 动画完成后真正启动应用
      this.startAbility();
    }
  }, () => {
    this.scale = 1.3;       // 放大到1.3
    this.opacity = 0;       // 淡出
  });
}

阶段

scale

opacity

时长

起点

1.0

1.0

0ms

终点

1.3

0

250ms

六、文件夹动画阶段(FolderAnimation

6.1 文件夹展开动画

拖拽一个图标到另一个图标上时,创建文件夹并展开:

sequenceDiagram
 participant U as 用户
 participant DC as DragController
 participant FM as FolderManager
 participant UI as ArkUI引擎

 U->>DC: 拖拽图标A到图标B
 DC->>FM: createFolder(A, B)
 FM->>UI: 触发共享元素动画
 Note over UI: 图标B位置 文件夹圆形背景
 UI->>UI: 背景放大 + 内容淡入
 UI->>U: 展开文件夹内容

6.2 共享元素过渡

// 文件夹图标(小)—— 共享元素起点
@Component
struct FolderIcon {
  build() {
    Stack() {
      // 文件夹预览图
      FolderPreview({ apps: this.apps })
    }
    .sharedTransition('folder_' + this.folderId, {
      duration: 300,
      curve: Curve.EaseInOut,
      zIndex: 100
    })
    .onClick(() => this.openFolder())
  }
}

// 文件夹展开页(大)—— 共享元素终点
@Component
struct FolderOpenView {
  build() {
    Stack() {
      // 文件夹内容网格
      GridLayout({ apps: this.apps })
    }
    .sharedTransition('folder_' + this.folderId, {
      duration: 300,
      curve: Curve.EaseInOut,
      zIndex: 100
    })
  }
}

动画参数

说明

过渡时长

300ms

从图标位置展开到全屏

缩放曲线

EaseInOut

开始慢-中间快-结束慢

内容淡入

150ms 延迟后开始

背景先展开,内容后淡入

背景模糊

200ms

桌面背景模糊化

关闭动画

逆向播放

展开的逆过程,收回图标位置

6.3 文件夹内容展开的动画序列

时间轴:
0ms    ─── 文件夹图标在桌面原位
       
50ms   ─── 背景圆角矩形开始放大
       
100ms  ─── 背景到达目标尺寸,开始显示内容网格
       
150ms  ─── 文件夹内应用图标逐个淡入(错位30ms
       │     图标1淡入(150ms) 图标2淡入(180ms) 图标3淡入(210ms)...
       
300ms  ─── 动画完成,文件夹完全展开

// 文件夹内图标错位淡入
ForEach(this.apps, (app, index) => {
  AppIcon({ app: app })
    .opacity(this.iconOpacity[index])
    .scale({ x: this.iconScale[index], y: this.iconScale[index] })
    .animation({
      delay: 150 + index * 30,  // 每个图标延迟30ms
      duration: 200,
      curve: Curve.EaseOut
    })
})

七、性能优化阶段(Optimization

7.1 帧率保障

优化策略

说明

UI 线程分离

动画运行在 UI 线程,数据计算在 Worker 线程

硬件加速

ArkUI 引擎使用 GPU 合成,减少 CPU 绘制负担

动画合并

多个属性变化合并到一个 animateTo 调用中

避免布局抖动

动画期间不触发 reflow/relayout

7.2 内存优化

优化策略

说明

离屏页面释放

非当前页及相邻页的图标资源释放

动画对象池

复用动画对象,避免频繁 GC

位图缓存

图标缩放结果缓存,避免重复计算

动画暂停

不可见时暂停动画,减少 CPU 消耗

7.3 动画性能监控

// 使用 PerformanceMonitor 监控帧率
performanceMonitor.startMonitor('launcher_animation_fps');
// 动画执行...
performanceMonitor.stopMonitor('launcher_animation_fps', (result) => {
  if (result.avgFps < 55) {
    hilog.warn(TAG, `动画帧率偏低: ${result.avgFps}fps`);
    // 触发降级策略
  }
});

监控指标

阈值

处理方式

平均帧率

≥ 55fps

正常

最低帧率

≥ 45fps

正常

帧率 < 45fps

卡顿

降低动画精度

帧率 < 30fps

严重卡顿

禁用部分动画

7.4 降级策略

当设备性能不足时,Launcher 自动降级动画效果:

降级等级

触发条件

效果

Level 0

正常(55fps

全部动画效果

Level 1

轻微卡顿(45~55fps

缩短动画时长、减少模糊效果

Level 2

卡顿(30~45fps

禁用弹簧曲线、改用线性动画

Level 3

严重卡顿(<30fps

仅保留基础过渡,禁用所有特效

八、动画的生命周期

用户操作触发
 
 
动画管理器分配 AnimationTask
 
 
检查是否与正在执行的动画冲突
 
 ├─ 无冲突 立即执行
 
 └─ 有冲突 取消旧动画,平滑过渡到新动画
 
 
ArkUI 引擎驱动逐帧渲染
 
 
动画完成 触发 onFinish 回调
 
 
释放动画资源

特性

说明

动画冲突处理

新动画打断旧动画时,从当前状态平滑过渡到新目标

动画队列

同一组件的多个动画按队列执行,不叠加

优先级

拖拽动画 > 点击动画 > 页面切换动画 > 装饰性动画

可中断

所有动画均可被新的用户操作打断

九、关键代码模块位置

源码层级(OpenHarmony 仓库路径):

模块

路径

说明

动画管理器

applications/standard/launcher/feature/ animation/

动画统一管理逻辑

页面切换

applications/standard/launcher/feature/ pagemanager/

PageView 切换动画

拖拽动画

applications/standard/launcher/feature/ dragcontroller/

拖拽跟随与归位动画

文件夹动画

applications/standard/launcher/feature/ folder/

文件夹展开/收起动画

ArkUI 动画引擎

foundation/arkui/ace_engine/

底层动画驱动引擎

弹簧曲线

foundation/arkui/ace_engine/curves/

物理曲线实现

十、一张图总结

flowchart TB
 A[ArkUI 动画引擎] --> B[页面切换动画]
    A --> C[图标交互动画]
 A --> D[文件夹动画]
    A --> E[应用启动动画]

 B --> B1[手势跟随
无插值]
 B --> B2[松手翻页
弹簧曲线 350ms]
    B --> B3[指示器联动
同步插值]

 C --> C1[点击按压
缩小0.85 100ms]
    C --> C2[长按抖动
±2° 循环]
 C --> C3[拖拽跟随
实时1:1]
    C --> C4[松手归位
弹簧动画]

 D --> D1[共享元素过渡
300ms]
    D --> D2[内容错位淡入
30ms间隔]
 D --> D3[背景模糊
200ms]

    E --> E1[放大1.3]
 E --> E2[淡出 250ms]

    A -.性能监控.-> F[帧率≥55fps]
 F -.降级.-> G[Level 0-3 自动降级]

十一、延伸话题

如果你感兴趣,可以进一步聊这几个方向:

  • 自定义贝塞尔曲线 —— 如何通过控制点设计独特的动画节奏感,让动画拥有品牌辨识度
  • 动画的可访问性 —— 为视障用户提供动画替代方案(触觉反馈、语音提示),以及减少动画模式的设计
  • 120Hz 高刷新率适配 —— 在高刷设备上如何利用额外帧率提升动画流畅度,同时控制功耗
Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐