HarmonyOS7 串联动画实战:用 setTimeout 编排动画序列
文章目录前言效果预览布局思路逐步实现定义独立的动画状态方块渲染与动画绑定核心setTimeout 串联动画波浪效果的嵌套 setTimeout关键代码踩坑记录坑1延迟时间太短会导致动画吞掉坑2快速连续点击会出bug坑3animation 和 animateTo 的区别坑4临时数组写法的注意事项写在最后前言做 App 动效的时候经常碰到一个需求——几个动画不能同时跑得一个接一个地依次执行。比如四个方块依次弹入画面或者一组卡片像多米诺骨牌一样波浪式出现。这种串联动画在 iOS 上有UIView.animate的 completion 回调在 Web 上有 CSS animation-delay。那在 HarmonyOS7 的 ArkUI 里怎么搞答案可能比你想的要简单setTimeoutanimateTo就能搞定。这篇文章就来拆解串联动画的实现思路顺便把踩过的坑也捋一遍。效果预览最终实现的效果是这样的页面上有四个彩色方块点击依次进场按钮后它们按照从左到右的顺序逐个从透明缩小状态变为正常显示。点击波浪效果则会出现更花哨的效果——每个方块先出现再消失形成一个传递的波浪。按钮效果描述动画时长依次进场四个方块依次从 opacity0 变为 opacity1约 1s 全部完成波浪效果逐个出现再消失形成波浪传递约 1.2s 全部完成| 重置 | 所有方块瞬间回到初始状态 | 无动画 |布局思路整个页面结构不复杂一个纵向滚动容器里面套一个卡片标题文字四个方块的横向排列RowForEach底部按钮组关键是每个方块需要绑定独立的动画状态值。这一点很重要——如果四个方块共享一个状态变量那就只能同时动没法分别控制。逐步实现定义独立的动画状态给每个方块准备一个独立的状态变量EntryComponentstruct SequentialAnimationChain{Statestep1:number0Statestep2:number0Statestep3:number0Statestep4:number0build(){Column(){// ...}}}为什么不用数组说实话ArkUI 的State对数组内元素的变更检测有时候不够灵敏直接改数组的某个索引值不一定能触发 UI 刷新。用独立的变量虽然写起来有点笨但最可靠。方块渲染与动画绑定四个方块通过ForEach渲染每个方块的opacity和scale绑定到对应的状态变量上Row({space:12}){ForEach([0,1,2,3],(idx:number){Column().width(60).height(60).backgroundColor(PRESET_COLORS[idx].value).borderRadius(12).opacity(([this.step1,this.step2,this.step3,this.step4])[idx]).scale({x:([this.step1,this.step2,this.step3,this.step4])[idx],y:([this.step1,this.step2,this.step3,this.step4])[idx]}).animation({duration:400,curve:Curve.EaseOut})})}.width(100%).justifyContent(FlexAlign.Center)这里用了个小技巧——把四个变量组成一个临时数组[this.step1, this.step2, this.step3, this.step4]然后通过索引idx取值。这样ForEach里面就不需要写if-else判断了。核心setTimeout 串联动画依次进场的实现逻辑是最关键的部分Button(依次进场).onClick((){// 先全部重置为 0this.step10;this.step20;this.step30;this.step40// 用不同的延迟时间依次触发动画setTimeout((){animateTo({duration:400},(){this.step11})},0)setTimeout((){animateTo({duration:400},(){this.step21})},200)setTimeout((){animateTo({duration:400},(){this.step31})},400)setTimeout((){animateTo({duration:400},(){this.step41})},600)})核心原理每个setTimeout的延迟时间递增 200ms让四个animateTo调用在时间轴上错开形成依次执行的视觉效果。画个时间轴就清楚了时间轴 0ms → step1 开始动画 (0→1, 持续400ms) 200ms → step2 开始动画 (0→1, 持续400ms) 400ms → step3 开始动画 (0→1, 持续400ms) 600ms → step4 开始动画 (0→1, 持续400ms)波浪效果的嵌套 setTimeout波浪效果更有意思——每个方块先出现再消失而且是逐个传递的Button(波浪效果).onClick((){for(leti0;i4;i){setTimeout((){// 第一阶段出现animateTo({duration:300,curve:Curve.EaseOut},(){if(i0)this.step11if(i1)this.step21if(i2)this.step31if(i3)this.step41})// 第二阶段延迟300ms后消失setTimeout((){animateTo({duration:300},(){if(i0)this.step10if(i1)this.step20if(i2)this.step30if(i3)this.step40})},300)},i*150)}})这段代码嵌套了两层setTimeout外层控制每个方块的启动时间间隔 150ms内层控制方块出现后的消失时机延迟 300ms关键代码把完整的核心逻辑整理一遍。先看重置操作Button(重置).onClick((){this.step10;this.step20;this.step30;this.step40})重置不需要animateTo直接赋值就会瞬间跳回初始状态。如果你希望重置也有动画过渡把赋值放进animateTo里就行。完整的组件结构EntryComponentstruct SequentialAnimationChain{Statestep1:number0Statestep2:number0Statestep3:number0Statestep4:number0build(){Column(){Scroll(){Column(){Text(串联动画).fontSize(18).fontWeight(FontWeight.Bold).margin({bottom:8})Column(){Row({space:12}){ForEach([0,1,2,3],(idx:number){Column().width(60).height(60).backgroundColor(PRESET_COLORS[idx].value).borderRadius(12).opacity(([this.step1,this.step2,this.step3,this.step4])[idx]).scale({x:([this.step1,this.step2,this.step3,this.step4])[idx],y:([this.step1,this.step2,this.step3,this.step4])[idx]}).animation({duration:400,curve:Curve.EaseOut})})}.width(100%).justifyContent(FlexAlign.Center)// 按钮组...}.width(100%).backgroundColor(#FFFFFF).borderRadius(12).padding(16)}.width(100%)}.layoutWeight(1)}.width(100%).height(100%).backgroundColor(#F5F6FA).padding(16)}}踩坑记录坑1延迟时间太短会导致动画吞掉如果两个setTimeout的延迟间隔太短比如只有 50ms前一个动画还没展开后一个就开始了视觉上看起来就像同时出现。建议间隔至少在150-200ms以上。坑2快速连续点击会出bug用户疯狂点击依次进场按钮会导致多组setTimeout叠加方块的状态会乱。解决办法很简单加个节流标记StateisAnimating:booleanfalseButton(依次进场).onClick((){if(this.isAnimating)returnthis.isAnimatingtrue// 动画逻辑...setTimeout((){this.isAnimatingfalse},1200)})坑3animation 和 animateTo 的区别.animation()是声明式动画绑定在组件属性上当属性变化时自动执行。而animateTo()是命令式调用在闭包里改变状态变量来触发动画。串联动画场景下两者配合使用效果最好——用.animation()定义动画参数用animateTo()控制触发时机。坑4临时数组写法的注意事项.opacity(([this.step1,this.step2,this.step3,this.step4])[idx])这行代码每次build()都会创建一个新的临时数组。在ForEach场景下这是可以接受的但如果列表很长比如几十个元素频繁创建数组会有性能问题建议改用 Map 或者独立组件。写在最后串联动画的核心就一句话用setTimeout控制时间轴用animateTo控制动画触发。这种写法虽然不像 iOS 的UIViewPropertyAnimator那么优雅但胜在简单直观不需要学习额外的 API。实际项目中把延迟时间和动画参数抽成常量维护起来也不会太痛苦。