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;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,928评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,192评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,468评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,186评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,295评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,374评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,403评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,186评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,610评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,906评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,075评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,755评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,393评论 3 320
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,079评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,313评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,934评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,963评论 2 351

推荐阅读更多精彩内容

  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,937评论 6 13
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,605评论 18 399
  • 深入响应式 追踪变化: 把普通js对象传给Vue实例的data选项,Vue将使用Object.defineProp...
    冥冥2017阅读 4,856评论 6 16
  • 我说的是我今天看到的一双眼睛。 今天的to do list 有一个任务,标色是黄色,不轻不重。是要和搭档一起采访一...
    _SSharon阅读 270评论 0 1
  • 前有一声 萦绕耳边 日夜不停 今有一惑 苦自思量 夜夜无眠 曾几何时 一歌 名何 颜如许 若弦撩动止水心 弦何在 ...
    dear依子阅读 177评论 0 1