这样做动画交互,一点都不费力!
本文由云+社区发表
作者:paulzeng
导语:lottie是airbnb开源的一个面向 ios、android、react native 的动画库,可实现非常复杂的动画,使用也及其简单,极大释放人力,值得一试。
一、简介
lottie 是airbnb开源的一个面向 ios、android、react native 的动画库,能分析 adobe after effects 导出的动画,并且能让原生 app 像使用静态素材一样使用这些动画,完美实现动画效果。
现在使用各平台的 native 代码实现一套复杂的动画是一件很困难并且耗时的事,我们需要为不同尺寸的屏幕加载不同的素材资源,还需要写大量难维护的代码,而lottie可以做到同一个动画文件在不同平台上实现相同的效果,极大减少开发时间,实现不同的动画,只需要设置不同的动画文件即可,极大减少开发和维护成本。
官方效果图:
二、如何使用
lottie支持多平台,使用同一个json动画文件,可在不同平台实现相同的效果。
android 通过airbnb的开源项目实现,最低支持 api 16;
ios 通过airbnb的开源项目实现,最低支持 ios 7;
react native,通过airbnb的开源项目实现;
这是react logo的动画,以下以android平台为例如何使用lottie
1.下载lottie
在项目的 build.gradle 文件添加依赖
dependencies { compile 'com.airbnb.android:lottie:2.1.0' }
2.添加 adobe after effects 导出的动画文件
lottie默认读取assets中的文件,我们需要把动画文件react.json 保存在app/src/main/assets文件里。(文件比较大,只展示了部分内容,文件链接)
{ "v": "4.6.0", "fr": 29.9700012207031, "ip": 0, "op": 141.000005743048, "w": 800, "h": 800, "ddd": 0, "assets": [ ], "layers": [ { "ddd": 0, "ind": 0, "ty": 4, "nm": "center_circle", "ks": {...}, "ao": 0, "shapes": [...], "ip": 0, "op": 900.000036657751, "st": 0, "bm": 0, "sr": 1 }, {...}, {...}, {...} ] }
3.使用lottie
在布局文件中直接添加lottie的lottieanimationview控件,即可在界面显示react logo动画效果
<com.airbnb.lottie.lottieanimationview android:id="@+id/animation_view" android:layout_width="wrap_content" android:layout_height="wrap_content" app:lottie_filename="react.json" app:lottie_loop="true" app:lottie_autoplay="true" />
4.引入lottie影响
(1)兼容性
lottie 最低支持版本api 16,低版本系统需要做降级动画或者不展示动画
(2)安装包
影响项 | 使用前 | 使用后 | 结论 |
---|---|---|---|
方法数 | 144807 | 145891 | 增加1084个方法 |
安装包大小 | 41969kb | 42037kb | 增大68kb |
这是用全民k歌release包的测试数据,lottie本身方法数不小,有方法数超标和安装包过大的风险,业务可自行评估
注:lottieanimationview继承于v7的appcompatimageview,需要引入v7兼容包,根据业务需要,可以源码引入lottie,让lottieanimationview继承与imageview,就不用引入v7兼容包,可减小安装大小。
三、使用小技巧
1.加载sdcard动画文件
stringbuilder stringbuilder = new stringbuilder(); bufferedreader bufferedreader = new bufferedreader(new filereader(new file(json_path + "react.json"))); string content = null; while ((content = bufferedreader.readline()) != null){ stringbuilder.append(content); } jsonobject jsonobject = new jsonobject(stringbuilder.tostring()); animationview.setanimation(jsonobject); animationview.loop(true); animationview.playanimation();
2.加载sdcard图片
animationview.setimageassetdelegate(new imageassetdelegate() { @override public bitmap fetchbitmap(lottieimageasset asset) { try { fileinputstream fileinputstream = new fileinputstream(image_path + asset.getfilename()); return bitmapfactory.decodestream(fileinputstream); ///把流转化为bitmap图片 } catch (exception e) { log.e(tag, "", e); } return null; } });
3.加载sdcard字体
animationview.setfontassetdelegate(new fontassetdelegate(){ public typeface fetchfont(string fontfamily) { typeface customfont = typeface.createfromfile(font_path + fontfamily); return customfont; } });
4.缓存动画
/* * lottie内部有两个缓存map(强引用缓存,弱引用缓存),在动画文件加载完成后会根据设置的缓存策略缓存动画,方便下次使用。 */ animationview.setanimation(animation, lottieanimationview.cachestrategy.strong); //强缓存 animationview.setanimation(animation, lottieanimationview.cachestrategy.weak); //弱缓存
四、lottie实现原理
设计师把一张复杂的图片使用多个图层来表示,每个图层展示一部分内容,图层中的内容也可以拆分为多个元素。拆分元素之后,根据动画需求,可以单独对图层或者图层中的元素做平移、旋转、收缩等动画。
lottie的使用的资源是需要先通过( bodymovin 插件本身是用于网页上呈现各种ae效果的一个开源库)将 adobe after effects (ae)生成的aep动画工程文件转换为通用的json格式描述文件。lottie则负责解析动画的数据,计算每个动画在某个时间点的状态,准确地绘制到屏幕上。
导出的json动画描述文件:
{ "v": "4.6.0", "fr": 29.9700012207031, "ip": 0, "op": 141.000005743048, "w": 800, "h": 800, "ddd": 0, "assets": [ ], "layers": [ {...}, ] }
lottie主要类图:
图:lottie_class
lottie对外通过控件lottieanimationview暴露接口,控制动画。
lottieanimationview继承自imageview,通过当前时间绘制canvas显示到界面上。这里有两个关键类:lottiecomposition 负责解析json描述文件,把json内容转成java数据对象;lottiedrawable负责绘制,把lottiecomposition转成的数据对象绘制成drawable显示到view上。顺序如下:
1.json文件解析
lottiecomposition负责解析json文件,建立数据到java对象的映射关系。
(1)解析json外部结构
lottiecomposition封装整个动画的信息,包括动画大小,动画时长,帧率,用到的图片,字体,图层等等。
json外部结构
{ "v": "4.6.0", //bodymovin的版本 "fr": 29.9700012207031, //帧率 "ip": 0, //起始关键帧 "op": 141.000005743048, //结束关键帧 "w": 800, //动画宽度 "h": 800, //动画高度 "ddd": 0, "assets": [...] //资源信息 "layers": [...] //图层信息 } //解析json的源码 static lottiecomposition fromjsonsync(resources res, jsonobject json) { rect bounds = null; float scale = res.getdisplaymetrics().density; int width = json.optint("w", -1); int height = json.optint("h", -1); if (width != -1 && height != -1) { int scaledwidth = (int) (width * scale); int scaledheight = (int) (height * scale); bounds = new rect(0, 0, scaledwidth, scaledheight); } long startframe = json.optlong("ip", 0); long endframe = json.optlong("op", 0); float framerate = (float) json.optdouble("fr", 0); string version = json.optstring("v"); string[] versions = version.split("[.]"); int major = integer.parseint(versions[0]); int minor = integer.parseint(versions[1]); int patch = integer.parseint(versions[2]); lottiecomposition composition = new lottiecomposition( bounds, startframe, endframe, framerate, scale, major, minor, patch); jsonarray assetsjson = json.optjsonarray("assets"); parseimages(assetsjson, composition); //解析图片 parseprecomps(assetsjson, composition); parsefonts(json.optjsonobject("fonts"), composition); //解析字体 parsechars(json.optjsonarray("chars"), composition); //解析字符 parselayers(json, composition); //解析图层 return composition; }
(2)解析图片资源
lottieimageasset类封装图片信息 "assets": [ //资源信息 { //第一张图片 "id": "image_0", //图片id "w": 58, //图片宽度 "h": 31, //图片高度 "u": "images/", //图片路径 "p": "img_0.png" //图片名称 }, {...} //第n张图片 ] static lottieimageasset newinstance(jsonobject imagejson) { return new lottieimageasset(imagejson.optint("w"), imagejson.optint("h"), imagejson.optstring("id"), imagejson.optstring("p")); }
(3)解析图层
layer封装图层信息,现在lottie只支持precomp,solid,image,null,shape,text这6中图层。
"layers": [ //图层信息 { //第一层动画 "ddd": 0, "ind": 0, //layer id 图层 id "ty": 4, //图层类型 "nm": "center_circle", "ks": {...}, //动画 "ao": 0, "shapes": [...], "ip": 0, //inframe 该图层起始关键帧 "op": 90, //outframe 该图层结束关键帧 "st": 0, //startframe 开始 "bm": 0, "sr": 1 }, {...} //第n层动画 ]
2.如何动起来
lottie时序图:
利用属性动画控制进度,每次进度改变通知到每一层,触发lottieanimationview重绘。
(1)利用属性动画计算进度
这里用到了属性动画来产生一个0~1的插值,根据不同的插值来设置当前动画进度。
代码如下:
public lottiedrawable() { animator.setrepeatcount(0); animator.setinterpolator(new linearinterpolator()); animator.addupdatelistener(new valueanimator.animatorupdatelistener() { @override public void onanimationupdate(valueanimator animation) { if (systemanimationsaredisabled) { animator.cancel(); setprogress(1f); } else { setprogress((float) animation.getanimatedvalue()); } } }); }
(2)通过compositionlayer把进度传递到各个图层
@override public void setprogress(@floatrange(from = 0f, to = 1f) float progress) { super.setprogress(progress); if (timeremapping != null) { long duration = lottiedrawable.getcomposition().getduration(); long remappedtime = (long) (timeremapping.getvalue() * 1000); progress = remappedtime / (float) duration; } if (layermodel.gettimestretch() != 0) { progress /= layermodel.gettimestretch(); } progress -= layermodel.getstartprogress(); for (int i = layers.size() - 1; i >= 0; i--) { layers.get(i).setprogress(progress); } }
(3)通知进度改变
void setprogress(@floatrange(from = 0f, to = 1f) float progress) { if (progress < getstartdelayprogress()) { progress = 0f; } else if (progress > getendprogress()) { progress = 1f; } if (progress == this.progress) { return; } this.progress = progress; for (int i = 0; i < listeners.size(); i++) { listeners.get(i).onvaluechanged(); } }
(4)最终回调到lottieanimationview的invalidatedrawable
@override public void invalidatedrawable(@nonnull drawable dr) { if (getdrawable() == lottiedrawable) { // we always want to invalidate the root drawable so it redraws the whole drawable. // eventually it would be great to be able to invalidate just the changed region. super.invalidatedrawable(lottiedrawable); } else { // otherwise work as regular imageview super.invalidatedrawable(dr); } }
(5)最后触发lottiedrawable重绘
@override public void draw(@nonnull canvas canvas) { ... matrix.reset(); matrix.prescale(scale, scale); compositionlayer.draw(canvas, matrix, alpha); //这里会调用所有layer的绘制方法 if (hasextrascale) { canvas.restore(); } }
五、性能
1.官方说明
如果没有mask和mattes,那么性能和内存非常好,没有bitmap创建,大部分操作都是简单的cavas绘制。
如果存在mattes,将会创建2~3个bitmap。bitmap在动画加载到window时被创建,被window删除时回收。所以不宜在recyclerview中使用包涵mattes或者mask的动画,否则会引起bitmap抖动。除了内存抖动,mattes和mask中必要的bitmap.erasecolor()和canvas.drawbitmap()也会降低动画性能。对于简单的动画,在实际使用时性能不太明显。
如果在列表中使用动画,推荐使用缓存lottieanimationview.setanimation(string, cachestrategy) 。
2.属性动画和lottie动画对比
以下性能对比是以k歌内单个礼物动画效果
属性动画 | lottie使用硬件加速 | lottie未使用硬件加速 | |
---|---|---|---|
帧率 | |||
内容 | |||
cpu |
lottie动画在未开启硬件加速的情况下,帧率、内存,cpu都比属性动画差,开启硬件加速后,性能差不多。
3、未开启硬件加速,lottie动画大小帧率对比
0.12倍 | 1倍 |
---|---|
主要耗时在draw方法,绘制区域越小,耗时越小
六、k歌可用的场景
1.特性引导视频
全民k歌每个大版本的首次启动都会有视频引导动画,每次都会在清晰度和文件大小平衡,最终导出一个大概有3-4m的引导视频,使用lottie可提高动画清晰度和减小安装包大小
2.loading动画
3.礼物动画
这是全民k歌的礼物面板,内部有大量礼物动画,每个礼物动画都不相同,动画过程中有大量的旋转,透明度,大小的变化,需要用属性动画实现,非常麻烦,代码可维护性也比较差。对比使用lottie后,有几大优势:
1、100%实现设计效果
2、客户端代码量极少,易维护
3、每个动画可动态配置动画样式(加载不同的json)
4、所有动画都可动态配置,动画配置文件,素材都可从网上加载
4.说唱
k歌的说唱功能需要歌词按照特定的动画展示出来,这里就涉及歌词放大,缩小,旋转等等特效。实现时,根据当前时间,在canvas上歌词绘制出来,最终再和声音融合在一起生成一个mv视频,这里就导致动画不能特别复杂,并且有一定的规律。
如果使用lottie后,可以把效果导出到json动画文件里,客户端加载动画文件,循环设置进度,读取每帧画面,再和声音融合生成mv。
优势:
1.动画丰富
2.代码量少
3.可使用设计导出的字体
代码
animationview.setprogress(progress); //设置当前进度 animationview.builddrawingcache(); //强制缓存绘制数据 bitmap image = animationview.getdrawingcache(); //获取当前绘制数据
七、总结
1.劣势
(1)性能不够好—某些动画特效,内存和性能不够好;相对于属性动画,在展示大动画时,帧率较低
2.优势
(1)开发效率高—代码实现简单,更换动画方便,易于调试和维护。
(2)数据源多样性—可从assets,sdcard,网络加载动画资源,能做到不发版本,动态更新
(3)跨平台—设计稿导出一份动画描述文件,android,ios,react native通用
lottie使用简单,易于上手,非常值得尝试。
八、参考资料
1.github - airbnb/lottie-android: render after effects animations natively on android and ios
2.lottie的使用及原理浅析 - 彩笔学长 - csdn博客
3.从json文件到炫酷动画-lottie实现思路和源码分析 - 简书
此文已由作者授权腾讯云+社区在各渠道发布
获取更多新鲜技术干货,可以关注我们