Lottie源码简单分析以及使用

Github上的Lottie

LottieComposition

  public ArrayList<String> getWarnings() {
    return new ArrayList<>(Arrays.asList(warnings.toArray(new String[warnings.size()])));
  }

这个方法通过HashSet<String>转换为ArrayList来获取warnings保存的一些报错日志。其中Array做为其中的转换桥梁。

Layer layerModelForId(long id) {
    return layerMap.get(id);
  }

Layer主要是AE的图层概念转换,以Model的形式保存在layerMap。其中layerMap的数据来源于AE导出的json解析出来的layers数组。每个Layer具有对应的id,这个方法就是通过id找到对应的Layer。在LottieComposition这个类里面有四个数据集合,分为precomps、images、layerMap、layers。它们获取数据简单流程图如下

图1.png

其中

  1. fromAssetFileName(Context context, String fileName,OnCompositionLoadedListener loadedListener)
  2. fromFileSync(Context context, String fileName)
  3. fromJson(Resources res, JSONObject json,OnCompositionLoadedListener loadedListener)
  4. fromInputStream(Context context, InputStream stream,OnCompositionLoadedListener loadedListener)
  5. fromInputStream(Resources res, InputStream stream)
  6. fromJsonSync(Resources res, JSONObject json)

我们可以通过上面的任何一个方法设置要做动画的json文件

LottieComposition其他的一些属性,会在fromJsonSyncjson格式里面读取它们对应的初始值

  private final Rect bounds; //绘制动画的范围
  private final long startFrame;   // 开始帧数
  private final long endFrame; // 结束帧数
  private final int frameRate; //帧数率
  private final float dpScale; //缩放大小

LottieDrawable

自定义的一个Drawable,主要是管控动画的start、resume、pause、reverse等等,以及创建compositionLayer,为绘制动画的图层做准备,简单逻辑如下

图2.png

在我们使用的动画的fragment里面可以调用LottieAnimationViewresumeAnimation()、pasureAnimation()、reverseAnimation()等方法来控制动画的状态。然后然后通过LottieAnimationView分配到创建的LottieDrawable里面的对应的各个方法,然后对动画进行各种状态处理。LottieDrawable大概代码如下

private final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
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());
        }
      }
    });
  }
public void resumeAnimation() {
    playAnimation(true);
  }
private void playAnimation(boolean setStartTime) {
    ...
    long playTime = setStartTime ? (long) (progress * animator.getDuration()) : 0;
    animator.start();
    if (setStartTime) {
      animator.setCurrentPlayTime(playTime);
    }
  }
public void cancelAnimation() {
    ...
    animator.cancel();
  }
 private void reverseAnimation(boolean setStartTime) {
   ...
    if (setStartTime) {
      animator.setCurrentPlayTime((long) (progress * animator.getDuration()));
    }
    animator.reverse();
  }

同时LottieDrawable也会在LottieAnimationView调用的setComposition()方法里面创建一个CompositionLayer对象,通过这个对象实现动画图层的各种绘制。大概代码如下

public boolean setComposition(LottieComposition composition) {
    ...
    buildCompositionLayer();
    applyColorFilters();

    setProgress(progress);
    ...

    return true;
  }

  private void buildCompositionLayer() {
    compositionLayer = new CompositionLayer(
        this, Layer.Factory.newInstance(composition), composition.getLayers(), composition);
  }

然后动画的执行后,会在动画的AnimatorUpdateListener回调中调用compositionLayersetProgress()方法,从而开始执行动画。

Layer

通过上文setProgress()方法的跟踪,最终会发现在BaseKeyframeAnimation类里面可以找到AnimationListener接口的onValueChanged()的调用

  void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
    ...
    for (int i = 0; i < listeners.size(); i++) {
      listeners.get(i).onValueChanged();
    }
  }

BaseKeyframeAnimation.AnimationListener接口,可以在BaseLayer里面有实现

@Override public void onValueChanged() {
    invalidateSelf();
  }
 private void invalidateSelf() {
    lottieDrawable.invalidateSelf();
  }

通过invalidateSelf()方法的调用,LottieDrawabledraw()方法也得到不断的执行,从而驱使BaseLayerdraw()方法也得到执行,draw()方法如下

  @Override
  public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
   ...
    drawLayer(canvas, matrix, alpha);
   ...
  }

通过drawLayer()这个抽象方法,实现各种不同的Layer效果,主要以下几个Layer

图3.png

其中会在CompositionLayer构造函数里面初始化不同的Layer

  CompositionLayer(LottieDrawable lottieDrawable, Layer layerModel, List<Layer> layerModels,
      LottieComposition composition) {
    super(lottieDrawable, layerModel);
     ...
    for (int i = layerModels.size() - 1; i >= 0; i--) {
      Layer lm = layerModels.get(i);
      BaseLayer layer = BaseLayer.forModel(lm, lottieDrawable, composition);
      ...
     }
...
  }

BaseLayer.java

根据不同的LayerType绘制不同的图层

 @Nullable
  static BaseLayer forModel(
    Layer layerModel, LottieDrawable drawable, LottieComposition composition) {
    switch (layerModel.getLayerType()) {
      case Shape:
        return new ShapeLayer(drawable, layerModel);
      case PreComp:
        return new CompositionLayer(drawable, layerModel,
            composition.getPrecomps(layerModel.getRefId()), composition);
      case Solid:
        return new SolidLayer(drawable, layerModel);
      case Image:
        return new ImageLayer(drawable, layerModel, composition.getDpScale());
      case Null:
        return new NullLayer(drawable, layerModel);
      case Text:
      case Unknown:
      default:
        // Do nothing
        Log.w(L.TAG, "Unknown layer type " + layerModel.getLayerType());
        return null;
    }
  }

LayerTypeLayer里面赋值

static Layer newInstance(JSONObject json, LottieComposition composition) {
....
  LayerType layerType;
      int layerTypeInt = json.optInt("ty", -1);
      if (layerTypeInt < LayerType.Unknown.ordinal()) {
        layerType = LayerType.values()[layerTypeInt];
      } else {
        layerType = LayerType.Unknown;
      }
....
}

BaseLayer.java里面

 @Override public void onValueChanged() {
    invalidateSelf();
  }

通过实现接口BaseKeyframeAnimation.AnimationListener,不断重绘,实现动画
从而调用draw()方法

  @Override
  public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
    if (!visible) {
      return;
    }
    buildParentLayerListIfNeeded();
    matrix.reset();
    matrix.set(parentMatrix);
    for (int i = parentLayers.size() - 1; i >= 0; i--) {
      matrix.preConcat(parentLayers.get(i).transform.getMatrix());
    }
    int alpha = (int)
        ((parentAlpha / 255f * (float) transform.getOpacity().getValue() / 100f) * 255);
    if (!hasMatteOnThisLayer() && !hasMasksOnThisLayer()) {
      matrix.preConcat(transform.getMatrix());
      drawLayer(canvas, matrix, alpha);
      return;
    }

    rect.set(0, 0, 0, 0);
    getBounds(rect, matrix);
    intersectBoundsWithMatte(rect, matrix);

    matrix.preConcat(transform.getMatrix());
    intersectBoundsWithMask(rect, matrix);

    rect.set(0, 0, canvas.getWidth(), canvas.getHeight());

    canvas.saveLayer(rect, contentPaint, Canvas.ALL_SAVE_FLAG);
    // Clear the off screen buffer. This is necessary for some phones.
    clearCanvas(canvas);
    drawLayer(canvas, matrix, alpha);

    if (hasMasksOnThisLayer()) {
      applyMasks(canvas, matrix);
    }

    if (hasMatteOnThisLayer()) {
      canvas.saveLayer(rect, mattePaint, SAVE_FLAGS);
      clearCanvas(canvas);
      //noinspection ConstantConditions
      matteLayer.draw(canvas, parentMatrix, alpha);
      canvas.restore();
    }

    canvas.restore();
  }

Lottie的原理

首先会通过LottieComposition.Factory的对应类型设置 json资源文件,然后再fromJsonSync方法里面会把json文件解析出图层的大小并且绘制相应的图片资源文件和图层。资源加载完后,会在回调里面设置LottieAnimationViewComposition,从而调用LottieDrawablesetComposition()方法,在setComposition方法里面会通过buildCompositionLayer()方法去创建一个CompositionLayer图层,其中CompositionLayer继承BaseLayer,通过BaseLayerforModel()静态方法获取不同的图层类型,然后LottieDrawablesetComposition()方法里面会开始执行一个ValueAnimation动画,这个动画会驱使BaseLayerdraw()方法不断执行,通过Matrix的矩阵形式不断的绘制各个图层从而形成动画,而这些图层的矩阵变换的数据来源于BaseKeyframeAnimation里面有一个Keyframe对象会去Json里面获取相应得数据。

简单使用

  • 方式1
fun createFromAssetFile(lottieAnimView: LottieAnimationView, fileName: String, init: (LottieAnimationView.() -> Unit)?) {
        lottieAnimView.setAnimation(fileName)
        if(init!=null)
        lottieAnimView.init()
        lottieAnimView.playAnimation()
    }

直接传入assets里面的.json文件名字,然后LottieComposition通过fromInputStream()等方法读取文件转换为string类型,从而得到JsonObject对象,最后在fromJsonSync()方法里面绘制图层

  • 方式2
 private val assetFolders = object : HashMap<String, String>() {
        init {
            put("WeAccept.json", "Images/WeAccept")
        }
    }
fun createFromAssetFile(context: Context,lottieAnimView: LottieAnimationView,
                                     fileName: String, init: (LottieAnimationView.() -> Unit)?) {
        lottieAnimView.imageAssetsFolder = assetFolders[fileName]
        LottieComposition.Factory.fromAssetFileName(context,fileName) { composition ->
            lottieAnimView.setComposition(composition!!)
            if(init!=null)
                lottieAnimView.init()
            lottieAnimView.playAnimation()
        }
    }

其中imageAssetsFolder是设置有些json动画需要的图片资源文件,然后通过LottieComposition里面对应的类型设置json动画资源,LottieComposition.Factory里面主要有如下几种方式

图3.png
  • 方式3

从手机文件夹里面选择 json文件

val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "*/*"
intent.addCategory(Intent.CATEGORY_OPENABLE)
startActivityForResult(Intent.createChooser(intent,"Select a JSON file"),RC_FILE)

得到选择json文件,然后得到InputStream对象,从而通过fromInputStream启动动画

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if(resultCode != Activity.RESULT_OK) return
        when(requestCode){
            RC_FILE -> onFileLoaded(data!!.data)
        }
    }
    fun onFileLoaded(uri:Uri){
        var inputStream: InputStream?= null
        try {
            when(uri.scheme){
                "file" -> {
                    inputStream  = FileInputStream(uri.path)
                }
                "content" -> {
                    inputStream = contentResolver.openInputStream(uri)
                }
                 else -> onLoadError()
            }
            lottieAnim.cancelAnimation()
            if(inputStream!=null)
          createFromFileAnim(ctx,lottieAnim,inputStream)
        }catch (e: FileNotFoundException){
            onLoadError()
        }
    }

    private fun onLoadError() {
        Snackbar.make(drawableLayout!!, "Failed to load animation", Snackbar.LENGTH_LONG).show()
    }
 fun createFromFileAnim(ctx: Context, lottieAnimView: LottieAnimationView, inputStream: InputStream) {
        LottieComposition.Factory.fromInputStream(ctx,inputStream) { composition ->
            if (composition == null) {
                return@fromInputStream
            }
            lottieAnimView.setComposition(composition)
            lottieAnimView.playAnimation()
        }
    }
  • 方式4
 fun createJsonAnim(ctx: Context,lottieAnimView: LottieAnimationView, jsonString: String) {
        try {
            val json = JSONObject(jsonString)
            LottieComposition.Factory
                    .fromJson(ctx.resources, json, OnCompositionLoadedListener { composition ->
                        if (composition == null) {
                            return@OnCompositionLoadedListener
                        }
                        lottieAnimView.setComposition(composition)
                        lottieAnimView.playAnimation()
                    })
        } catch (e: JSONException) {
            
        }
    }

最后,写了一个自己的LottieSimle,小小学习下

LottieSimple

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,904评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,581评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,527评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,463评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,546评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,572评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,582评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,330评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,776评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,087评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,257评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,923评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,571评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,192评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,436评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,145评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容

  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥ios动画全貌。在这里你可以看...
    每天刷两次牙阅读 8,481评论 6 30
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥iOS动画全貌。在这里你可以看...
    F麦子阅读 5,109评论 5 13
  • 转载:http://www.jianshu.com/p/32fcadd12108 每个UIView有一个伙伴称为l...
    F麦子阅读 6,192评论 0 13
  • 转载:http://www.cnblogs.com/jingdizhiwa/p/5601240.html 1.ge...
    F麦子阅读 1,541评论 0 1
  • 清晨,她醒来,拢了拢身下的被子,旁边男人厚重的鼾声传来,她愣了一下,明明旁边有陪伴她的人,为什么她还是有一种孑然一...
    张小至Cpearl阅读 386评论 0 0