作者:岑彧
背景
Lottie 是一个由 Airbnb 开源的横跨 Android,iOS,Web 等多端的一个动画计划,它以 JSON 的形式解决了开发者对简单动画实现的开发成本问题。
家喻户晓,闲鱼团队是比拟早在客户端侧抉择 Flutter 计划的技术团队, 以后的闲鱼工程里也蕴含很多的 Flutter 界面。而官网却始终没有提供 Lottie-Flutter 计划,以后也有一些第三方开发者提供了相干实现计划,基本上分为两种:
- 在 Native 端进行数据解析和渲染,再应用桥接的形式把渲染数据传输到 Flutter 端进行显示;
- 在 Flutter 间接进行数据解析和应用 Flutter 绘图能力进行渲染显示。
不过以后曾经开源的计划都存在一些问题,前者会在性能和显示存在一些问题,例如显示闪动白屏。后者在一些能力反对上存在一些性能缺点,例如不反对文本动画等。所以这始终是闲鱼团队乃至整个 Flutter 开发者个人的一个痛点。
我的项目架构
闲鱼团队在调研了官网开源的 lottie-android 库之后,发现不论是数据解析能力,还是图形绘制能力。Flutter 都提供了媲美 Android 的实现计划。所以参考 lottie-android 库实现了一个性能齐备,性能优异的纯 Dart Package 来提供 Flutter 上的 Lottie 动画反对。
如上图所示,整个我的项目由根底模块,接口层和控件层形成,而后反对矢量图形,填充描边等能力,详情可见 Lottie 反对能力,反对的能力也和 lottie-android 大致相同。
根底模块
根底模块是与 FlutterSDK 提供的各种能力间接交互的中央,次要分为数据模型模块,动画绘制模块,数据解析模块和工具模块。首先对于整个框架来说,咱们首先能够拿到蕴含整个动画信息的 JSON 文件,所以须要先通过咱们的数据解析模块,把 JSON 文件外面蕴含的数据和信息解析并传递给数据模型模块,动画绘制模块负责拿到数据模型模块里的对象之后,调用 Flutter 提供的绘图能力来进行图形的绘制,而工具模块就次要负责获取屏幕信息,字符串解决,日志打印等工具类能力。
接口层
接口层次要负责 JSON 数据的输出和动画绘制管制和调用,JSON 信息通过数据解析模块最终会生成一个 LottieComposition 对象,这个对象里承载着整个 JSON 的动画信息。而后将这个对象传递给 LottieDrawable,而后 LottieDrawable 会把对象传递传递给动画绘制模块,这样动画绘制模块就能够拿到动画信息,而后 LottieDrawable 再调用动画绘制模块来进行动画的绘制和刷新。
组件层
组件层,这里次要是咱们继承 Flutter 的 Widget 实现的自定义组件,也是框架裸露给开发者的接口。开发者只须要新建一个 LottieAnimationView,并把 JSON 文件的门路传递给它,反对 Asset,Url,File 三种模式, 而后再把 LottieAnimationView 像一个一般 Widget 放到 FlutterUI 里,就能够实现一个简略的 Lottie 动画播放器了,当然也会裸露动画的管制接口以及控件的布局接口,只须要在新建 LottieAnimationView 的时候传入 AnimationController,width,height,alignment 等属性就能够实现对动画的进一步定制。
工作流程
整体思路
设计师在应用 AE 制作一段动画时,这个动画其实是由不同的图层组成的,AE 提供了多个图层供设计师抉择,例如纯色层(通常当做背景)、形态层(绘制各种矢量图形)、文本层、图片层等,每一个图层都能够设置平移、旋转、放缩等变换。每个图层可能又蕴含多个元素,例如形态图层可能由多个根本矢量图形和钢笔门路图形组合成为一个具备设计感的图案,每个元素也可能蕴含本人的变换,除了根底变换之外,还能够设置色彩、形态这样的变换。以上图层和元素的动画就组成了一个残缺的动画。
如上图所示,咱们在 AE 中新建了一个纯色图层并填充上蓝色,而后新建了一个形态图层,并给这个形态图层增加了一个位移动画(即给形态图层 1 变换中的地位设置两个关键帧,并在关键帧上设置初始值和最终值),而后在形态图层中增加一个矩形门路和一个黄色的填充,而后同样的办法给矩形的大小和圆度设置动画,不过大小的关键帧为 0 秒到 3 秒,圆度的关键帧为 3 秒到 5 秒。所以就实现了一个矩形从左到右的同时,先变大而后变为圆形的动画。而后咱们通过 Lottie 提供的 BodyMovin 插件将以上的动画导出为 JSON 格局的文件,这个 JSON 文件里就蕴含了刚刚咱们的所有绘制和关键帧信息。
如上图所示,拿到这个 JSON 文件之后,咱们首先通过了数据解析把设计师在 AE 中制作的各种图层信息和动画信息都解析传递给一个 LottieComposition 对象,而后 LottieDrawable 获取到这个 LottieComposition 对象并调用底层的 Canvas 来进行图形的绘制,通过 AnimationBuilder 来进行进度的管制,进度发生变化时告诉 Drawable 进行重绘,绘制模块会获取到处于该进度时的各项属性值,而后就实现了动画的播放。
数据加载和显示
咱们的组件层提供三种形式来进行 JSON 文件的获取,别离为 asset(程序内置资源),url(网络资源),file(文件资源)。整个数据的加载和显示的流程图大抵如下所示,省略了底层绘制的细节:
这里以 fromAsset 形式举例,其余两种的加载形式和这种雷同,都对立由 LottieCompositionFactory 进行解决。这里咱们依据构造函数的不同将将加载形式分为三种,即 asset,file 和 url。而后依据类型的不同调用 LottieCompositionFactory 里的不同加载办法将对应的内置资源、网络资源和文件资源加载进来并进行 JSON 文件的解析,而后最终的产物是一个 LottieComposition 对象,这个对象通过异步加载解析,在解析实现之后会告诉 LottieAnimationView 进行调用。咱们将加载实现的 LottieComposition 对象传递给咱们的绘制类,LottieDrawable 会依据 composition 里的内容建设图层组,图层组里蕴含如形态,文本层等图层,和设计师在 AE 制作动画时创立的图层一一对应。每个图层有不同的绘制规定和办法,而后在 LottieAnimationView 里获取到零碎的 Canvas 传递给 LottieDrawable 并调用 draw 办法。这样就能够应用零碎画布绘制咱们本人的动画内容了。
动画绘制与播放
实现了动画的加载与显示,咱们还须要让画面动起来。咱们通过 AnimationBuilder 的形式将 AnimationController 的 value 设置为 LottieDrawable 的 progress,而后触发重绘使咱们的底层通过 progress 去获取以后进度的各项动画属性,这样就能够实现动画的成果了。时序图大抵如下所示:
咱们在 LottieAnimationView 里通过 Flutter 内置的 AnimationController 来管制动画,其中 forward 办法能够让 Animation 的 progress 从零开始减少,这也是咱们动画播放的开始。咱们一直调用 setProgress 函数将动画的进度设置到各层,最终达到 KeyframeAnimation 层,更新以后进度。进度扭转之后咱们须要告诉下层进行界面的重绘,最终将 LottieDrawable 里的一个 isDirty 的变量设为 true。咱们在 setProgress 函数里,在实现进度设置之后咱们获取 lottieDrawable 的 isDirty 变量,如果这个变量为 true,证实进度曾经更新,此时咱们调用重写的办法 markNeedPaint(),这时候零碎会标记以后组件为须要更新的组件,Flutter 会调用咱们重写的 paint 函数,对整个画面进行重绘。咱们和显示的流程一样,一层层进行绘制,在底层咱们会依据以后进度拿到 KeyframeAnimation 中对应的属性值,而后绘制进去的画面就会产生变动。通过这样一直的更新进度,而后从新获取以后进度对应的属性进行重绘,这样就能够实现动画的播放成果。
实现差别
组件层
Android 端
对于 lottie-android 来说,AnimationView 和 Drawable 组成了整个组件层。AnimationView 继承于 ImageView,LottieDrawable 继承于 Drawable。整个工作的流程和下面所说的基本相同,开发者在 xml 文件中写入 LottieAnimationView 并设置 JSON 文件资源门路。而后 AnimationView 会发动数据获取和解析,解析实现之后把 Composition 对象传递给 LottieDrawable,而后调用重写的 draw 办法来进行动画展现。
而后整个动画的播放,暂停,进度等管制都是通过开发者在代码中获取 AnimationView 的援用而后调用各种办法来实现的,然而其实真正的动画管制是由 LottieDrawable 里的 ValueAnimator 来管制的。在初始化 LottieDrawable 的同时也会创立 ValueAnimator,它会产生一个 0~1 的插值,依据不同的插值来设置以后动画进度。LottieAnimationView 里的暂停,播放等动画管制办法其实就是调用了这个 ValueAnimator 本身的对应办法来实现动画的管制。
Flutter 端
对于 Flutter 来说,并没有提供相似于 ImageView 和 Drawable 这样的组件让咱们继承和重写,咱们须要自定义一个 Widget,自定义组件个别有三种形式:
- 原生组件的组合
此处咱们显然不能应用这个办法,因为咱们须要获取零碎提供的画布来进行绘制。
- 实现 CustomPainter
在 Flutter 中,提供了一个自绘 UI 的接口 CustomPainter,这个接口会提供一块 2D 画布 Canvas,Canvas 外部封装了一些根本绘制的 API,开发者能够通过 Canvas 绘制各种自定义图形。咱们能够在重写的 paint 办法中获取到零碎的 canvas,而后把这个 canvas 传递给咱们的 LottieDrawable 就能够实现动画的绘制了,而后在属性变动时导致画面须要刷新时在 shouldRepaint 返回 true。然而这个计划会有一些问题无奈解决,咱们都晓得整个 LottieAnimationView 是作为一个 Widget 嵌入到 FlutterUI 当中的,咱们往往须要自定义动画播放区域(即 LottieAnimationView)的大小,然而当开发者没有设定这个宽高值的时候或者是设定的尺寸大于父布局的尺寸的时候,咱们也要依据父布局对子布局的束缚来进行尺寸的适配和转换。然而在 Flutter 提供的这个 CustomPainter 中,没有裸露相应的接口让咱们获取到这个 Widget 所对应的 RenderObject 的 constraint 属性,也就无奈在开发者没有设置 LottieAnimationView 本身的 width 和 height 时依据父布局的束缚进行尺寸适配,所以放弃了这个实现计划。
- 自定义 RenderObject
咱们都晓得 Flutter 中的 Widget 只是一些轻量的款式配置信息,真正进行图形渲染的类是 RenderObject。所以咱们天然也能够重写这个 RenderObject 类中的 paint 办法来获取零碎画布来进行绘制。这个计划会比上一个计划简单一些,咱们须要先定义一个继承于 RenderBox 的 RenderLottie 类,而后重写 paint 办法来把零碎的 canvas 传递给 LottieDrawable,在须要进行刷新的中央调用 markNeedPaint 办法,就能够实现界面重绘。而后对于 RenderObject 来说,咱们能够获取到以后组件的 constraint 属性,也就是在开发者没有设置 LottieAnimationView 的尺寸或者是设置的尺寸超出复布局的时候咱们也能够自适应父布局的尺寸了。接下来须要定义一个继承于 LeafRenderObjectWidget 的组件 LeafRenderLottie 并重写 createRenderObject 办法并返回 RenderLottie 对象,重写 updateRenderObject 办法更新 RenderLottie 的进度等各项属性。这就实现了一个 LottieWidget 的实现。那咱们如何来进行动画的播放管制呢,咱们的 LottieAnimationView 是作为一个 Widget 嵌入到 FlutterUI 当中的,个别不会去获取它的援用来调用办法,那咱们就传入一个 Flutter 提供的 AnimationController,而后在 LottieAnimationView 的 build 办法中返回一个 AnimationBuilder 并把 AnimationController 的进度值传给 LeafRenderLottie,如果开发者没有传入 AnimationController,咱们就提供一个默认的 controller 来进行简略的动画播放就能够了。要害代码如下所示:
@override
void paint(PaintingContext context, Offset offset) {if (_drawable == null) return;
_drawable.draw(context.canvas, offset & size,
fit: _fit, alignment: _alignment);
}
//RenderLottie 的 paint 办法
文本绘制
Android 端
Android SDK 里的 Canvas 提供了 drawText 的办法,能够应用画布间接绘制文本。Android 实现计划如下:
private void drawCharacter(String character, Paint paint, Canvas canvas) {if (paint.getColor() == Color.TRANSPARENT) {return;}
if (paint.getStyle() == Paint.Style.STROKE && paint.getStrokeWidth() == 0) {return;}
canvas.drawText(character, 0, character.length(), 0, 0, paint);
}
Flutter 端
然而在 Flutter 的 Canvas 里却没有这种办法,通过调研之后咱们发现 Flutter 提供了一个专门的 TextPainter 来进行文本的绘制。Flutter 实现计划如下:
void _drawCharacter(String character, TextStyle textStyle, Paint paint, Canvas canvas) {if (paint.color.alpha == 0) {return;}
if (paint.style == PaintingStyle.stroke && paint.strokeWidth == 0) {return;}
if (paint.style == PaintingStyle.fill) {textStyle = textStyle.copyWith(foreground: paint);
} else if (paint.style == PaintingStyle.stroke) {textStyle = textStyle.copyWith(background: paint);
}
var painter = TextPainter(text: TextSpan(text: character, style: textStyle),
textDirection: _textDirection,
);
painter.layout();
painter.paint(canvas, Offset(0, -textStyle.fontSize));
}
贝塞尔曲线
Android 端
咱们在背景中提到过,贝塞尔曲线是组成动画的三元素之一。咱们的动画往往不是线性播放的,如果须要实现先快后慢这样的成果。咱们就须要在通过进度获取属性值的时候,应用贝塞尔曲线能力进行从进度到属性值的映射。Android SDK 里提供了 PathInterpolator 来实现,咱们的 JSON 文件里应用两个控制点来形容贝塞尔曲线,咱们将这两个控制点的坐标传给 PathInterpolator,而后在属性值获取的时候,调用插值器的 getInterpolation 就能够拿到映射后的值了。以下是要害办法实现:
interpolator = PathInterpolatorCompat.create(cp1.x, cp1.y, cp2.x, cp2.y);
public static Interpolator create(float controlX1, float controlY1,
float controlX2, float controlY2) {if (Build.VERSION.SDK_INT >= 21) {return new PathInterpolator(controlX1, controlY1, controlX2, controlY2);
}
return new PathInterpolatorApi14(controlX1, controlY1, controlX2, controlY2);
}
public PathInterpolator(float controlX1, float controlY1, float controlX2, float
controlY2) {initCubic(controlX1, controlY1, controlX2, controlY2);
}
private void initCubic(float x1, float y1, float x2, float y2) {Path path = new Path();
path.moveTo(0, 0);
path.cubicTo(x1, y1, x2, y2, 1f, 1f);
initPath(path);
}
//Andorid 内置贝塞尔曲线生成要害办法
Flutter 端
而 Flutter 里没有提供这样现成的门路插值器,咱们只有依据源码来自行实现。查看 Android 相干源码之后,我发现咱们只须要将 JSON 里两个控制点的坐标传入 Flutter path 中的 cubicTo 办法就能够生成该贝塞尔曲线,而后再自行实现一个入参为工夫 t,后果为映射后进度 p 的办法就能够,而具体的实现参考 PathInterpolator 中的 getInterpolation 就能够实现。以下是要害办法实现:
interpolator = PathInterpolator.cubic(cp1.dx, cp1.dy, cp2.dx, cp2.dy);
factory PathInterpolator.cubic(double controlX1, double controlY1, double controlX2, double controlY2) {
return PathInterpolator(_initCubic(controlX1, controlY1, controlX2, controlY2));
}
static Path _initCubic(double controlX1, double controlY1, double controlX2, double controlY2) {final path = Path();
path.moveTo(0.0, 0.0);
path.cubicTo(controlX1, controlY1, controlX2, controlY2, 1.0, 1.0);
return path;
}
自定义 Flutter 贝塞尔曲线生成要害办法
成果比照
咱们以后曾经应用 fish-lottie 实现了一个闭环 Demo 工程,在外面也同样选取了 lottie-android 工程里的 lottie json 文件来进行测试,发现在 release 包无论是从晦涩度,还是动画还原度上,都达到了官网示例 App 的水准,上面我会用一些动图来比照进行阐明:
上述中,前者是应用 fish-lottie 在 flutter 页面播放的动画,后者是 lottie-android 在 native 页面播放的动画,不难看出 fish-lottie 无论是从渲染还是播放,都能够达到和 lottie-android 媲美的水平。
上述中,前者是应用 fish-lottie 的动静文本动画,后者是 lottie-android 的动静文本动画,能够看出 fish-lottie 在动静的属性和文本实时渲染方面也能够提供不输于 lottie-android 的成果。而且因为咱们的文本绘制实现计划与原生有肯定的差别,咱们能够更好的将字体款式接口裸露进去,让开发者不止能够对文本进行定制,在款式方面也能够进行实时动静定制,这是目前 lottie-android 没有提供的性能。
将来瞻望——从动态到交互
以后 Lottie 的应用场景都仅仅是一段动画的动态播放。例如点赞之后会呈现大拇指的动画,珍藏之后会呈现心形的动画,最多通过进度来管制一些整个动画的播放。然而在实现整个框架的过程中,我发现 lottie-android 其实曾经具备一些可交互的能力,应用办法如下:
val shirt = KeyPath("Shirt", "Group 5", "Fill 1")
animationView.addValueCallback(shirt, LottieProperty.COLOR) {Colors.XXX} // 需定制的色彩
以上代码实现的成果如下图所示:
lottie-android 实现计划
从以上的代码咱们能够看出,要想实现动静属性管制,咱们须要传入三个参数,第一个参数相似于一个定位符,须要通过门路的模式来定位到咱们想进行属性管制的矢量图形内容,第二个参数是一个属性枚举变量,它表明了咱们管制的属性类型,最初一个参数是一个回调函数,须要返回咱们动静扭转的目标值。
因为下层组件层和 lottie-android 有比拟大的差别,所以 fish-lottie 以后只实现了动画播放的能力反对,可交互能力正在开发当中。
fish-lottie 实现思路
因为下层组件的双端实现的差异性和 UI 构建个性,Flutter 中咱们个别不会获取 Widget 的援用来调用它的办法。所以不能像 lottie-android 一样间接应用 lottieAnimationView.addValueCallback() 来进行动静属性管制,咱们在实现动画的进度管制的时候其实也遇到过一样的问题。所以咱们的实现思路这其实和 AnimationCtroller 一样,咱们也实现一个 PropertiesController(属性控制器),把咱们须要批改的一系列的指标图形,指标属性和回调函数传递给这个控制器,再把这个控制器作为 LottieAnimationView 构造函数的一个参数传递给 LottieDrawable,而后由这个属性控制器来发动指标图形绘制类的匹配和回调函数设置。底层的绘制类和帧动画类中的办法和 lottie-android 保持一致。根本的思路和 lottie-android 保持一致,只是 LottieAnimationView 不再承当属性管制的责任,而是由 PropertiesController 来承当。
落地方向
有了交互能力,咱们不再只能管制动画的播放了。咱们能够通过获取用户的点击触摸事件来进行动画上的反馈,以此来实现一些比较复杂的交互动画。
如上图所示,这个搜寻框背景的动画成果如果开发者间接进行开发是很难实现的。而通过 lottie 咱们就有比拟清晰的思路,制作一个流动的果冻背景动画,两个内容动画,一个黑夜星月动画,一个白天云彩动画,咱们能够通过点击事件来管制果冻背景动画背景在彩色和蓝紫渐变色之间进行切换,以及扭转一下它的部分形态,还有两个内容动画的显示和暗藏。在点击第一个 Pillow 按钮时把果冻背景动画色彩切换为蓝紫渐变色,而后显示云彩动画。点击第二个 Baby 按钮时把果冻背景动画的背景色切换为彩色,而后显示星月动画。而后对于云彩动画的 3D 成果,咱们能够通过手机设施的陀螺仪传感器来获取手机的侧偏移角度,而后依据角度来扭转云彩动画各个元素的地位。这样之前开发成本过高甚至无奈实现的简单交互动画成果,就能够通过 lottie 很轻松的实现进去了。
- 《【AliFlutter】Flutter Fish Redux 2.0 架构演进实际》
关注咱们,每周 3 篇挪动干货 & 实际给你思考!