HarmonyOS 6.1 开发者盛宴|《灵犀厨房》实战(二十三):【交互动效】转场、列表动画与趣味反馈——让每一次点击都有温度
参数选择如果更大如果更小位移 40vp屏幕高度约 5%动画太“跳”,用户注意力被分散几乎感觉不到滑入时长 500ms刚好感知,不等待用户觉得 App 慢动画一闪而过足够让用户感知到“滑入”,但不至于让他们等待。的减速收尾让卡片像“轻轻落下”而非“匀速滑入”,这是物理直觉在 UI 中的投射。本篇是《灵犀厨房》系列中“投入最少、感知最强”的一篇。转场动画:Hero 卡片和推荐瀑布流有了“呼吸感”——不
HarmonyOS 6.1 开发者盛宴|《灵犀厨房》实战(二十三):【交互动效】转场、列表动画与趣味反馈——让每一次点击都有温度
摘要:前面 22 篇我们完成了《灵犀厨房》的核心功能——推荐、菜谱、流转、语音、视频。但功能齐全 ≠ 体验优秀。用户在第一秒就会用「眼睛」和「手指」投票:页面切换卡不卡?列表加载生不生硬?按钮有没有反馈?本篇将用 HarmonyOS 6.1.0 的 **属性动画(.animation)**和 变换(.scale / .translate / .opacity),为《灵犀厨房》注入「灵气」——让页面有呼吸感、让列表有节奏感、让每一次点击都有温度。我们不增加任何新功能,只改动约 80 行动效代码,让 App 从“能用”跃升到“好用”。
一、引言:功能齐全 ≠ 体验优秀
一个有趣的实验:把《灵犀厨房》第 22 篇的版本拿给一个没用过的人,让他从首页点进菜谱详情。他会顺利完成任务——但不会觉得“好用”。他会觉得页面“突然就出现了”,列表“一下子就全出来了”,按钮“点了好像没点”。
这不是 Bug,但比 Bug 更致命——用户不会抱怨,他们只会不再打开。
动效不是“炫技”。好的动效像空气——你感觉不到它的存在,但一旦缺失,一切都变得生硬。本篇要做的,就是用最少的代码,给《灵犀厨房》注入这种“空气感”。
图一解读:三个维度对应三个核心技法,每个技法服务一个明确的设计目标。转场动画用透明度+位移让元素“滑”进来而非“闪现”;列表动画用延迟制造节奏感,像“翻开书页”一样逐项展开;趣味反馈用缩放模拟物理按压,让屏幕上的按钮有了“弹性”。三者互不依赖,可以单独应用,也可以组合使用。
二、核心原理:ArkUI 属性动画的“三段式”模型
在进入具体实现之前,先理解 HarmonyOS 属性动画的工作原理:
初始状态 ──→ 触发条件(状态变量变化) ──→ 目标状态
│
└── .animation({ duration, curve, delay })
│
└── ArkUI 自动插值计算中间帧
你只需要做两件事:
- 声明初始状态和目标状态(如
opacity: 0 → 1) - 告诉 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.Down。onClick 只能感知抬起,无法实现这个效果。
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 注入体验控制快感,让交互如丝般顺滑。
📦 获取基线版本源码包:包括第1-15篇所有代码 + 架构文档 + Flask 后端
如果你觉得这篇文章对您有所帮助,麻烦您动动发财之手点赞 👍、收藏 ⭐ 和评论 💬。谢谢大家!!
纯血鸿蒙,用心造厨。我们下一篇见!
更多推荐



所有评论(0)