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

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

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


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

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

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

动效不是"炫技"。好的动效像空气------你感觉不到它的存在,但一旦缺失,一切都变得生硬。本篇要做的,就是用最少的代码,给《灵犀厨房》注入这种"空气感"。
#mermaid-svg-AF2M2sbPX7zcDu7B{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AF2M2sbPX7zcDu7B .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AF2M2sbPX7zcDu7B .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AF2M2sbPX7zcDu7B .error-icon{fill:#552222;}#mermaid-svg-AF2M2sbPX7zcDu7B .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AF2M2sbPX7zcDu7B .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AF2M2sbPX7zcDu7B .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AF2M2sbPX7zcDu7B .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AF2M2sbPX7zcDu7B .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AF2M2sbPX7zcDu7B .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AF2M2sbPX7zcDu7B .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AF2M2sbPX7zcDu7B .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AF2M2sbPX7zcDu7B .marker.cross{stroke:#333333;}#mermaid-svg-AF2M2sbPX7zcDu7B svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AF2M2sbPX7zcDu7B p{margin:0;}#mermaid-svg-AF2M2sbPX7zcDu7B .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-AF2M2sbPX7zcDu7B .cluster-label text{fill:#333;}#mermaid-svg-AF2M2sbPX7zcDu7B .cluster-label span{color:#333;}#mermaid-svg-AF2M2sbPX7zcDu7B .cluster-label span p{background-color:transparent;}#mermaid-svg-AF2M2sbPX7zcDu7B .label text,#mermaid-svg-AF2M2sbPX7zcDu7B span{fill:#333;color:#333;}#mermaid-svg-AF2M2sbPX7zcDu7B .node rect,#mermaid-svg-AF2M2sbPX7zcDu7B .node circle,#mermaid-svg-AF2M2sbPX7zcDu7B .node ellipse,#mermaid-svg-AF2M2sbPX7zcDu7B .node polygon,#mermaid-svg-AF2M2sbPX7zcDu7B .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AF2M2sbPX7zcDu7B .rough-node .label text,#mermaid-svg-AF2M2sbPX7zcDu7B .node .label text,#mermaid-svg-AF2M2sbPX7zcDu7B .image-shape .label,#mermaid-svg-AF2M2sbPX7zcDu7B .icon-shape .label{text-anchor:middle;}#mermaid-svg-AF2M2sbPX7zcDu7B .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AF2M2sbPX7zcDu7B .rough-node .label,#mermaid-svg-AF2M2sbPX7zcDu7B .node .label,#mermaid-svg-AF2M2sbPX7zcDu7B .image-shape .label,#mermaid-svg-AF2M2sbPX7zcDu7B .icon-shape .label{text-align:center;}#mermaid-svg-AF2M2sbPX7zcDu7B .node.clickable{cursor:pointer;}#mermaid-svg-AF2M2sbPX7zcDu7B .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AF2M2sbPX7zcDu7B .arrowheadPath{fill:#333333;}#mermaid-svg-AF2M2sbPX7zcDu7B .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AF2M2sbPX7zcDu7B .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AF2M2sbPX7zcDu7B .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AF2M2sbPX7zcDu7B .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AF2M2sbPX7zcDu7B .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AF2M2sbPX7zcDu7B .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AF2M2sbPX7zcDu7B .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AF2M2sbPX7zcDu7B .cluster text{fill:#333;}#mermaid-svg-AF2M2sbPX7zcDu7B .cluster span{color:#333;}#mermaid-svg-AF2M2sbPX7zcDu7B div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AF2M2sbPX7zcDu7B .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AF2M2sbPX7zcDu7B rect.text{fill:none;stroke-width:0;}#mermaid-svg-AF2M2sbPX7zcDu7B .icon-shape,#mermaid-svg-AF2M2sbPX7zcDu7B .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AF2M2sbPX7zcDu7B .icon-shape p,#mermaid-svg-AF2M2sbPX7zcDu7B .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AF2M2sbPX7zcDu7B .icon-shape .label rect,#mermaid-svg-AF2M2sbPX7zcDu7B .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AF2M2sbPX7zcDu7B .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AF2M2sbPX7zcDu7B .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AF2M2sbPX7zcDu7B :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 设计目标
核心技法
动效设计的三个维度
转场动画

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):

typescript 复制代码
setTimeout(() => { this.heroReady = true; }, 150);

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


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

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

3.1 实现

typescript 复制代码
@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 核心实现

typescript 复制代码
@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)也需要自己的入场节奏:

typescript 复制代码
.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 做无限呼吸动画:

typescript 复制代码
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 按压缩放

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

typescript 复制代码
@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 收藏按钮爆发动画

typescript 复制代码
@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 字形"视觉流:

typescript 复制代码
@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() 方法,使视频播放进度与下半部步骤展示保持同步:

typescript 复制代码
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 后端
如果你觉得这篇文章对您有所帮助,麻烦您动动发财之手点赞 👍、收藏 ⭐ 和评论 💬。谢谢大家!!

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

相关推荐
罗超驿2 小时前
22.任务清单应用开发实战:从HTML结构到JavaScript交互的完整实现
javascript·html·交互
若兰幽竹10 小时前
【HarmonyOS 6.1 全场景实战】《灵犀厨房》实战(二十一):【服务卡片】在桌面查看烹饪进度——主进程强推与跨进程桥接
服务卡片·华为鸿蒙系统·灵犀厨房·harmonyos6.1
若兰幽竹19 小时前
【HarmonyOS 6.1 全场景实战】《灵犀厨房》实战(排错指南):【服务卡片跳转】页面栈“迷航”——从“回不去的主页”到精准 Tab 唤醒的全链路修复
华为鸿蒙系统·灵犀厨房·harmonyos6.1·排错指南
ZC跨境爬虫21 小时前
跟着 MDN 学CSS day_36:(float、clear与BFC深度解析)
前端·javascript·css·ui·交互
爱喝水的鱼丶1 天前
SAP-ABAP:SAP 简单报表输出开发系列(共6篇)第三篇:SAP ALV 报表样式定制:字段布局与交互功能配置
服务器·开发语言·学习·交互·sap·abap
Wch1G0z8A1 天前
Slickflow.AI 基于 Harness 工程规范的多智能体交互过程实现
人工智能·交互
এ慕ོ冬℘゜2 天前
异步下拉选择组件|动态接口渲染、表单校验、交互优化全方案
交互
前端不太难2 天前
从语言生成到世界交互:AGI的具身化演进之路
状态模式·交互·agi