作者:岑彧

背景

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 篇挪动干货&实际给你思考!