这样做动画交互,一点都不费力!

47次阅读

共计 8923 个字符,预计需要花费 23 分钟才能阅读完成。

本文由云 + 社区发表作者:paulzeng
导语:Lottie 是 Airbnb 开源的一个面向 iOS、Android、React Native 的动画库,可实现非常复杂的动画,使用也及其简单,极大释放人力,值得一试。

一、简介
Lottie 是 Airbnb 开源的一个面向 iOS、Android、React Native 的动画库,能分析 Adobe After Effects 导出的动画,并且能让原生 App 像使用静态素材一样使用这些动画,完美实现动画效果。
现在使用各平台的 native 代码实现一套复杂的动画是一件很困难并且耗时的事,我们需要为不同尺寸的屏幕加载不同的素材资源,还需要写大量难维护的代码,而 Lottie 可以做到同一个动画文件在不同平台上实现相同的效果,极大减少开发时间,实现不同的动画,只需要设置不同的动画文件即可,极大减少开发和维护成本。
官方效果图:

二、如何使用
Lottie 支持多平台,使用同一个 JSON 动画文件,可在不同平台实现相同的效果。
Android 通过 Airbnb 的开源项目 lottie-android 实现,最低支持 API 16;
IOS 通过 Airbnb 的开源项目 lottie-ios 实现,最低支持 IOS 7;
React Native,通过 Airbnb 的开源项目 lottie-react-native 实现;

这是 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(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 实现思路和源码分析 – 简书
4.Most Popular – LottieFiles
此文已由作者授权腾讯云 + 社区在各渠道发布
获取更多新鲜技术干货,可以关注我们腾讯云技术社区 - 云加社区官方号及知乎机构号

正文完
 0