img

📖 引言

在上一篇文章中,我们深入探讨了 AnimationDemo 组件的 setInterval + Math.sin 数值驱动动画机制,它实现了太阳系、海洋世界、雨天气象等六种持续运行的场景动画。然而,科普应用中的动画不仅限于"一直循环播放"的场景演示——更多时候,我们需要的是由用户操作触发的过渡动画:点击卡片后展开详情、打卡弹窗弹出时的缩放入场、答题反馈区域的淡入淡出、收藏按钮的状态切换等等。这些动画的共同特点是:由离散事件触发,有明确的起点和终点,持续一段有限的时间后结束

对于这类"事件驱动型"动画,ArkUI 提供了 animateTo 显式动画 API。它是 HarmonyOS 动画体系中最常用、最灵活的过渡动画方案。本文将以《奇妙科学乐园》项目中的实际场景为切入点,系统讲解 animateTo 的核心参数(curve 缓动曲线、duration 时长、iterations 重复次数、onFinish 回调),并结合弹窗入场、卡片展开、答题反馈、收藏切换等具体案例,演示如何用显式动画提升儿童科普应用的交互体验。

源码仓库https://atomgit.com/2301_79280419/WonderSciencePark


🎯 学习目标

完成本文后,你将能够:

  • ✅ 理解 animateTo 显式动画与 setInterval 数值动画的本质区别
  • ✅ 掌握 animateTodurationcurveiterationsonFinish 四大核心参数
  • ✅ 运用 animateTo 实现弹窗缩放入场 / 卡片展开收起 / 答题反馈过渡
  • ✅ 理解 Curve.EaseInOutCurve.Friction 等常见缓动曲线的适用场景
  • ✅ 掌握 iterations 重复动画的设计技巧(如按钮闪烁提示)
  • ✅ 学会通过 onFinish 回调实现动画串联和状态同步

💡 需求分析

animateTo vs setInterval:两种动画范式的对比

在《奇妙科学乐园》项目中,我们实际使用了两种不同的动画技术路线:

维度 setInterval + Math.sin(第47篇) animateTo 显式动画(本篇)
触发方式 自动持续运行 用户事件触发
动画时长 无限循环 有限时长(毫秒级)
状态变化 连续数值驱动(0-99循环) 离散状态切换(boolean等)
适用场景 场景演示(太阳系、鲸鱼) UI过渡(弹窗、展开、反馈)
性能特点 每帧重新计算 由渲染引擎优化插值
代码复杂度 需手动管理定时器生命周期 一行API调用即可

项目中需要 animateTo 的场景清单

奇妙科学乐园 - 需要显式动画的交互场景
├── 打卡弹窗 CheckInDialog
│   └── 弹窗弹出时的缩放入场效果
├── 答题页面 Quiz
│   ├── 选择答案后反馈区域的淡入过渡
│   ├── 答题进度条的平滑增长
│   └── 下一题时的内容切换动画
├── 文章详情页 TopicDetail
│   ├── 收藏按钮的状态切换动画
│   └── 底部操作栏的弹出/收起
├── 答题结果页 QuizResult
│   └── 星星评级的逐个点亮动画
├── 实验详情页 LabDetail
│   └── 步骤切换时的内容过渡
└── 首页 Index
    └── 骨架屏到真实内容的切换过渡

animateTo API 签名解析

// animateTo 完整签名
animateTo(
  options: AnimationOptions,   // 动画配置参数
  event: () => void            // 状态变更闭包
): void

// AnimationOptions 核心字段
interface AnimationOptions {
  duration?: number;      // 动画时长(毫秒),默认1000
  curve?: Curve | string; // 缓动曲线,默认Curve.EaseInOut
  delay?: number;         // 延迟执行(毫秒),默认0
  iterations?: number;     // 重复次数,默认1(-1为无限循环)
  playMode?: PlayMode;     // 播放模式(Normal/Reverse/Alternate)
  onFinish?: () => void;   // 动画结束回调
}

🏗️ 整体架构设计

显式动画的工作原理

animateTo 的核心原理可以用一句话概括:在闭包中修改 @State 变量时,ArkUI 渲染引擎不会立即跳到终态,而是按照指定的 durationcurve 在起始值和终止值之间进行插值,逐帧更新 UI。

用户点击  animateTo 启动  闭包中修改 @State  引擎开始插值动画

时间轴:
  0ms        150ms       300ms       450ms       600ms(duration结束)
  |-----------|-----------|-----------|-----------|
  起始值 ---- 25% ---- 50% ---- 75% ---- 终止值
                
            curve控制每帧的插值比例

缓动曲线 curve 对动画质感的影响

缓动曲线决定了动画过程中"速度"的变化方式,是动画"手感"的关键参数:

Curve 值 速度变化 适用场景 直观感受
Curve.Linear 匀速 进度条、计时器 机械、无感情
Curve.EaseIn 慢→快 元素离场 加速离开
Curve.EaseOut 快→慢 元素入场 快速到达后减速
Curve.EaseInOut 慢→快→慢 状态切换 最自然的过渡
Curve.Friction 快→缓慢停止 物理模拟 弹性惯性
Curve.Sharp 极快→缓慢 点击反馈 敏捷响应
Curve.Cubic 三次贝塞尔 通用过渡 平滑柔和
spring() 弹簧物理 按钮回弹 自然弹跳(第82篇详解)

🔧 核心实现拆解

1. 打卡弹窗的缩放入场动画

CheckInDialog 是《奇妙科学乐园》的每日签到弹窗,目前使用 CustomDialogController 的标准弹出效果。我们可以用 animateTo 为弹窗内的元素添加更丰富的入场动画,让签到体验更具仪式感。

当前实现回顾(CheckInDialog.ets):

// entry/src/main/ets/components/common/CheckInDialog.ets
@CustomDialog
export struct CheckInDialog {
  controller: CustomDialogController;
  @State streakDays: number = 0;
  @State weekDays: string[] = ['一', '二', '三', '四', '五', '六', '日'];
  @State checkedDays: boolean[] = [false, false, false, false, false, false, false];
  @State todayChecked: boolean = false;

  // ... 省略其他逻辑
}

使用 animateTo 增强打卡按钮的点击反馈:

// 打卡按钮点击时,使用 animateTo 添加缩放动画
onCheckIn(): void {
  if (this.todayChecked) return;

  // ✅ 正确:使用 animateTo 包裹状态变更,实现平滑过渡
  animateTo({
    duration: 300,
    curve: Curve.EaseInOut,
    onFinish: () => {
      // 动画完成后延迟关闭弹窗
      setTimeout(() => {
        this.controller.close();
      }, 500);
    }
  }, () => {
    // 闭包内修改 @State 变量,触发动画插值
    this.checkedDays[this.getCurrentDayIndex()] = true;
    this.todayChecked = true;
  });

  // 持久化打卡记录
  userPrefs.checkInAndUpdateDays().then((days: number) => {
    // ✅ 正确:在 animateTo 之外修改非动画状态
    animateTo({ duration: 400, curve: Curve.EaseOut }, () => {
      this.streakDays = days;
    });
  });
}

关键设计要点:

  • duration: 300:300毫秒是按钮状态切换的"黄金时长",既不过快让用户看不清,也不过慢造成等待感。
  • Curve.EaseInOut:慢→快→慢的节奏让状态切换显得自然。
  • onFinish 回调:在动画播放完毕后才关闭弹窗,确保用户看到完整的打卡确认效果。
  • 连续两天数的 animateTo 与打卡状态的 animateTo 分离,避免互相干扰。

对比:不使用 animateTo 的问题:

// ❌ 错误:直接修改状态,无过渡效果
onCheckIn(): void {
  if (this.todayChecked) return;
  this.checkedDays[this.getCurrentDayIndex()] = true;
  this.todayChecked = true;
  // UI瞬间跳变,体验生硬
  setTimeout(() => {
    this.controller.close();
  }, 800);
}

2. 答题反馈区域的淡入过渡

在 Quiz 答题页面中,用户选择答案后会出现绿色(正确)或红色(错误)的反馈区域。当前实现是直接显示,没有任何过渡效果。我们用 animateTo 为反馈区域添加淡入 + 位移的入场动画。

当前实现回顾(Quiz.ets 中 showFeedback 的条件渲染):

// 当前实现:直接显示反馈区域
if (this.showFeedback) {
  Column({ space: 8 }) {
    Row() {
      Image(this.isCorrect ? trophy icon : empty icon);
      Text(this.isCorrect ? '回答正确!' : '回答错误');
    }
    // ... 解释内容
  }
  .width('100%')
  .padding(14)
  .backgroundColor(this.isCorrect ? '#e8f5e9' : '#ffebee');
}

使用 animateTo + @State 实现反馈区域动画:

@State showFeedback: boolean = false;
@State feedbackOpacity: number = 0;     // 控制透明度
@State feedbackOffsetY: number = 20;      // 控制Y轴偏移(从下方滑入)
@State feedbackScale: number = 0.95;      // 控制缩放

selectOption(index: number) {
  if (this.showFeedback) return;
  this.selectedOption = index;

  const result = quizEngine.submitAnswer(index);
  if (result) {
    this.isCorrect = result.isCorrect;
    this.correctIdx = result.correctIndex;
    this.explanation = result.explanation;

    // ✅ 正确:用 animateTo 驱动反馈区域的入场动画
    animateTo({
      duration: 350,
      curve: Curve.EaseOut,
      delay: 200,  // 延迟200ms,让用户先看到选中状态
    }, () => {
      this.showFeedback = true;
      this.feedbackOpacity = 1;
      this.feedbackOffsetY = 0;
      this.feedbackScale = 1;
    });
  }
}

在 build 中使用动画状态:

if (this.showFeedback) {
  Column({ space: 8 }) {
    // ... 反馈内容
  }
  .width('100%')
  .padding(14)
  .backgroundColor(this.isCorrect ? '#e8f5e9' : '#ffebee')
  .borderRadius(12)
  .border({ width: 1, color: this.isCorrect ? '#a5d6a7' : '#ef9a9a' })
  .opacity(this.feedbackOpacity)           // 透明度动画
  .translate({ y: this.feedbackOffsetY })  // 位移动画
  .scale({ x: this.feedbackScale, y: this.feedbackScale })  // 缩放动画
  .margin({ bottom: 16 });
}

✅ 正确做法:使用多个 @State 变量(opacity、offsetY、scale)分别控制不同的动画属性,animateTo 会同时对这些属性进行插值。

❌ 错误做法:只用 showFeedback 的 true/false 控制显示/隐藏,没有过渡效果,体验生硬。

答题进度条的平滑增长:

// 当前实现:进度条直接跳变
Progress({ value: this.currentIndex + 1, total: this.questionCount })

// ✅ 优化:用 animateTo 实现进度条平滑过渡
@State progressValue: number = 1;

nextQuestion() {
  const hasMore = quizEngine.nextQuestion();
  if (hasMore) {
    const nextQ = quizEngine.getCurrentQuestion();
    this.currentIndex++;
    this.selectedOption = -1;
    this.showFeedback = false;

    // 进度条平滑增长
    animateTo({ duration: 400, curve: Curve.EaseInOut }, () => {
      this.progressValue = this.currentIndex + 1;
    });

    // 反馈区域重置为隐藏状态(不使用动画,瞬间重置)
    this.feedbackOpacity = 0;
    this.feedbackOffsetY = 20;
    this.feedbackScale = 0.95;

    this.currentQuestion = nextQ;
  } else {
    this.currentQuestion = null;
    this.showFeedback = false;
  }
}

3. 收藏按钮的状态切换动画

TopicDetail 页面底部的收藏按钮,当前实现是通过 promptAction.showToast 显示"已收藏/已取消收藏"的提示。我们可以用 animateTo 让按钮本身产生视觉反馈,增强操作确认感。

当前实现回顾:

toggleFavorite() {
  if (!this.topic) return;
  userPrefs.toggleFavorite(this.topic.id).then((isFav: boolean) => {
    this.isFavorite = isFav;
    promptAction.showToast({
      message: isFav ? '已收藏' : '已取消收藏',
      duration: 1500
    });
  });
}

使用 animateTo 增强收藏按钮反馈:

@State isFavorite: boolean = false;
@State collectIconScale: number = 1;         // 收藏图标缩放
@State collectBgColor: string = ThemeColors.BG_PRIMARY;  // 按钮背景色

toggleFavorite() {
  if (!this.topic) return;
  userPrefs.toggleFavorite(this.topic.id).then((isFav: boolean) => {
    // ✅ 正确:用 animateTo 实现收藏状态的平滑切换
    animateTo({
      duration: 250,
      curve: Curve.EaseOut,
      iterations: 1,  // 只播放一次
      onFinish: () => {
        // 动画结束后恢复图标原始大小
        animateTo({ duration: 200, curve: Curve.EaseInOut }, () => {
          this.collectIconScale = 1;
        });
      }
    }, () => {
      this.isFavorite = isFav;
      this.collectBgColor = isFav ? '#fff0f0' : ThemeColors.BG_PRIMARY;
      this.collectIconScale = 1.3;  // 先放大
    });

    promptAction.showToast({
      message: isFav ? '已收藏' : '已取消收藏',
      duration: 1500
    });
  }).catch((err: Error) => {
    Logger.error(TAG, '切换收藏失败', err);
    promptAction.showToast({
      message: '操作失败,请重试',
      duration: 1500
    });
  });
}

在 build 中应用动画状态:

Row({ space: 6 }) {
  Image($r('app.media.icon_collect'))
    .width(16)
    .height(16)
    .fillColor(this.isFavorite ? ThemeColors.PRIMARY : '#cccccc')
    .scale({ x: this.collectIconScale, y: this.collectIconScale });  // 缩放动画
  Text('收藏')
    .fontSize(14)
    .fontWeight(FontWeight.Medium)
    .fontColor(ThemeColors.PRIMARY);
}
.padding({ left: 24, right: 24, top: 10, bottom: 10 })
.backgroundColor(this.collectBgColor)  // 背景色动画
.border({ width: 1, color: ThemeColors.PRIMARY })
.borderRadius(9999)
.onClick(() => {
  this.toggleFavorite();
});

动画时序分析:

用户点击收藏
  │
  ├─ 0ms: animateTo 启动
  │    ├─ isFavorite = true
  │    ├─ collectBgColor = '#fff0f0'
  │    └─ collectIconScale = 1.3(图标放大)
  │
  ├─ 250ms: onFinish 回调触发
  │    └─ animateTo 启动(恢复动画)
  │         └─ collectIconScale = 1(图标恢复)
  │
  └─ 450ms: 全部动画完成
       └─ 图标先放大再缩回,形成"弹跳"效果

4. iterations 重复动画:按钮闪烁提示

animateToiterations 参数可以实现动画的重复播放。在《奇妙科学乐园》中,我们可以用它来实现"签到提醒按钮"的闪烁效果,引导儿童用户注意到需要打卡。

@State checkInBtnOpacity: number = 1;
private blinkTimer: number = -1;

aboutToAppear() {
  // 签到按钮闪烁提示:持续闪烁直到用户点击
  this.startCheckInBlink();
}

aboutToDisappear() {
  if (this.blinkTimer !== -1) {
    clearInterval(this.blinkTimer);
    this.blinkTimer = -1;
  }
}

/**
 * 签到按钮闪烁提示动画
 * 使用 animateTo + iterations 实现有限次重复
 */
startCheckInBlink(): void {
  if (this.todayChecked) return;

  // ✅ 正确:iterations: 3 表示动画重复3次
  animateTo({
    duration: 600,
    curve: Curve.EaseInOut,
    iterations: 3,       // 闪烁3次后停止
    playMode: PlayMode.Alternate,  // 正反交替播放
    onFinish: () => {
      // 3次闪烁后,每5秒再次闪烁,持续提醒
      if (!this.todayChecked) {
        this.blinkTimer = setTimeout(() => {
          this.startCheckInBlink();
        }, 5000);
      }
    }
  }, () => {
    this.checkInBtnOpacity = 0.5;  // 从1变到0.5
  });
}

onCheckIn(): void {
  if (this.todayChecked) return;
  // 停止闪烁
  if (this.blinkTimer !== -1) {
    clearTimeout(this.blinkTimer);
    this.blinkTimer = -1;
  }
  // 立即恢复按钮状态
  this.checkInBtnOpacity = 1;
  // ... 其他打卡逻辑
}

iterations 参数详解:

iterations 值 行为 适用场景
1(默认) 播放一次 普通状态切换
2 播放两次 双击强调
3 播放三次 闪烁提示
-1 无限循环 加载指示器

playMode 参数详解:

PlayMode 值 行为 每次迭代效果
PlayMode.Normal 每次从头播放 A→B, A→B, A→B
PlayMode.Reverse 每次反向播放 A→B, B→A, A→B
PlayMode.Alternate 正反交替 A→B, B→A, A→B

✅ 正确做法:PlayMode.Alternate + iterations: 3 组合,实现"亮→暗→亮→暗→亮→暗"的闪烁效果。

❌ 错误做法:用 setInterval + setTimeout 手动控制闪烁,代码复杂且容易出 bug。

5. 首页骨架屏到真实内容的切换过渡

Index 页面从骨架屏切换到真实内容时,当前是瞬间跳变。用 animateTo 可以实现骨架屏的淡出 + 真实内容的淡入,过渡更加自然。

@State isLoading: boolean = true;
@State contentOpacity: number = 0;       // 内容区域透明度
@State skeletonOpacity: number = 1;      // 骨架屏透明度

/**
 * 从 scienceData 加载数据
 */
private loadData(): void {
  this.hasError = false;

  // ✅ 正确:用 animateTo 实现骨架屏→内容的平滑过渡
  animateTo({
    duration: 500,
    curve: Curve.EaseInOut,
  }, () => {
    this.isLoading = false;
    this.contentOpacity = 1;
    this.skeletonOpacity = 0;
  });

  if (this.loadTimer >= 0) {
    clearInterval(this.loadTimer);
    this.loadTimer = -1;
  }
  this.recommendedTopics = scienceData.getRecommendedTopics(3);
  this.categories = scienceData.getAllCategories();
}

在 build 中应用透明度:

// 骨架屏:加载中
if (this.isLoading) {
  Column() {
    this.SkeletonContent();
  }
  .width('100%')
  .height('100%')
  .opacity(this.skeletonOpacity);  // 淡出

} else if (this.hasError) {
  this.ErrorContent();

} else {
  // 正常状态:淡入
  Column() {
    // ... 真实内容
  }
  .width('100%')
  .height('100%')
  .opacity(this.contentOpacity);  // 淡入
}

6. 实验步骤切换的过渡动画

LabDetail 页面在切换实验步骤时,当前是直接替换内容。我们可以添加 animateTo 实现步骤内容的淡出→淡入过渡。

@State currentStep: number = 0;
@State stepContentOpacity: number = 1;     // 步骤内容透明度
@State stepContentOffsetY: number = 0;    // 步骤内容Y偏移

nextStep() {
  if (this.experiment && this.currentStep < this.experiment.steps.length - 1) {
    // ✅ 正确:先淡出当前步骤
    animateTo({
      duration: 200,
      curve: Curve.EaseIn,
      onFinish: () => {
        // 淡出完成后切换步骤
        this.currentStep++;
        this.stepContentOffsetY = 20;  // 设置偏移初始值

        // 再淡入新步骤
        animateTo({
          duration: 300,
          curve: Curve.EaseOut,
        }, () => {
          this.stepContentOpacity = 1;
          this.stepContentOffsetY = 0;
        });
      }
    }, () => {
      this.stepContentOpacity = 0;
      this.stepContentOffsetY = -20;  // 向上滑出
    });
  }
}

动画时序分析:

用户点击"下一步"
  │
  ├─ 0ms: animateTo(淡出) 启动
  │    ├─ stepContentOpacity: 1 → 0
  │    └─ stepContentOffsetY: 0 → -20(向上滑出)
  │
  ├─ 200ms: onFinish 触发
  │    ├─ currentStep++(切换步骤数据)
  │    └─ animateTo(淡入) 启动
  │         ├─ stepContentOpacity: 0 → 1
  │         └─ stepContentOffsetY: 20 → 0(从下方滑入)
  │
  └─ 500ms: 全部动画完成

✅ 正确做法:通过 onFinish 回调实现"先淡出→切换数据→再淡入"的动画串联。

❌ 错误做法:同时修改 currentStep 和透明度,导致"新内容以半透明状态出现"的闪烁问题。


📊 animateTo 参数速查表

duration 时长选择指南

时长范围 感受 适用场景 项目案例
100-200ms 快速、敏捷 按钮按压、图标切换 收藏图标弹跳
200-400ms 自然、舒适 状态切换、内容展开 答题反馈淡入
400-600ms 从容、优雅 页面过渡、大区块变化 骨架屏切换
600-1000ms 缓慢、隆重 强调性动画 步骤切换

curve 曲线选择指南

// 根据交互类型选择合适的缓动曲线

// ✅ 入场动画:快速出现,缓慢稳定
animateTo({ curve: Curve.EaseOut, duration: 300 }, () => { ... });

// ✅ 退场动画:缓慢启动,快速离开
animateTo({ curve: Curve.EaseIn, duration: 200 }, () => { ... });

// ✅ 状态切换:两端缓慢,中间快速
animateTo({ curve: Curve.EaseInOut, duration: 400 }, () => { ... });

// ✅ 物理交互:模拟惯性减速
animateTo({ curve: Curve.Friction, duration: 500 }, () => { ... });

// ✅ 快捷操作:极速响应
animateTo({ curve: Curve.Sharp, duration: 150 }, () => { ... });

iterations 使用模板

// ✅ 单次播放(默认)
animateTo({ iterations: 1 }, () => { this.scale = 1.2; });

// ✅ 有限次重复(如闪烁3次)
animateTo({ iterations: 3, playMode: PlayMode.Alternate }, () => {
  this.opacity = 0.5;
});

// ✅ 无限循环(如加载动画)
animateTo({ iterations: -1, playMode: PlayMode.Alternate }, () => {
  this.rotation = 360;
});

⚠️ 避坑指南

1. 闭包外修改状态不会触发动画

// ❌ 错误:在 animateTo 闭包之外修改状态,不产生动画效果
animateTo({ duration: 300, curve: Curve.EaseInOut }, () => {
  this.showFeedback = true;
});
this.feedbackOpacity = 1;  // 这行在闭包外,不会参与动画插值!

// ✅ 正确:所有需要动画过渡的状态变更都必须放在闭包内
animateTo({ duration: 300, curve: Curve.EaseInOut }, () => {
  this.showFeedback = true;
  this.feedbackOpacity = 1;
  this.feedbackOffsetY = 0;
});

2. 异步回调中使用 animateTo 的注意事项

// ❌ 错误:在 Promise 回调中直接使用 animateTo 可能不生效
userPrefs.toggleFavorite(id).then((isFav) => {
  // 如果此时组件已被销毁,animateTo 可能报错
  animateTo({ duration: 300 }, () => { this.isFavorite = isFav; });
});

// ✅ 正确:添加组件存活检查
userPrefs.toggleFavorite(id).then((isFav: boolean) => {
  if (!this.topic) return;  // 组件已销毁,不执行动画
  animateTo({ duration: 300, curve: Curve.EaseOut }, () => {
    this.isFavorite = isFav;
  });
});

3. duration 不宜过长

// ❌ 错误:duration 设置过长,用户等待感明显
animateTo({ duration: 2000 }, () => { this.showDetail = true; });

// ✅ 正确:儿童应用中,单个动画不超过600ms
animateTo({ duration: 400, curve: Curve.EaseInOut }, () => {
  this.showDetail = true;
});

4. 多个 animateTo 嵌套的风险

// ❌ 错误:animateTo 嵌套调用,可能导致动画状态混乱
animateTo({ duration: 300 }, () => {
  this.opacity = 0.5;
  animateTo({ duration: 200 }, () => {  // 嵌套!
    this.scale = 1.2;
  });
});

// ✅ 正确:使用 onFinish 串联动画
animateTo({
  duration: 300,
  curve: Curve.EaseOut,
  onFinish: () => {
    animateTo({ duration: 200, curve: Curve.EaseInOut }, () => {
      this.scale = 1.2;
    });
  }
}, () => {
  this.opacity = 0.5;
});

5. 动画属性必须是可插值的

// ❌ 错误:boolean 类型不能被 animateTo 插值
animateTo({ duration: 300 }, () => {
  this.showFeedback = true;  // boolean 跳变,无插值过程
});

// ✅ 正确:使用数值类型控制可插值属性
animateTo({ duration: 300 }, () => {
  this.showFeedback = true;  // 控制 if 渲染
  this.feedbackOpacity = 1;  // opacity 可插值
  this.feedbackOffsetY = 0;  // translateY 可插值
});

🔄 进阶思考:animateTo 与 transition 的对比

ArkUI 还提供了 transition 属性式转场动画,它与 animateTo 的区别在于:

维度 animateTo 显式动画 transition 属性式动画
触发方式 代码主动调用 组件出现/消失时自动触发
控制粒度 精确控制时长、曲线 相对固定
适用场景 状态变更的过渡 if/ForEach 控制的组件出入场
配合使用 独立使用 可与 animateTo 叠加
// transition 的用法(组件出入场自动动画)
if (this.showFeedback) {
  Column() {
    Text('回答正确!')
  }
  .transition(TransitionEffect.opacity(AnimationConstants.DURATION)
    .translate({ y: 20 }))
  // 当 showFeedback 从 false 变为 true 时,
  // 组件自动执行 opacity 0→1 + translateY 20→0 的入场动画
}

在《奇妙科学乐园》中,我们推荐:

  • 状态数值变化(opacity、scale、translate):使用 animateTo
  • 组件出现/消失(if 条件渲染):使用 transitionanimateTo + 数值控制
  • 页面间跳转:使用 PageTransitionEnter/Exit(第81篇详解)

📝 小结

animateTo 显式动画是 ArkUI 动画体系中使用频率最高的 API。本文从以下七个方面进行了系统讲解:

  1. 两种动画范式对比:setInterval 持续动画 vs animateTo 事件驱动动画的本质区别
  2. API 签名解析:duration、curve、delay、iterations、playMode、onFinish 六大参数详解
  3. 打卡弹窗动画:onFinish 回调实现动画完成后的后续操作
  4. 答题反馈过渡:opacity + translateY + scale 多属性组合动画
  5. 收藏按钮反馈:onFinish 串联实现"放大→恢复"的弹跳效果
  6. iterations 闪烁提示:PlayMode.Alternate 实现签到按钮的有限次闪烁
  7. 步骤切换动画:onFinish 串联"淡出→切换→淡入"的复合动画

核心设计理念:显式动画的本质是"闭包内修改 @State,引擎负责插值"。开发者只需关注"从什么状态变到什么状态",以及"花多长时间、按什么节奏变化",具体的逐帧计算由 ArkUI 渲染引擎自动完成。 这种声明式的动画思维,让复杂过渡效果的实现变得极其简洁。


源码仓库https://atomgit.com/2301_79280419/WonderSciencePark
相关文件

  • entry/src/main/ets/components/common/CheckInDialog.ets
  • entry/src/main/ets/pages/Quiz.ets
  • entry/src/main/ets/pages/TopicDetail.ets
  • entry/src/main/ets/pages/LabDetail.ets
  • entry/src/main/ets/pages/Index.ets

🔗 相关链接

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐