引言
在 Cocos Creator 开发中,进度条(ProgressBar)的动画需求远不止"从 0 到 1"这么简单。我们常常遇到这样的场景:
-
一次性完成多个任务,获得积分跨越多级,最后定格在对应级的对应积分;
-
一个大型任务被拆分为多个子阶段,每个阶段内进度线性变化,但阶段之间的进度占比固定;
-
动画过程中需要动态调整参数(如循环次数、时长),且希望视觉上连续不突兀;
-
需要随时暂停、恢复进度动画,并准确回调当前状态。
Cocos 自带的 tween 模块虽然强大,但面对上述复杂逻辑,代码会迅速变得臃肿且难以维护。为此,我们设计并封装了两个高复用性的进度条动画组件:TweenProgressLoopComp(循环进度组件)和 TweenProgressSegmentComp(分段进度组件)。本文将深入剖析它们的核心实现原理、关键代码片段以及典型应用场景,帮助你在项目中游刃有余地驾驭各种进度动画。
一、TweenProgressLoopComp:循环 + 最终目标进度
1. 功能概述
该组件让进度条先完成若干次从 0 到 1 的循环,每次循环结束后可停留短暂间隔,最后到达指定的目标进度(可以是任意值,不一定是 1)。它完美模拟了"蓄力 - 释放"或"加载 - 完成"的视觉效果。
2. 核心属性与方法
| 属性/方法 | 说明 |
|---|---|
duration, delay, interval, easeType |
基础动画参数 |
loopCount |
循环次数(0 表示无循环,直接到目标) |
targetProgress |
最终停留的目标进度(0~1) |
setProgress(params, isAnimate) |
设置目标并播放/不播放动画 |
pause() / resume() |
暂停/恢复动画 |
updateParams(params, smooth) |
更新动画参数,smooth 为 true 时平滑过渡 |
| 五类回调 | start, update, complete, segmentStart, segmentComplete |
3. 核心代码与原理剖析
3.1 Tween 链的动态构建
组件根据 loopCount 决定构建简单动画或循环动画。下面是循环动画的构建代码:
TypeScript
private createLoopTween(): void {
const remainingLoops = this.getParam('loopCount', 0) - this.currentLoopCount;
if (remainingLoops <= 0) {
this.createFinalSegment().start();
return;
}
// 第一个循环段(可能带有初始延迟)
let fullTween = this.createLoopSegment(true);
for (let i = 1; i < remainingLoops; i++) {
fullTween = fullTween.then(this.createLoopSegment(false));
}
// 最后追加最终段
this.currentTween = fullTween.then(this.createFinalSegment()).start();
}
private createLoopSegment(isInitial: boolean): Tween<ProgressBar> {
return tween(this.target)
.delay(isInitial ? this.getParam('delay', 0) : 0)
.call(() => this.executeCallback('segmentStart', 0, this.currentLoopCount + 1))
.to(this.getParam('duration', 1.0), { progress: 1 }, {
easing: this.getParam('easeType', 'linear'),
onUpdate: () => this.executeCallback('update'),
onComplete: () => {
this.currentLoopCount++;
this.executeCallback('segmentComplete', 1, this.currentLoopCount);
},
})
.delay(this.getParam('interval', 0.7))
.call(() => {
if (!this.isAnimationStopped && this.target) {
this.target.progress = 0; // 归零,准备下一次循环
}
});
}
解释:
-
createLoopSegment创建一个标准的循环段:从当前进度到1,完成后回调segmentComplete,然后延迟interval,最后将进度强行置0。 -
createLoopTween根据剩余循环次数,将多个循环段用then串联,最后追加createFinalSegment()。 -
这种链式构建保证了每段之间的顺序执行,且利用了 tween 的
delay和call实现归零和间隔。
3.2 暂停与恢复的状态机
暂停时记录关键状态,恢复时根据状态重建动画。下面是 resume 中处理循环动画的核心代码:
TypeScript
// 暂停时保存
this.pauseProgress = this.target.progress;
this.animationType = this.activeParams.loopCount === 0 ? 'simple' : 'loop';
this.stopCurrentTween();
// 恢复时(循环动画)
const elapsedInSegment = this.pauseProgress; // 当前段已执行比例 (0~1)
const remainingInSegment = 1 - elapsedInSegment;
const remainingDuration = totalDuration * remainingInSegment;
// 先补完当前段
this.currentTween = tween(this.target)
.to(remainingDuration, { progress: 1 }, {
easing: this.activeParams.easeType,
onComplete: () => {
this.currentLoopCount++;
// 继续剩余循环
const remainingLoops = this.activeParams.loopCount! - this.currentLoopCount;
if (remainingLoops > 0) {
this.currentTween = this.buildRemainingLoopTween(remainingLoops).start();
} else {
this.currentTween = this.createFinalSegment().start();
}
}
})
.start();
关键点:
-
暂停时记录了当前进度
pauseProgress,该值就是当前段内的完成比例(因为每段都是从0到1)。 -
恢复时先计算当前段剩余时长,用
to动画补完当前段。 -
当前段完成后,再根据剩余循环次数构建后续段或最终段。
3.3 平滑参数更新(smooth 模式)
updateParams 的 smooth 模式复用暂停/恢复机制,实现无缝参数变更:
TypeScript
public updateParams(params: AnimationParams, smooth: boolean = false): void {
Object.assign(this.activeParams, params);
if (this.currentTween && !this.isAnimationStopped && !this.isPaused) {
if (smooth) {
this.pause(); // 记录当前状态
this.resume(); // 基于新参数重建动画
} else {
this.restartAnimation();
}
}
}
二、TweenProgressSegmentComp:多段进度,精准映射
1. 功能概述
该组件将进度条划分为多个段,每段占整体进度的比例固定,但每段对应的数值范围可以自定义。支持两种模式:
-
均匀值模式:每段数值范围均匀。
-
不均匀值模式:每段数值范围可自定义(累加或绝对值)。
2. 核心原理:值与进度的双向映射
这是分段组件的数学基础。内部维护 segmentData 数组,包含每段的起始值、结束值以及占整体进度的比例(固定为 1/段数)。两个核心转换函数如下:
TypeScript
private valueToProgress(value: number): number {
if (value >= this.maxValue) return 1;
if (value <= 0) return 0;
if (this._segmentMode === SegmentMode.EVEN_PROGRESS_EVEN_VALUE) {
return value / this.maxValue;
}
const segmentIndex = this.findSegmentForValue(value);
const segment = this.segmentData[segmentIndex];
const segmentRatio = (value - segment.startValue) / (segment.endValue - segment.startValue);
return (segmentIndex + segmentRatio) / this.segmentData.length;
}
private progressToValue(progress: number): number {
if (progress >= 1) return this.maxValue;
if (progress <= 0) return 0;
if (this._segmentMode === SegmentMode.EVEN_PROGRESS_EVEN_VALUE) {
return progress * this.maxValue;
}
const segmentCount = this.segmentData.length;
const segmentIndex = Math.min(Math.floor(progress * segmentCount), segmentCount - 1);
const segment = this.segmentData[segmentIndex];
const segmentProgress = (progress - segmentIndex / segmentCount) * segmentCount; // 0~1
return segment.startValue + segmentProgress * (segment.endValue - segment.startValue);
}
解释:
-
valueToProgress:找到值所在的段,计算段内比例,再转换为整体进度。 -
progressToValue:根据整体进度定位段,计算段内进度,再映射为数值。 -
这两个函数保证了无论动画如何驱动
progress,我们都能实时得到准确的currentValue。
3. 分段动画的构建
当调用 setProgress(targetValue) 时,组件会遍历所有经过的段,为每个段创建子 tween。下面是正向遍历的代码片段:
TypeScript
private createProgressTween(): void {
const direction = this.targetValue > this.currentValue ? 1 : -1;
const startSegment = this.currentSegment;
const endSegment = this.findSegmentForValue(this.targetValue);
let tweenChain: Tween<ProgressBar> | null = null;
let isFirstSegment = true;
if (direction > 0) {
for (let seg = startSegment; seg <= endSegment; seg++) {
const segEndValue = (seg === endSegment) ? this.targetValue : this.segmentData[seg].endValue;
const segTween = this.createSegmentTween(seg, segEndValue, isFirstSegment, direction);
tweenChain = tweenChain ? tweenChain.then(segTween) : segTween;
if (seg < endSegment) {
tweenChain = tweenChain.delay(this.getParam('interval', 0.5));
}
isFirstSegment = false;
}
} // 反向类似...
// 最后添加 complete 回调
this.currentTween = tweenChain.call(() => this.executeCallback('complete')).start();
}
每个段的具体动画由 createSegmentTween 生成:
TypeScript
private createSegmentTween(segmentIndex: number, targetValue: number, isFirstSegment: boolean, direction: 1 | -1): Tween<ProgressBar> {
const segment = this.segmentData[segmentIndex];
const startProgress = this.valueToProgress(this.currentValue);
const targetProgress = this.valueToProgress(targetValue);
const progressDelta = Math.abs(targetProgress - startProgress);
const segmentFullProgress = segment.progressSize; // 1/段数
const progressRatio = progressDelta / segmentFullProgress;
const duration = this.getParam('duration', 1.0) * progressRatio; // 按比例缩放时长
return tween(this.target)
.call(() => {
if (isFirstSegment) this.executeCallback('start');
})
.delay(isFirstSegment ? this.getParam('delay', 0) : 0)
.to(duration, { progress: targetProgress }, {
easing: this.getParam('easeType', 'linear'),
onStart: () => {
// 如果刚好在段边界,触发 segmentStart
if (this.approxEqual(this.currentValue, segment.startValue) ||
this.approxEqual(this.currentValue, segment.endValue)) {
this.executeCallback('segmentStart', segmentIndex);
}
},
onUpdate: (target) => {
this.currentValue = this.progressToValue(target.progress);
this.executeCallback('update', segmentIndex);
},
onComplete: () => {
this.currentValue = targetValue;
// 更新当前段索引
if (direction > 0 && targetValue >= segment.endValue - 1e-6) {
this.currentSegment = segmentIndex + 1;
} else if (direction < 0 && targetValue <= segment.startValue + 1e-6) {
this.currentSegment = segmentIndex - 1;
}
this.executeCallback('segmentComplete', segmentIndex);
},
});
}
关键点:
-
时长
duration根据该段实际需要移动的进度比例进行缩放(例如只走半段,时长减半)。 -
通过
valueToProgress和progressToValue保证数值与进度同步。 -
段完成后更新
currentSegment,便于下一段构建。
4. 暂停恢复的复杂状态管理
由于分段动画涉及多段,暂停时需要保存足够的信息:
TypeScript
private pauseState: {
progress: number;
currentValue: number;
currentSegment: number;
targetValue: number;
remainingDuration: number; // 当前段剩余时长(秒)
direction: 1 | -1;
} | null = null;
public pause(): void {
if (this.currentTween && !this.isPaused && this.target) {
const progress = this.target.progress;
const direction = this.targetValue > this.currentValue ? 1 : -1;
const segmentProgress = this.calculateSegmentProgress(this.currentValue, this.currentSegment);
const remainingRatio = direction > 0 ? (1 - segmentProgress) : segmentProgress;
const remainingDuration = this.getParam('duration', 1.0) * remainingRatio;
this.pauseState = { progress, currentValue: this.currentValue, currentSegment: this.currentSegment, targetValue: this.targetValue, remainingDuration, direction };
this.stopCurrentTween();
this.isPaused = true;
}
}
恢复时,对第一段使用固定的 remainingDuration,后续段仍用标准时长:
TypeScript
private createResumeTween(remainingFirstSegmentDuration: number, direction: 1 | -1): void {
// 遍历剩余段,第一段使用 fixedDuration = remainingFirstSegmentDuration
for (let seg = startSegment; seg <= endSegment; seg++) {
const segTween = this.createResumeSegmentTween(seg, segEndValue, isFirstSegment, direction,
isFirstSegment ? remainingFirstSegmentDuration : undefined);
// 链式组合...
}
}
private createResumeSegmentTween(segmentIndex: number, targetValue: number, isFirstSegment: boolean, direction: 1 | -1, fixedDuration?: number): Tween<ProgressBar> {
const duration = fixedDuration !== undefined ? fixedDuration : this.getParam('duration', 1.0);
// 其余与 createSegmentTween 类似...
}
这种设计保证了从暂停点无缝续播,不会出现跳帧或时长错误。
三、循环组件 vs 分段组件:如何选择?
| 维度 | 循环组件 (Loop) | 分段组件 (Segment) |
|---|---|---|
| 核心诉求 | 重复填充 + 最终定格 | 将进度按阶段划分,每段值可独立定义 |
| 进度-值关系 | 线性,0→1 循环 | 分段线性,支持均匀或自定义值映射 |
| 典型回调需求 | 循环次数、当前循环进度 | 段索引、段内进度、整体进度 |
| 暂停恢复复杂度 | 中等(需记录循环次数、段内进度) | 较高(需记录当前段、剩余时长、方向) |
| 适用场景 | 加载动画、充能、倒计时 | 任务流程、等级经验、多步骤操作 |
两者并非互斥,有时甚至可以组合使用------例如分段组件的每一段内部再使用循环效果,不过这就需要更复杂的嵌套封装了。
四、扩展思考
-
编辑器可视化配置:可以将两个组件的参数暴露在 Cocos Creator 的属性检查器中,方便策划调整。
-
性能优化:当段数极多时,可使用单 tween + 手动计算进度替代链式 tween,避免大量对象创建。
-
更多映射模式 :支持进度非均匀分布(自定义每段占整体比例),只需修改
progressSize计算逻辑即可。
结语
通过封装 TweenProgressLoopComp 和 TweenProgressSegmentComp,我们得以在 Cocos Creator 项目中快速实现高复用性的复杂进度动画。它们不仅解决了基础 tween 难以处理的多段、循环需求,更提供了完善的暂停恢复、参数热更新和回调机制,让开发者能更专注于业务逻辑,而非动画细节。
如果你也在为进度条动画的复杂性而烦恼,不妨尝试基于这两个思路封装自己的组件,或直接参考本文的实现。
本文由作者原创,旨在分享 Cocos 开发中的实用技巧。转载需注明出处。