Laya 动画系列一 Tween和TimeLine

一、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;
}

你可能感兴趣的:(Laya 动画系列一 Tween和TimeLine)