lottie-android 框架使用及源码解析

安卓动画

最近业务太多,好久没更新。。花了两个晚上研究了一些lottie框架的实现,学到了一些思路,有机会可以把view绘制深入学习一下,ok开始。

https://github.com/airbnb/lottie-android

Lottie,Airbnb开源的一个牛逼的动画框架,绚丽的动画效果令人瞠目。

Example2.gif

没错这在以往的意识来看是根本不可能实现的动画效果,那么究竟它是如何实现的呢?

初探

打开LottieSample工程,并将它运行起来,首页就可以看到上图中间的这个动画效果,而代码实现更是简单到没朋友。

xml:

Paste_Image.png

java代码:

Paste_Image.png

没错就是初始化了一个LottieAnimationView并且调用playAnimation()方法,就出现了上图的动画效果,这里注意到在xml初始化参数中有个lottie_fileName参数,传了一个貌似是json文件路径,而在assets的Logo目录下,确实有个LogoSmall.json文件,打开一看懵逼了,完全看不懂。

原来这个json文件的内容不是手写的,而是软件生成的,设计师可以使用Adobe的 After Effects(简称 AE)工具制作这个动画,在AE中安装一个叫做Bodymovin的插件,使用这个插件可以将动画效果生成一个json文件,而这个json文件通过LottieAnimationView解析并最终生成绚丽的动画效果展示在我们面前。

使用方法

Lottie supports API 14 and above,要求4.0以上

依赖

dependencies {  
  compile 'com.airbnb.android:lottie:1.5.1'
}
使用方法一:初始化一个LottieAnimationView
 <com.airbnb.lottie.LottieAnimationView
        android:id="@+id/animation_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:lottie_fileName="hello-world.json"
        app:lottie_loop="true"
        app:lottie_autoPlay="true" />

只接受这三个参数,语意清楚就不多解释了。

也可以通过java代码设置

LottieAnimationView animationView = (LottieAnimationView) findViewById(R.id.animation_view);
animationView.setAnimation("hello-world.json");
animationView.loop(true);

setAnimation有三个方法

Paste_Image.png

其中String是fileName,是在assets目录下的文件,CacheStrategy表示缓存策略,

Paste_Image.png

代表使用何种策略进行存储,默认为None即不存储,而使用时会优先从内存缓存中命中读取,从而减小IO开销。

JSONObject直接传入一段json数据,可以通过网络获取一段json进行解析处理。

使用方法二:使用LottieComposition
Paste_Image.png

在LottieComposition中提供了三种from方法,可以接受assets文件名、json对象、流对象三种参数,Sync表示同步,但是却是包可见方法,并不能被外部调用。

LottieComposition.fromJson(getResources(), jsonObject, new LottieComposition.OnCompositionLoadedListener() {
    @Override
    public void onCompositionLoaded(LottieComposition composition) {
        animationView.setComposition(composition);
        animationView.playAnimation();
    }
});

外部调用时只提供异步方法,使用AsyncTask进行异步调用,将JsonObject的解析处理过程放在异步线程处理,并将解析生成的LottieComposition对象回调主线程,因为这个json对象可能有上百k之大,所以整个处理过程的复杂度和耗时还是很高的,所以不要在ui线程中解析处理。

一点想法

我们可以通过请求的方式获取json对象,并将解析的过程放在网络请求的异步线程中处理,使用反射调用同步方法,将调用放在异步线程中执行,这样就可以将整个过程请求和解析的过程封装在一起。

注意点:

LottieAnimationView内部有个LottieDrawable对象,setComposition方法实质上是将LottieComposition应用到LottieDrawable上,官方readme上有这样一段说明

Paste_Image.png

但应该是后面改过,LottieDrawable是包可见的,外部无法调用到,并且在LottieDrawable类注释上有这样一段描述。

Paste_Image.png

推荐使用LottieAnimationView而不是直接使用LottieDrawable,因为LottieDrawable的回收LottieAnimationView帮你做了,而自己操作LottieDrawable需要考虑的回收调用。

Paste_Image.png

所以仅推荐以上两种用法,不推荐直接使用Drawable的方式除非一定需要。

源码解析

好了,说完用法,要来看看到底这个过程发生了什么。

有两个重要的过程

一、json文件解析成LottieComposition的过程

所有的文件解析过程都会走到LottieComposition下的fromJsonSync方法,返回一个LottieComposition对象,中间都是对jsonObject的解析过程,将jsonObject中的信息解析到LottieComposition对象中。

static LottieComposition fromJsonSync(Resources res, JSONObject json) {
  LottieComposition composition = new LottieComposition(res);

  ···

  try {
    JSONArray jsonLayers = json.getJSONArray("layers");
    for (int i = 0; i < jsonLayers.length(); i++) {
      Layer layer = Layer.fromJson(jsonLayers.getJSONObject(i), composition);
      addLayer(composition, layer);
    }
  } catch (JSONException e) {
    throw new IllegalStateException("Unable to find layers.", e);
  }

  ····

  return composition;
}

这段代码就是把jsonobject中的数据赋值给LottieComposition对象变量,看下图LottieComposition的变量。

Paste_Image.png

bounds代表边界,start和end代表开始和结束时间,duration为时长,scale为为density。Layer就是图层的概念,里面存放的是图层的数据,在循环遍历jsonLayers生成Layer对象时调用了fromJson方法,同样的也是解析和赋值过程。

static Layer fromJson(JSONObject json, LottieComposition composition) {
  Layer layer = new Layer(composition);

  ····

  return layer;
}
Paste_Image.png

以上为Layer类中的变量,除了基础变量外,会看到红框中的变量,这些变量是跟动画相关的参数,都是AnimatableValue的实现类。

AnimatableValue的继承关系如图,看样子是控制颜色、scale、path等基础动画的。

Paste_Image.png

那么生成的LottieComposition对象可以理解成一个包含所有图层动画信息的对象,等下看看这些变量是如何被使用的。

二、生成LayerView树

生成的LottieComposition是通过LottieDrawable的setComposition方法将动画信息进行设置的,核心调用方法为buildLayersForComposition。

private void buildLayersForComposition(LottieComposition composition) {
  ···
  LongSparseArray<LayerView> layerMap = new LongSparseArray<>(composition.getLayers().size());
  List<LayerView> layers = new ArrayList<>(composition.getLayers().size());
  LayerView maskedLayer = null;
  for (int i = composition.getLayers().size() - 1; i >= 0; i--) {
    Layer layer = composition.getLayers().get(i);
    LayerView layerView;
    if (maskedLayer == null) {
      layerView =
          new LayerView(layer, composition, getCallback(), mainBitmap, maskBitmap, matteBitmap);
    } else {
      ···
      layerView =
          new LayerView(layer, composition, getCallback(), mainBitmapForMatte, maskBitmapForMatte,
              null);
    }
    layerMap.put(layerView.getId(), layerView);
    if (maskedLayer != null) {
      maskedLayer.setMatteLayer(layerView);
      maskedLayer = null;
    } else {
      layers.add(layerView);
      if (layer.getMatteType() == Layer.MatteType.Add) {
        maskedLayer = layerView;
      }
    }
  }

  for (int i = 0; i < layers.size(); i++) {
    LayerView layerView = layers.get(i);
    addLayer(layerView);
  }

  for (int i = 0; i < layerMap.size(); i++) {
    long key = layerMap.keyAt(i);
    LayerView layerView = layerMap.get(key);
    LayerView parentLayer = layerMap.get(layerView.getLayerModel().getParentId());
    if (parentLayer != null) {
      layerView.setParentLayer(parentLayer);
    }
  }
}

将之前解析出来的Layers数据倒序遍历并生成同等数量的LayerView,将LayerView通过addLayer方法添加到layers列表里面,这段代码执行完,就生成了一个LayerView的树状结构,以LottieDrawable为根节点(LottieDrawable也是继承自AnimatableLayer,跟LayerView相同)。

void addLayer(AnimatableLayer layer) {
  layer.parentLayer = this;
  layers.add(layer);
  layer.setProgress(progress);
  invalidateSelf();
}

在LayerView的构造器中有个方法:

private void setupForModel() {
  setBackgroundColor(layerModel.getSolidColor());
  setBounds(0, 0, layerModel.getSolidWidth(), layerModel.getSolidHeight());

  setPosition(layerModel.getPosition().createAnimation());
  setAnchorPoint(layerModel.getAnchor().createAnimation());
  setTransform(layerModel.getScale().createAnimation());
  setRotation(layerModel.getRotation().createAnimation());
  setAlpha(layerModel.getOpacity().createAnimation());

  setVisible(layerModel.hasInAnimation(), false);

  List<Object> reversedItems = new ArrayList<>(layerModel.getShapes());
  Collections.reverse(reversedItems);
  Transform currentTransform = null;
  ShapeTrimPath currentTrimPath = null;
  ShapeFill currentFill = null;
  ShapeStroke currentStroke = null;

  for (int i = 0; i < reversedItems.size(); i++) {
    Object item = reversedItems.get(i);
    if (item instanceof ShapeGroup) {
      GroupLayerView groupLayer = new GroupLayerView((ShapeGroup) item, currentFill,
          currentStroke, currentTrimPath, currentTransform, getCallback());
      addLayer(groupLayer);
    } else if (item instanceof ShapeTransform) {
      currentTransform = (ShapeTransform) item;
    } else if (item instanceof ShapeFill) {
      currentFill = (ShapeFill) item;
    } else if (item instanceof ShapeTrimPath) {
      currentTrimPath = (ShapeTrimPath) item;
    } else if (item instanceof ShapeStroke) {
      currentStroke = (ShapeStroke) item;
    } else if (item instanceof ShapePath) {
      ShapePath shapePath = (ShapePath) item;
      ShapeLayerView shapeLayer =
          new ShapeLayerView(shapePath, currentFill, currentStroke, currentTrimPath,
              new ShapeTransform(composition), getCallback());
      addLayer(shapeLayer);
    } else if (item instanceof RectangleShape) {
      RectangleShape shapeRect = (RectangleShape) item;
      RectLayer shapeLayer =
          new RectLayer(shapeRect, currentFill, currentStroke, new ShapeTransform(composition),
              getCallback());
      addLayer(shapeLayer);
    } else if (item instanceof CircleShape) {
      CircleShape shapeCircle = (CircleShape) item;
      EllipseShapeLayer shapeLayer =
          new EllipseShapeLayer(shapeCircle, currentFill, currentStroke, currentTrimPath,
              new ShapeTransform(composition), getCallback());
      addLayer(shapeLayer);
    }
  }

  if (maskBitmap != null && layerModel.getMasks() != null && !layerModel.getMasks().isEmpty()) {
    setMask(new MaskLayer(layerModel.getMasks(), getCallback()));
    maskCanvas = new Canvas(maskBitmap);
  }
  buildAnimations();
}

这里的layerModel就是刚才解析出来的Layer,这里用到了刚才红框圈起来的那些变量,调用了AnimatableValue的createAnimation方法,生成了一个KeyframeAnimation对象,查看KeyframeAnimation,发现是抽象类,可以看到有几个关键的变量。

首先有个AnimationListener的list,通过观察者模式修改订阅者的信息,等下看看谁是订阅者。还有个progress变量和setProgress方法,应为进度控制。

void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
  if (progress < getStartDelayProgress()) {
    progress = 0f;
  } else if (progress > getDurationEndProgress()) {
    progress = 1f;
  } else {
    progress = (progress - getStartDelayProgress()) / getDurationRangeProgress();
  }
  if (progress == this.progress) {
    return;
  }
  this.progress = progress;

  T value = getValue();
  for (int i = 0; i < listeners.size(); i++) {
    listeners.get(i).onValueChanged(value);
  }
}

调用setProgress方法,会将getValue的结果传递给所有的订阅者。

拿ColorKeyframeAnimation的getValue的实现类为例

float percentageIntoFrame = 0;
if (!isDiscrete) {
  percentageIntoFrame = (progress - startKeytime) / (endKeytime - startKeytime);
  if (interpolators != null) {
    percentageIntoFrame =
        interpolators.get(keyframeIndex).getInterpolation(percentageIntoFrame);
  }
}

int startColor = values.get(keyframeIndex);
int endColor = values.get(keyframeIndex + 1);

return (Integer) argbEvaluator.evaluate(percentageIntoFrame, startColor, endColor);

以上这段代码是getValue的具体实现,可以看到是将开始颜色和结束颜色通过progress计算一个当前进度值,并计算介于两个颜色的中间颜色。

其他类似。

最后再看一下AnimatableLayer的变量

Paste_Image.png

每个图层会有自己的parentLayer,会有平移动画、透明度动画、旋转动画、位置及进度信息,这些都放在animations列表里面,同时还有个layers列表,表示当前层还会包含的一些图层信息。

所以第二步可以理解为把第一步的信息生成AnimatableLayer树的过程,包含所有的图层实现,进度控制,动画信息,都已经准备好等待被调用了。

三、动画执行

最后来说动画执行,调用了playAnimation方法,最终是调用到一个属性动画执行,

private final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
      @Override public void onAnimationUpdate(ValueAnimator animation) {
        setProgress(animation.getAnimatedFraction());
      }
    });

属性动画的执行是通过调用setProgress。

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

  for (int i = 0; i < layers.size(); i++) {
    layers.get(i).setProgress(progress);
  }
}

刚才提到这是个树状结构,所以通过修改progress,整个树就运作起来,通过layers.setProgress设置所有子图层的progress,子图层又包含了animations和layers,每个图层的animations存放了很多的AnimatableValue,通过setProgress,将修改的value值回调订阅者,而订阅者其实就是LottieDrawable,从根节点开始invalidateSelf,调用到draw方法中进行绘制。

@Override
public void draw(@NonNull Canvas canvas) {
  int saveCount = canvas.save();
  applyTransformForLayer(canvas, this);

  int backgroundAlpha = Color.alpha(backgroundColor);
  if (backgroundAlpha != 0) {
    int alpha = backgroundAlpha;
    if (this.alpha != null) {
      alpha = alpha * this.alpha.getValue() / 255;
    }
    solidBackgroundPaint.setAlpha(alpha);
    if (alpha > 0) {
      canvas.drawRect(getBounds(), solidBackgroundPaint);
    }
  }
  for (int i = 0; i < layers.size(); i++) {
    layers.get(i).draw(canvas);
  }
  canvas.restoreToCount(saveCount);
}

void applyTransformForLayer(@Nullable Canvas canvas, AnimatableLayer layer) {
    if (canvas == null) {
      return;
    }
    // TODO: Determine if these null checks are necessary.
    if (layer.position != null) {
      PointF position = layer.position.getValue();
      if (position.x != 0 || position.y != 0) {
        canvas.translate(position.x, position.y);
      }
    }

    if (layer.rotation != null) {
      float rotation = layer.rotation.getValue();
      if (rotation != 0f) {
        canvas.rotate(rotation);
      }
    }

    if (layer.transform != null) {
      ScaleXY scale = layer.transform.getValue();
      if (scale.getScaleX() != 1f || scale.getScaleY() != 1f) {
        canvas.scale(scale.getScaleX(), scale.getScaleY());
      }
    }

    if (layer.anchorPoint != null) {
      PointF anchorPoint = layer.anchorPoint.getValue();
      if (anchorPoint.x != 0 || anchorPoint.y != 0) {
        canvas.translate(-anchorPoint.x, -anchorPoint.y);
      }
    }
  }

看到这里,明白了,每次value值发生变化,drawable就会重绘,所有的图层都会进行绘制,重绘时使用新的值进行绘制,从而完成了动画的变化。简单点说,就是每个progress的值,会对应每个图层中的一个状态,progress的改变,就是把这些状态不断绘制出来,从而实现了动画的效果。

一开始以为是属性动画相关,没想到深入到view的绘制,实现相当复杂,�膜拜大神。

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

推荐阅读更多精彩内容