完整源码SportTrackDemo-CountdownOverlay.ets上一篇文章我们完成了运动健康类控制交互按钮组件本篇内容接上一篇点击「开始」按钮后页面呈现倒计时组件并且增加了视觉感。在运动健康类应用中倒计时是用户点击「开始运动」后的第一个交互。3、2、1、开始这短短几秒的动画既给用户准备时间也营造仪式感。本文将从设计思路出发分享一套流畅的倒计时数字动画组件并支持数字变化回调方便配合语音播报等拓展功能。关于语音播报下一篇文章详细讲解设计思路以及功能封装。一、设计思路1.1 为什么需要倒计时倒计时的核心作用是给用户准备时间。用户点击「开始运动」后不会立即开始记录而是有3秒时间收起手机、调整姿势。同时倒计时动画也营造了仪式感让开始运动更有“出发”的感觉。1.2 动画设计原则原则说明实现方式醒目数字要足够大吸引注意力字体大小 120绿色#4CAF50节奏感每个数字停留时间一致每个数字约1秒衔接感倒计时结束与运动开始无缝衔接「开始」飞入底部按钮位置不遮挡倒计时浮在地图上方背景透明背景色透明只显示数字可扩展支持回调通知父组件当前数字提供onNumberChange回调1.3 动画时序设计数字3: [放大300ms] → [停留400ms] → [淡出300ms] 总计1000ms 数字2: [放大300ms] → [停留400ms] → [淡出300ms] 总计1000ms 数字1: [放大300ms] → [停留400ms] → [淡出300ms] 总计1000ms 开始: [放大300ms] → [停留400ms] → [飞入300ms] 总计1000ms ──────────────────────────────────────────────── 总时长: 约 4 秒1.4 为什么最后加个「开始」一开始我也只想做 3、2、1但总感觉缺点什么就像人说话卡壳了说了一半。虽然只是显示文字但如果加入语音播报呢把运动看成一场正式的比赛裁判员那句「开始」才是真正的命令。3、2、1 只是准备「开始」才是出发的信号。1.5 动画曲线选择动画阶段使用曲线原因数字放大Curve.FastOutSlowIn先快后慢有弹性感数字弹出有力数字淡出Curve.EaseOut缓慢消失自然过渡飞入Curve.EaseIn先慢后快模拟被吸入的效果二、效果预览三、核心代码实现3.1 组件属性定义Componentexportstruct CountdownOverlay{// 是否激活倒计时由父组件控制显示/隐藏PropisActive:booleanfalse;// 背景颜色默认透明不遮挡下层内容PropbgColor:ResourceStrrgba(0, 0, 0, 0);// 倒计时结束回调动画全部执行完毕后触发onFinish?:()void;// 数字/文字变化回调每个数字/文字显示时触发onNumberChange?:(text:string)void;// 当前显示的倒计时文字3、2、1、开始StateprivatecountdownText:string;// 文字缩放比例用于放大/缩小动画StateprivatecountdownScale:number1.0;// 文字透明度用于淡入/淡出动画StateprivatecountdownOpacity:number1.0;// 文字垂直偏移量用于开始飞入动画StateprivatecountdownOffsetY:number0;// 动画是否正在执行防止重复触发StateprivateisAnimating:booleanfalse;// 倒计时步骤数组privatereadonlysteps:string[][3,2,1,开始];// 当前执行到第几步privatecurrentStepIndex:number0;// 存储所有定时器ID用于组件销毁时清理privatetimeouts:number[][];}3.2 构建方法build(){if(this.isActive){Stack(){Column().width(100%).height(100%).backgroundColor(this.bgColor)Text(this.countdownText).fontSize(this.countdownText开始?90:120).fontWeight(FontWeight.Bold).fontColor(#4CAF50).scale({x:this.countdownScale,y:this.countdownScale}).opacity(this.countdownOpacity).offset({y:this.countdownOffsetY})}.width(100%).height(100%).onAppear((){this.resetToDefault();this.startCountdown();}).onDisAppear((){this.clearAllTimeouts();this.resetToDefault();}).hitTestBehavior(HitTestMode.Block)}}3.3 核心动画逻辑privateresetToDefault():void{this.countdownText;this.countdownScale1.0;this.countdownOpacity1.0;this.countdownOffsetY0;this.isAnimatingfalse;this.currentStepIndex0;}privatestartCountdown():void{if(this.isAnimating)return;this.isAnimatingtrue;this.playStepAnimation();}privateplayStepAnimation():void{if(this.currentStepIndexthis.steps.length){this.isAnimatingfalse;this.onFinish?.();return;}consttextthis.steps[this.currentStepIndex];constisLastStep(text开始);// 更新显示文字this.countdownTexttext;// 回调出去播放语音this.onNumberChange?.(text);this.countdownScale0.3;this.countdownOpacity1;this.countdownOffsetY0;this.getUIContext().animateTo({duration:300,curve:Curve.FastOutSlowIn,onFinish:(){consttimeoutIdsetTimeout((){if(isLastStep){this.getUIContext().animateTo({duration:300,curve:Curve.EaseIn,onFinish:(){this.currentStepIndex;this.playStepAnimation();}},(){this.countdownScale0.1;this.countdownOffsetY200;this.countdownOpacity0;});}else{this.getUIContext().animateTo({duration:300,curve:Curve.EaseOut,onFinish:(){this.currentStepIndex;this.playStepAnimation();}},(){this.countdownScale0.5;this.countdownOpacity0;});}},400);this.timeouts.push(timeoutId);}},(){this.countdownScale1.2;});}privateclearAllTimeouts():void{for(constidofthis.timeouts){clearTimeout(id);}this.timeouts[];}设计说明countdownText初始为空动画从 3 开始自然执行无需特殊处理索引使用animateTo实现动画onFinish回调串联步骤setTimeout控制停留时间让用户看清每个数字onNumberChange回调每个数字/文字显示时立即通知父组件方便同步语音播报onDisAppear清理所有定时器并重置状态避免内存泄漏四、父组件使用示例StateisCountdownActive:booleanfalse;privatespeechManager:SpeechManagerSpeechManager.getInstance();privatestartCountdown():void{this.isCountdownActivetrue;}// 在 build 中CountdownOverlay({isActive:this.isCountdownActive,onNumberChange:(text:string){// 动画显示什么数字/文字就播报什么this.speechManager.speakCountdownText(text);},onFinish:(){this.isCountdownActivefalse;this.startTracking();}})五、踩坑经验问题原因解决方案背景不透明遮挡地图背景色设置了不透明使用rgba(0,0,0,0)透明背景点击穿透到下层按钮倒计时层未阻止触摸添加.hitTestBehavior(HitTestMode.Block)「开始」字体太大与数字使用相同字号条件判断设置不同字号开始90数字120动画与组件销毁冲突组件销毁时动画仍在执行onDisAppear中清理定时器并重置状态数字重复出现初始值与动画第一帧重复countdownText初始设为空字符串七、总结该组件可直接集成到跑步、骑行、步行等运动场景中使用也适用于任何需要倒计时功能的应用。通过onNumberChange回调父组件可以实时感知倒计时的每一个数字/文字轻松扩展语音播报、日志记录等功能。本文通过完整的代码示例演示了如何设计一个流畅的倒计时组件我分享的不仅仅是如何实现这个动画而是动画的本质是在特定的时间做什么样的“事情”通过一连串的时间-动作完成一套动画。动画也分很多种类而这个动画很简单还有很多复杂的动画通过一系列计算完成某一个动作。如果觉得本文对你有帮助请点赞、收藏、转发谢谢