一、AnimationPlayerBase extends Sprite
动画播放基类,提供了基础的动画播放控制方法和帧标签事件相关功能。可以继承此类,但不要直接实例化此类,因为有些方法需要由子类实现。
/**
* 开始播放动画。play(...)方法被设计为在创建实例后的任何时候都可以被调用,
* 当相应的资源加载完毕、调用动画帧填充方法(set frames)或者将实例显示在舞台上时,
* 会判断是否正在播放中,如果是,则进行播放。
* 配合wrapMode属性,可设置动画播放顺序类型。
* @param start (可选)指定动画播放开始的索引(int)或帧标签(String)。
* 帧标签可以通过addLabel(...)和removeLabel(...)进行添加和删除。
* @param loop (可选)是否循环播放。
* @param name (可选)动画名称。
* @param showWarn(可选)是否动画不存在时打印警告
*/
public function play(start:* = 0, loop:Boolean = true,
name:String = "",showWarn:Boolean=true):void {
this._isPlaying = true;
this.index = (start is String) ? _getFrameByLabel(start) : start;
this.loop = loop;
this._actionName = name;
_isReverse = wrapMode == WRAP_REVERSE;
if (this.interval > 0) {
timerLoop(this.interval, this, _frameLoop, null, true, true);
}
}
可以看到interval控制了帧率,再看看_frameLoop方法,首先判断_isReverse和wrapMode,也就是播放模式,最后执行这一句this.index = this._index;
public function set index(value:int):void {
_index = value;
_displayToIndex(value);
if (_labels && _labels[value]) {
var tArr:Array = _labels[value];
for (var i:int = 0, len:int = tArr.length; i < len; i++) {
event(Event.LABEL, tArr[i]);
}
}
}
/**
* @private
* 显示到某帧
* @param value 帧索引
*/
protected function _displayToIndex(value:int):void {
}
处理了Event.LABEL帧事件,并且把具体如何显示帧,留给了子类去实现_displayToIndex方法。至于帧事件,可以看一下这个方法:
/**
* 增加一个帧标签到指定索引的帧上。当动画播放到此索引的帧时
* 会派发Event.LABEL事件,派发事件是在完成当前帧画面更新之后。
* @param label 帧标签名称
* @param index 帧索引
*/
public function addLabel(label:String, index:int):void {
if (!_labels) _labels = {};
if (!_labels[index]) _labels[index] = [];
_labels[index].push(label);
}
_labels是一个Object,它的key是一个数字,也就是帧索引(注意帧索引,是从0开始的)。它的value是一个数组,会把帧标签名称都存到这个数组中。从上面的set index方法中也能看到,进入某一帧时,会判断_labels相应的帧标签数组,派发里面所有名称的事件。
二、Animation extends AnimationPlayerBase
1.首先去看如何显示帧_displayToIndex
/**@private */
override protected function _displayToIndex(value:int):void {
if (this._frames) this.graphics = this._frames[value];
}
官方对_frames的解释是,_frames是当前动画的帧图像数组。本类中,每个帧图像是一个Graphics对象,而动画播放就是定时切换Graphics对象的过程。可以看到_displayToIndex方法确实是在切换graphics对象。
2.动画缓存池framesMap
public static var framesMap:Object = {};
动画模版缓存池是以一定的内存开销来节省CPU开销,当相同的动画模版被多次使用时,相比于每次都创建新的动画模版,使用动画模版缓存池,只需创建一次,缓存之后多次复用,从而节省了动画模版创建的开销。
动画模版缓存池,以key-value键值对存储,key可以自定义,也可以从指定的配置文件中读取,value为对应的动画模版,是一个Graphics对象数组.使用loadImages(...)、loadAtlas(...)、loadAnimation(...)、set source方法可以创建动画模版。
3.有三个类似的方法:loadImages,loadAtlas,loadAnimation,以及它们三个的综合体set source
public function loadImages(urls:Array, cacheName:String = ""):Animation {
this._url = "";
if (!_setFramesFromCache(cacheName)) {
this.frames = framesMap[cacheName] ?
framesMap[cacheName] : createFrames(urls, cacheName);
}
return this;
}
public function loadAtlas(url:String,
loaded:Handler = null, cacheName:String = ""):Animation {
this._url = "";
var _this_:Animation = this;
function onLoaded(loadUrl:String):void {
if (url === loadUrl) {
_this_.frames = framesMap[cacheName] ?
framesMap[cacheName] : createFrames(url, cacheName);
if (loaded) loaded.run();
}
}
if (!_this_._setFramesFromCache(cacheName)) {
if (Loader.getAtlas(url)) onLoaded(url);
else Laya.loader.load(url, Handler.create(
null, onLoaded, [url]), null, Loader.ATLAS);
}
return this;
}
public function loadAnimation(url:String,
loaded:Handler = null, atlas:String = null):Animation {
this._url = url;
var _this_:Animation = this;
if (!_actionName) _actionName = "";
if (!_this_._setFramesFromCache("")) {
if (!atlas || Loader.getAtlas(atlas)) {
_loadAnimationData(url, loaded, atlas);
} else {
Laya.loader.load(atlas, Handler.create(this, _loadAnimationData,
[url, loaded, atlas]), null, Loader.ATLAS)
}
} else {
_this_._setFramesFromCache(_actionName, true);
index = 0;
if (loaded) loaded.run();
}
return this;
}
根据参数来看:
loadImages加载的是图片路径集合,比如[url1,url2,url3,...]。
loadAtlas加载的是图集路径,比如animation.loadAtlas("resource/ani/fighter.json");
loadAnimation加载的是由IDE创建的ani文件,比如ani.loadAnimation("tinyGame/coin0.ani");
这里注意_url属性,这个属性只有在loadAnimation这个方式时,记录传入的ani路径,其它两个方式,都是空字符串。它的用处是在_setFramesFromCache方法中,强行把传入的name参数变成_url + "#" + name;
/**@private */
protected function _setFramesFromCache(name:String,
showWarn:Boolean = false):Boolean {
if (_url) name = _url + "#" + name;
if (name && framesMap[name]) {
var tAniO:*;
tAniO = framesMap[name];
if (tAniO is Array) {
this._frames = framesMap[name];
this._count = _frames.length;
} else {
if (tAniO.nodeRoot) {
//如果动画数据未解析过,则先进行解析
framesMap[name] = _parseGraphicAnimationByData(tAniO);
tAniO = framesMap[name];
}
this._frames = tAniO.frames;
this._count = _frames.length;
//如果读取的是动画配置信息,帧率按照动画设置的帧率播放
if (!_frameRateChanged) _interval = tAniO.interval;
_labels = _copyLabels(tAniO.labels);
}
return true;
} else {
if (showWarn) trace("ani not found:", name);
}
return false;
}
也就是说,IDE创建的ani,会被缓存成key是"url#动画名称" 对应相应动画名称的动画模板。而另外两种方式,是否缓存,以及缓存key叫什么,是可以自己控制的,对应的参数是cacheName。cacheName默认为空,也就是不进行缓存。如果不为空,则表示使用此值做key进行动画模板缓存。
至于set source,算是对上面三个方法的一个整体封装吧,传入参数如:图集:"xx/a1.atlas";图片集合:"a1.png,a2.png,a3.png";LayaAir IDE动画"xx/a1.ani"
public function set source(value:String):void {
if (value.indexOf(".ani") > -1) loadAnimation(value);
else if (value.indexOf(".json") > -1 ||
value.indexOf("als") > -1 ||
value.indexOf("atlas") > -1) loadAtlas(value);
else loadImages(value.split(","));
}
4.createFrames
这个方法在loadImages和loadAtlas都在使用,区别就是一个传入的是urls(图片路径集合),一个是url(图集路径)。
public static function createFrames(url:*, name:String):Array {
var arr:Array,i:int,n:int,g:Graphics;
if (url is String) {
var atlas:Array = Loader.getAtlas(url);
if (atlas && atlas.length) {
arr = [];
for (i = 0, n = atlas.length; i < n; i++) {
g = new RunDriver.createGraphics();
g.drawTexture(Loader.getRes(atlas[i]), 0, 0);
arr.push(g);
}
}
} else if (url is Array) {
arr = [];
for (i = 0, n = url.length; i < n; i++) {
g = new RunDriver.createGraphics();
g.loadImage(url[i], 0, 0);
arr.push(g);
}
}
if (name) framesMap[name] = arr;
return arr;
}
从这里也能看出如何去创建一个Graphics对象。注意最后一句if (name) framesMap[name] = arr;
,印证了上面所说,cacheName为空时,不会缓存。
由于这是个静态方法,也可以在不创建Animation实例前,先创建动画缓存,比如:
//创建动画模板dizziness
Laya.Animation.createFrames(this.aniUrls("die", 4), "die");
Laya.Animation.createFrames(this.aniUrls("fire", 7), "fire");
Laya.Animation.createFrames(this.aniUrls("atk", 3), "atk");
Laya.Animation.createFrames(this.aniUrls("move", 4), "move");
/**
* 创建一组动画的url数组(美术资源地址数组)
* aniName 动作的名称,用于生成url
* length 动画最后一帧的索引值,
*/
private aniUrls(aniName: string, length: number): any {
var urls: any = [];
for (var i: number = 1; i < length; i++) {
//动画资源路径要和动画图集打包前的资源命名对应起来
urls.push("imgs/role/wp116/" + aniName + i + ".png");
}
return urls;
}
5.clearCache
根据上面所说,IDE创建的ani,会以"url#aniName"作为key来缓存,清理时也要注意这一点。
public static function clearCache(key:String):void {
var cache:Object = framesMap;
var val:String;
var key2:String = key + "#";
for (val in cache) {
if (val === key || val.indexOf(key2) == 0) {
delete framesMap[val];
}
}
}
6.重写的play方法
override public function play(start:* = 0,
loop:Boolean = true, name:String = "",showWarn:Boolean=true):void {
if (name) _setFramesFromCache(name, showWarn);
this._isPlaying = true;
this.index = (start is String) ? _getFrameByLabel(start) : start;
this.loop = loop;
this._actionName = name;
_isReverse = wrapMode == WRAP_REVERSE;
if (this._frames && this.interval > 0) {
timerLoop(this.interval, this, _frameLoop, null, true, true);
}
}
start参数支持帧标签来控制播放。
loop参数默认是true,即循环播放
关于name参数,注释是这样写的:动画模板在动画模版缓存池中的key,也可认为是动画名称。如果name为空,则播放当前动画序列帧;如果不为空,则在动画模版缓存池中寻找key值为name的动画模版,如果存在则用此动画模版初始化当前序列帧并播放,如果不存在,则仍然播放当前动画序列帧;如果没有当前动画的帧数据,则不播放,但该实例仍然处于播放状态。
//创建一个Animation实例
var tl:Animation = new Animation();
//加载动画文件
tl.loadAnimation("TimeLine.ani");
//添加到舞台
Laya.stage.addChild(tl);
//播放Animation动画
tl.play();
//创建一个新的Animation实例
var tl2:Animation = new Animation();
//加载动画文件
tl2.loadAnimation("TimeLine.ani");
//添加到舞台
Laya.stage.addChild(tl2);
//播放Animation动画的pivot动画
tl2.play(0, true, "pivot");
//动画的显示位置
tl2.pos(300,0);
上面的代码,就是IDE制作的ani中,有两个动画,可以通过name参数去选择播放,缺省播放第一个动画。
7.在loadAnimation方式中,牵涉到_loadAnimationData方法,进而使用了_parseGraphicAnimation方法。这就牵涉到GraphicAnimation类了。
三、帧动画的数据解析
下面这三个类,针对IDE制作的动画及动效模板进行解析。比较复杂,暂时不做阅读。
1.FrameAnimation extends AnimationPlayerBase
关键帧动画播放类
2.EffectAnimation extends FrameAnimation
动效模板。用于为指定目标对象添加动画效果。每个动效有唯一的目标对象,而同一个对象可以添加多个动效。 当一个动效开始播放时,其他动效会自动停止播放
3.GraphicAnimation extend FrameAnimation
就是IDE创建的时间轴动画
四、IDE动画编辑器使用
参考时间轴动画编辑器详解,介绍很详细,以下记录自己的使用经验
1.选中左侧属性,可以设置相应的缓动函数类型和标签
当设置标签后,设置标签的帧会出现红色圆点。可以在项目中,通过标签名用代码对该帧进行操作。
2.多个节点,也就是分层动画,比如上图中的GraphicNode2,Graphic3
注意官方示例中所说,拖拽一个新的组件到场景。即会自动新增一个节点层,刚开始我很困惑怎么增加新节点层。
另外,注意,时间轴动画的负坐标区域内,无法触发点击事件,如果需要用到点击事件交互,则动画的X与Y必须位于正坐标区域,也就是十字红线交叉的右下区域。
3.设置关键帧坐标时TIPS
如果要改坐标,先改后面的。如果从前往后改,后面的坐标动了,前一个会自动变成中间值 。
4.我想做一个光斑绕扇形移动的动画,如下图,圆弧段可以逐帧移动,而两个直线区域则可以使用补间动画,补间动画起始点和结束点,相隔的帧数,可以大概估算一下,保证每帧的移动速度和圆弧段每帧移动速度差不多就行。
这里有个方便的技巧就是,先建一个测试动画,把光斑都放到同一帧,否则无法直观看到全部的运动轨迹。当然,把背景图片也放进去,位置更精确。然后再建正式动画,每一帧的位置就可以参考测试动画里的坐标了。
5.Animation中是可以使用遮罩的
如图,这个动画显示的不是光斑,而是光条。
方法就是,在Animation里拖一个Sprite,设置renderType为mask。然后在里面用Lines画出轨迹,同时设置线宽。当然越宽,光条露出来的就越粗了。
五、动效模板
参考创建动效模板(EffectAnimation)
EffectAnimation extends FrameAnimation
动效模板。用于为指定目标对象添加动画效果。每个动效有唯一的目标对象,而同一个对象可以添加多个动效。 当一个动效开始播放时,其他动效会自动停止播放
动效模板是基于时间轴的动画效果,通过预设动画效果,然后把效果附加给某个组件。使得组件无需编码,却轻松实现与编码相同的动画效果。动效模板不能独立显示,仅可作为动效模板让UI页面中的组件获得动画效果。
1.按照上述示例,发现绑定一个组件后,我的组件坐标会改变为动效模板的坐标
参考关于动效模板使用后改变了原来组件的坐标问题
2.希望某个动效完成后,做一些事情
参考监听动效动画的COMPLETE(播放完成)事件
事件目前只有这几个,暂时不支持自定义事件。
3.参考动画模板中,要求到拖入一张图,然后制作一个动画,这张图是随便都可以的吗
资源目录中png和jpg格式的都可以