一、tween
缓动--简单的Tween
缓动--逐字缓动
缓动--缓动函数演示
缓动动画
private function _beginLoop():void {
Laya.scaleTimer.frameLoop(1, this, _doEase);
}
/**执行缓动**/
private function _doEase():void {
_updateEase(Browser.now());
}
/**@private */
public function _updateEase(time:Number):void {
var target:* = this._target;
if (!target) return;
//如果对象被销毁,则立即停止缓动
/*[IF-FLASH]*/
if (target is Node && target.destroyed) return clearTween(target);
//[IF-JS]if (target.destroyed) return clearTween(target);
var usedTimer:Number = this._usedTimer = time - this._startTimer - this._delay;
if (usedTimer < 0) return;
if (usedTimer >= this._duration) return complete();
var ratio:Number = usedTimer > 0 ? this._ease(usedTimer, 0, 1, this._duration) : 0;
var props:Array = this._props;
for (var i:int, n:int = props.length; i < n; i++) {
var prop:Array = props[i];
target[prop[0]] = prop[1] + (ratio * prop[2]);
}
if (update) update.run();
}
1.可以看出,帧循环frameLoop根据Browser.now()来计算对象的缓动属性的。ratio根据设定的ease来返回变化值,比如:
/**
* 定义无加速持续运动。
* @param t 指定当前时间,介于 0 和持续时间之间(包括二者)。
* @param b 指定动画属性的初始值。
* @param c 指定动画属性的更改总计。
* @param d 指定运动的持续时间。
* @return 指定时间的插补属性的值。
*/
public static function linearNone(t:Number, b:Number, c:Number, d:Number):Number {
return c * t / d + b;
}
2.update属性是什么呢?可以看出,update是个Handler,每次更新时都会回调。
/**更新回调,缓动数值发生变化时,回调变化的值*/
public var update:Handler;
/** @private */
public function _create(target:*, props:Object, duration:int, ease:Function...
if (!target) throw new Error("Tween:target is null");
this._target = target;
this._duration = duration;
this._ease = ease || props.ease || easeNone;
this._complete = complete || props.complete;
this._delay = delay;
this._props = [];
this._usedTimer = 0;
this._startTimer = Browser.now();
this._usedPool = usePool;
this.update = props.update;
...
3.另外,也可以研究一下clearAll(target),其实用了一个static的字典(js里是个Array)。顺便能看到from和to方法中的coverBefore参数(默认false),如果选择了覆盖,会先执行clearTween,也就是去执行clearAll方法,把目标对象所有缓动都清掉了。
/*[IF-FLASH]*/
private static var tweenMap:flash.utils.Dictionary = new flash.utils.Dictionary(true);
//[IF-JS] private static var tweenMap:Array = {};
//判断是否覆盖
//[IF-JS]var gid:int = (target.$_GID || (target.$_GID = Utils.getGID()));
/*[IF-FLASH]*/
var gid:* = target;
if (!tweenMap[gid]) {
tweenMap[gid] = [this];
} else {
if (coverBefore) clearTween(target);
tweenMap[gid].push(this);
}
/**
* 清理指定目标对象上的所有缓动。
* @param target 目标对象。
*/
public static function clearAll(target:Object):void {
/*[IF-FLASH]*/
if (!target) return;
//[IF-JS]if (!target || !target.$_GID) return;
/*[IF-FLASH]*/
var tweens:Array = tweenMap[target];
//[IF-JS]var tweens:Array = tweenMap[target.$_GID];
if (tweens) {
for (var i:int, n:int = tweens.length; i < n; i++) {
tweens[i]._clear();
}
tweens.length = 0;
}
}
4.from和to方法中的autoRecover参数,注释中用处:是否自动回收,默认为true,缓动结束之后自动回收到对象池
这个参数在传入_create方法时,名字变成usePool了,被保存到全局变量_usedPool中。然后在_clear方法中被使用。
public function _clear():void {
pause();
Laya.scaleTimer.clear(this, firstStart);
this._complete = null;
this._target = null;
this._ease = null;
this._props = null;
if (this._usedPool) {
this.update = null;
Pool.recover("tween", this);
}
}
也就是调用clear时,全部清理干净,如果usePool,则自动放回池子里。
5.complete方法
在_updateEase方法中,可以看到if (usedTimer >= this._duration) return complete();
/**
* 立即结束缓动并到终点。
*/
public function complete():void {
if (!this._target) return;
//立即执行初始化
Laya.scaleTimer.runTimer(this, firstStart);
//缓存当前属性
var target:* = this._target;
var props:* = this._props;
var handler:Handler = this._complete;
//设置终点属性
for (var i:int, n:int = props.length; i < n; i++) {
var prop:Array = props[i];
target[prop[0]] = prop[1] + prop[2];
}
if (update) update.run();
//清理
clear();
//回调
handler && handler.run();
}
也就是不管是自己执行完了,还是提前主动调用了complete方法,都会执行clear方法。那么根据usePool属性,默认为true,会自动还给池子。这里注意可能有BUG:我们某些时候会把Tween当成一个变量保存到全局属性里,然后在页面关闭时,自己去清理掉这个tween.那么如果清理时,这个tween已经执行完,然后通过complete和clear方法还给了池子,并且池子又把这个tween分配给了别的地方在用。此时去清理,就会导致别的TWEEN莫名停掉。比如,场景上有很多鱼在动,我打开了一个面板上面有动画,我提前关闭了面板并清理动画,却发现有某条鱼不动了……此时,最简单的解决方式是不要让面板上的动画自动回收,而是手动来控制回收的时机
二、TimeLine
1.在Tween的_create方法中,最后有个参数runNow
if (runNow) {
if (delay <= 0) firstStart(target, props, isTo);
else Laya.scaleTimer.once(delay, this, firstStart, [target, props, isTo]);
} else {
_initProps(target, props, isTo);
}
return this;
}
private function firstStart(target:*, props:Object, isTo:Boolean):void {
if (target.destroyed) {
this.clear();
return;
}
_initProps(target, props, isTo);
_beginLoop();
}
private function _beginLoop():void {
Laya.scaleTimer.frameLoop(1, this, _doEase);
}
可以看出,runNow为true时,除了_initProps外,还会额外执行_beginLoop。在Tween类中,runNow一直为true,而在TimeLine中则一直为false。这也能说明两者的区别,timeline是创建完多个tween之后,统一运行的。也就是先做什么动画,然后再做什么,整个顺序由时间轴来控制。看一下官方示例使用方式:
timeLine.addLabel("turnRight",0).to(target,{x:450, y:100, scaleX:0.5, scaleY:0.5},2000,null,0)
.addLabel("turnDown",0).to(target,{x:450, y:300, scaleX:0.2, scaleY:1, alpha:1},2000,null,0)
.addLabel("turnLeft",0).to(target,{x:100, y:300, scaleX:1, scaleY:0.2, alpha:0.1},2000,null,0)
.addLabel("turnUp",0).to(target,{x:100, y:100, scaleX:1, scaleY:1, alpha:1},2000,null,0);
timeLine.play(0,true);
timeLine.on(Event.COMPLETE,this,this.onComplete);
timeLine.on(Event.LABEL, this, this.onLabel);
2._startTime默认值为0,相当于TimeLine的时间轴是从0开始算的。
创建的tween数据,都会保存成tweenData内部类的类型,放入_tweenDataList。
public var type:int = 0;//0代表TWEEN,1代表标签
private function _create(target:*, props:Object, duration:Number,
ease:Function, offset:Number, isTo:Boolean):TimeLine {
var tTweenData:tweenData = Pool.getItemByClass("tweenData",tweenData);
tTweenData.isTo = isTo;
tTweenData.type = 0;
tTweenData.target = target;
tTweenData.duration = duration;
tTweenData.data = props;
tTweenData.startTime = _startTime + offset;
tTweenData.endTime = tTweenData.startTime + tTweenData.duration;
tTweenData.ease = ease;
_startTime = Math.max(tTweenData.endTime, _startTime);
_tweenDataList.push(tTweenData);
_startTimeSort = true;
_endTimeSort = true;
return this;
}
可以看到,每次调用_create时,都会更新_startTime(_startTime = Math.max(tTweenData.endTime, _startTime);
注意endTime之前是这样计算的tTweenData.endTime = tTweenData.startTime + tTweenData.duration;
),为下一次_create方法设置时间点做准备。这里也给了我们启发,如果希望用Timeline同时执行两个动作,那么第二个动作的offset可以是一个负值,即前一个动画duration的负值。这样两个动作的startTime就会是一致的。比如希望鱼钩hookImg和绳子ropeImg一起伸长:
this._extendTimeLine.to(this.ropeImg, { height: d }, 1000, Laya.Ease.linearNone);
this._extendTimeLine.to(this.hookImg, { y: d - 4 }, 1000, Laya.Ease.linearNone, -1000);
3.再看看addLabel方法:
/**
* 在时间队列中加入一个标签。
* @param label 标签名称。
* @param offset 标签相对于上个动画的偏移时间(单位:毫秒)。
*/
public function addLabel(label:String, offset:Number):TimeLine {
var tTweenData:tweenData = Pool.getItemByClass("tweenData",tweenData);
tTweenData.type = 1;
tTweenData.data = label;
tTweenData.endTime = tTweenData.startTime = _startTime + offset;
_labelDic || (_labelDic = {});
_labelDic[label] = tTweenData;
_tweenDataList.push(tTweenData);
return this;
}
这里有点奇怪,endTime和startTime是相等的,data居然是label。这说明addLabel添加的并不是一段动画,只是个标记。这个标记携带了一些信息,比如时间点,然后就能通过控制时间,跳到指定的label了
/**
* 从指定的标签开始播。
* @param Label 标签名。
*/
public function gotoLabel(Label:String):void {
if (_labelDic == null) return;
var tLabelData:tweenData = _labelDic[Label];
if (tLabelData) gotoTime(tLabelData.startTime);
}
4.在play方法中,会先根据_startTimeSort标记,来对_tweenDataList进行排序。同时遍历_tweenDataList,如果是tween,则把初始属性都存到_firstTweenDic里面,方便跳转时,回到初始状态。
然后就是和tween一样,开始帧循环frameLoop:
_lastTime = Browser.now();
Laya.timer.frameLoop(1, this, _update);
play方法会根据传入的时点间,或者标签名,进行不同的解析。
if (timeOrLabel is String) {
gotoLabel(timeOrLabel);
} else {
gotoTime(timeOrLabel);
}
5.在_update方法中,会对所有的tween处理:
for (p in _tweenDic) {
tTween = _tweenDic[p];
tTween._updateEase(tCurrTime);
}
也可以看到tween并不是刚开始就全部创建了,而是时间到了才创建。
/**当前动画数据播放到第几个了*/
private var _index:int = 0;
var tTween:Tween;
if (_tweenDataList.length != 0 && _index < _tweenDataList.length) {
var tTweenData:tweenData = _tweenDataList[_index];
if (tCurrTime >= tTweenData.startTime) {
_index++;
//创建TWEEN
if (tTweenData.type == 0) {
_gidIndex++;
tTween = Pool.getItemByClass("tween", Tween);
tTween._create(tTweenData.target, tTweenData.data, tTweenData.duration, tTweenData.ease,
Handler.create(this, _animComplete, [_gidIndex]), 0, false, tTweenData.isTo, true, false);
tTween.setStartTime(tCurrTime);
tTween.gid = _gidIndex;
_tweenDic[_gidIndex] = tTween;
tTween._updateEase(tCurrTime);
} else {
this.event(Event.LABEL, tTweenData.data);
}
}
}
6.最后看看gotoLabel和gotoTime
/**
* 从指定的标签开始播。
* @param Label 标签名。
*/
public function gotoLabel(Label:String):void {
if (_labelDic == null) return;
var tLabelData:tweenData = _labelDic[Label];
if (tLabelData) gotoTime(tLabelData.startTime);
}
gotoTime上来先做了清理工作。然后根据time >= tTweenData.endTime,把已经经历过的属性,直接叠加到对象上;当然在这个For循环开始前,要先针对endTime属性做一下排序。然后就是和_update方法一样,创建当前正在行动的TWEEN放入_tweenDic。至于后续的tween,就继续交给_update帧循环中,等到了时间点,再创建剩余的tween.
这里也能看到,label只是帮助打个时间点,可以快捷地播放这段动画。
timeLine.addLabel("turnRight",0).to(target,{x:450, y:100, scaleX:0.5, scaleY:0.5},2000,null,0)
.addLabel("turnDown",0).to(target,{x:450, y:300, scaleX:0.2, scaleY:1, alpha:1},2000,null,0)
.addLabel("turnLeft",0).to(target,{x:100, y:300, scaleX:1, scaleY:0.2, alpha:0.1},2000,null,0)
.addLabel("turnUp",0).to(target,{x:100, y:100, scaleX:1, scaleY:1, alpha:1},2000,null,0);
timeLine.play("turnUp");
7.帧相关
private var _frameRate:int = 60;
/**
* @private
* 设置帧索引
*/
public function set index(value:int):void {
_frameIndex = value;
gotoTime(_frameIndex / _frameRate * 1000);
}
/**
* 得到总帧数。
*/
public function get total():int {
_total = Math.floor(_startTime / 1000 * _frameRate);
return _total;
}