HarmonyOS 6.1 开发者盛宴|《灵犀厨房》实战(二十三):【交互动效】转场、列表动画与趣味反馈——让每一次点击都有温度

摘要:前面 22 篇我们完成了《灵犀厨房》的核心功能——推荐、菜谱、流转、语音、视频。但功能齐全 ≠ 体验优秀。用户在第一秒就会用「眼睛」和「手指」投票:页面切换卡不卡?列表加载生不生硬?按钮有没有反馈?本篇将用 HarmonyOS 6.1.0 的 **属性动画(.animation)**和 变换(.scale / .translate / .opacity),为《灵犀厨房》注入「灵气」——让页面有呼吸感、让列表有节奏感、让每一次点击都有温度。我们不增加任何新功能,只改动约 80 行动效代码,让 App 从“能用”跃升到“好用”。


一、引言:功能齐全 ≠ 体验优秀

一个有趣的实验:把《灵犀厨房》第 22 篇的版本拿给一个没用过的人,让他从首页点进菜谱详情。他会顺利完成任务——但不会觉得“好用”。他会觉得页面“突然就出现了”,列表“一下子就全出来了”,按钮“点了好像没点”。

这不是 Bug,但比 Bug 更致命——用户不会抱怨,他们只会不再打开

动效不是“炫技”。好的动效像空气——你感觉不到它的存在,但一旦缺失,一切都变得生硬。本篇要做的,就是用最少的代码,给《灵犀厨房》注入这种“空气感”。

设计目标

核心技法

动效设计的三个维度

转场动画
Hero 卡片上滑入场

列表动画
购物清单错落入场

趣味反馈
收藏按钮爆发动画

.opacity() + .translate()
透明度 + 位移

.animation({ delay })
错落延迟

.scale() + onTouch
缩放 + 触摸事件

让页面有呼吸感

让列表有节奏感

让点击有温度

图一解读:三个维度对应三个核心技法,每个技法服务一个明确的设计目标。转场动画用透明度+位移让元素“滑”进来而非“闪现”;列表动画用延迟制造节奏感,像“翻开书页”一样逐项展开;趣味反馈用缩放模拟物理按压,让屏幕上的按钮有了“弹性”。三者互不依赖,可以单独应用,也可以组合使用。


二、核心原理:ArkUI 属性动画的“三段式”模型

在进入具体实现之前,先理解 HarmonyOS 属性动画的工作原理:

初始状态 ──→ 触发条件(状态变量变化) ──→ 目标状态
              │
              └── .animation({ duration, curve, delay })
                    │
                    └── ArkUI 自动插值计算中间帧

你只需要做两件事:

  1. 声明初始状态和目标状态(如 opacity: 0 → 1
  2. 告诉 ArkUI 变化发生时用什么曲线、多久完成.animation({...})

中间的所有插值计算、帧渲染、硬件加速——ArkUI 全部自动完成。这就是声明式动画的核心优势:你描述“从哪到哪”,框架负责“怎么过去”

2.1 动画曲线的选择

曲线 视觉效果 适用场景 本篇使用
Curve.EaseOut 先快后慢,像“轻轻落下” 入场动画、弹回动画
Curve.EaseInOut 两端慢中间快,像“呼吸” 循环动画
Curve.Linear 匀速 进度条 ❌(太机械)

设计决策:为什么本篇几乎全部使用 Curve.EaseOut?因为自然界中没有“匀速运动”——物体总是先快后慢地停下。EaseOut 模拟了这种物理惯性,让动画看起来“自然”而非“机械”。

2.2 动画触发的“黄金窗口”

所有入场动画都有一个微小的 setTimeout 延迟(80~150ms):

setTimeout(() => { this.heroReady = true; }, 150);

这不是为了“炫技”,而是为了避开首次布局的帧丢失。组件首次渲染时,ArkUI 需要完成布局计算、绘制、合成——如果在这期间触发动画,可能被系统跳帧,导致动画“凭空出现”而非“滑入”。150ms 的延迟确保布局完成后再启动动画,用户看到的是完整的入场过程。


三、实战一:转场动画——Hero 卡片上滑入场

首页的 AI 食材识别卡片是用户打开 App 第一眼看到的焦点区域。让它“滑”进来,比直接出现更有吸引力。

3.1 实现

@Local heroReady: boolean = false;

aboutToAppear(): void {
  setTimeout(() => { this.heroReady = true; }, 150);
}

buildHeroCard() {
  Column() {
    // ...卡片内容...
  }
  .opacity(this.heroReady ? 1 : 0)
  .translate({ y: this.heroReady ? 0 : 40 })
  .animation({ duration: 500, curve: Curve.EaseOut })
}

3.2 设计决策:为什么是 40vp 和 500ms?

参数 选择 如果更大 如果更小
位移 40vp 屏幕高度约 5% 动画太“跳”,用户注意力被分散 几乎感觉不到滑入
时长 500ms 刚好感知,不等待 用户觉得 App 慢 动画一闪而过

这是一个平衡点:足够让用户感知到“滑入”,但不至于让他们等待Curve.EaseOut 的减速收尾让卡片像“轻轻落下”而非“匀速滑入”,这是物理直觉在 UI 中的投射。


四、实战二:列表动画——购物清单错落入场

购物清单是分组列表。一次性全部显示会显得“生硬”;每一行错落 60ms 依次出现,则像“被轻轻翻开”。

4.1 核心实现

@Local listReady: boolean = false;

aboutToAppear(): void {
  setTimeout(() => { this.listReady = true; }, 80);
}

ListItem() {
  Row({ space: 10 }) {
    Circle().width(8).height(8).fill(categoryColor)
    Text(item).fontSize(15)
  }
}
.opacity(this.listReady ? 1 : 0)
.translate({ x: this.listReady ? 0 : -20 })
.animation({
  duration: 350,
  curve: Curve.EaseOut,
  delay: 80 + itemIndex * 60   // ← 关键:延迟递增
})

4.2 品类分组的双层延迟

品类(ListItemGroup)也需要自己的入场节奏:

.opacity(this.listReady ? 1 : 0)
.animation({
  duration: 400,
  curve: Curve.EaseOut,
  delay: 100 + categoryIndex * 120   // 品类间延迟 120ms
})

整体效果:

品类「肉类」    ← 延迟 100ms 出现
  牛腩 500g    ← 延迟 180ms 出现
  排骨 500g    ← 延迟 240ms 出现
品类「蔬菜」    ← 延迟 220ms 出现
  番茄 3个     ← 延迟 300ms 出现

4.3 趣味点缀:呼吸圆点

每个食材项末尾加了一个微小的彩色圆点,用 PlayMode.Alternate 做无限呼吸动画:

Circle().width(5).height(5).fill(categoryColor)
  .opacity(0.35)
  .animation({
    duration: 2000 + itemIndex * 300,  // 每项错开周期,避免同步闪烁
    curve: Curve.EaseInOut,
    iterations: -1,                     // 无限循环
    playMode: PlayMode.Alternate        // 来回播放
  })

设计考量:为什么每个圆点要错开 300ms 起始周期?如果所有圆点同时明灭,会产生“集体闪烁”的视觉疲劳。错开周期后,它们像各自在“呼吸”,整体视觉效果柔和且不死板。


五、实战三:趣味反馈——按压缩放与爆发动画

5.1 RecommendCard 按压缩放

用户手指按下卡片时,给出一个微小的“陷下去”的感觉——这是触觉反馈的视觉替代。

@State isPressed: boolean = false;

build() {
  Column() { /* ... */ }
  .scale({ x: this.isPressed ? 0.97 : 1, y: this.isPressed ? 0.97 : 1 })
  .animation({ duration: 150, curve: Curve.EaseOut })
  .onTouch((event: TouchEvent) => {
    if (event.type === TouchType.Down) {
      this.isPressed = true;
    } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
      this.isPressed = false;
    }
  })
}

为什么用 onTouch 而非 onClick

事件 触发时机 能否感知 Down
onClick 手指抬起时
onTouch Down / Up / Cancel 均可

按压反馈的关键是“按下去的瞬间”——TouchType.DownonClick 只能感知抬起,无法实现这个效果。

5.2 收藏按钮爆发动画

@Local heartScale: number = 1;
@Local heartLiked: boolean = false;

Button() {
  if (this.heartLiked) {
    SymbolGlyph($r('sys.symbol.heart_fill')).fontSize(16).fontColor(['#FF2D55'])
  } else {
    SymbolGlyph($r('sys.symbol.heart')).fontSize(16).fontColor([Color.White])
  }
}
.scale({ x: this.heartScale, y: this.heartScale })
.animation({ duration: 300, curve: Curve.EaseOut })
.onClick(() => {
  this.heartLiked = !this.heartLiked;
  this.heartScale = 1.3;                      // 瞬间放大到 130%
  setTimeout(() => { this.heartScale = 1; }, 150);  // 150ms 后弹回
})

动画时序

点击瞬间:  scale 1→1.3, liked false→true
150ms 后:  scale 1.3→1(弹回)
300ms 后:  动画完成

整个爆发动画在 300ms 内完成——这是用户感知“反馈”的最佳窗口。再短则无感,再长则拖沓。

5.3 RecommendCard 错落入场

每张卡片按索引延迟触发入场,在双列瀑布流中自然形成“Z 字形”视觉流:

@Prop itemIndex: number = 0;
@State cardReady: boolean = false;

aboutToAppear(): void {
  setTimeout(() => { this.cardReady = true; }, 80 + this.itemIndex * 70);
}

.opacity(this.cardReady ? 1 : 0)
.translate({ y: this.cardReady ? 0 : 30 })
.animation({ duration: 400, curve: Curve.EaseOut, delay: this.itemIndex * 70 })

六、视频进度 → 步骤同步(续上篇)

在上篇 AVPlayer 集成的基础上,新增 syncStepWithVideoProgress() 方法,使视频播放进度与下半部步骤展示保持同步:

private syncStepWithVideoProgress(): void {
  if (this.videoDuration <= 0 || this.recipe.steps.length <= 1) return;
  const progress: number = this.videoCurrentTime / this.videoDuration;
  const newStepIndex: number = Math.min(
    Math.floor(progress * this.recipe.steps.length),
    this.recipe.steps.length - 1
  );
  if (newStepIndex !== this.currentStepIndex) {
    this.currentStepIndex = newStepIndex;
  }
}

生产环境中,可由后端返回每步的精确时间戳,替换均分逻辑。


七、代码增删改清单

文件 新增/修改 动效变更
pages/Index.ets 修改 Hero 卡片上滑入场
components/RecommendCard.ets 重大修改 新增 isPressed 按压缩放 97%、错落入场上滑、接受 itemIndex 属性
pages/ShoppingListPage.ets 重构 错落入场(双层延迟)、呼吸圆点、品类逐级出现
pages/RecipeDetailPage.ets 修改 收藏按钮爆发动画、视频进度→步骤同步

八、设计决策

决策 选择 理由
入场动画触发时机 setTimeout(80~150ms) 等待首次布局完成,避免动画在帧丢失中被吞掉
缩放比例 97%(按下)/ 130%(爆发) 97% 微妙但可感知;130% 明显但不溢出
列表错落延迟 60ms/项 10 项列表 ≈ 600ms 完成,用户不等待
呼吸动画 PlayMode.Alternate + 错开周期 避免所有圆点同步闪烁,减少视觉疲劳
按压反馈 API onTouch 而非 onClick onClick 只有 Up 事件,无法感知 Down 的按压瞬间
动画曲线 Curve.EaseOut 先快后慢的减速曲线模拟物理惯性,比线性更自然

九、本阶段总结与下篇预告

本篇是《灵犀厨房》系列中“投入最少、感知最强”的一篇。我们没有新增任何功能,但用约 80 行动效代码,让整个 App 从“能用”跃升到“好用”:

  • 转场动画:Hero 卡片和推荐瀑布流有了“呼吸感”——不是凭空出现,而是“滑”进来的
  • 列表动画:购物清单从生硬的静态列表变成了有节奏的“翻开”效果
  • 趣味反馈:收藏按钮的“啵”一下、卡片的“陷进去”,让每次点击都有了温度

好的动效不是“炫技”,而是让用户感觉不到技术在运作——一切如此自然,就像 App 本来就该这样。

下篇预告:第 24 篇《手势操作:滑动调整“火力大小”》。我们要让用户在智慧厨电模拟器上,像拧燃气灶旋钮一样,用手指滑动来调节火力。这是动效与交互的结合——手势驱动的动画,比定时器驱动的动画更贴近直觉。


📚 本系列持续更新中:下一篇《手势操作-滑动调整火力大小》将为 App 注入体验控制快感,让交互如丝般顺滑。

🔗 专栏入口:[《HarmonyOS6.1全场景实战》合集]

📦 获取基线版本源码包包括第1-15篇所有代码 + 架构文档 + Flask 后端

如果你觉得这篇文章对您有所帮助,麻烦您动动发财之手点赞 👍、收藏 ⭐ 和评论 💬。谢谢大家!!

纯血鸿蒙,用心造厨。我们下一篇见!

Logo

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

更多推荐