美文网首页Android UI相关Android资源
lottie-android 框架使用及源码解析

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

作者: 林帅并不帅 | 来源:发表于2017-03-03 21:39 被阅读7205次

    安卓动画

    最近业务太多,好久没更新。。花了两个晚上研究了一些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的绘制,实现相当复杂,�膜拜大神。

    相关文章

      网友评论

      • 走川:哈哈哈哈哈哈,刚好用这个,居然看到你的文章
      • 594ae1c068a3:插件安装失败 : Failed to install, status = -403!
        f72a0e91d857:用github上的README中的第五种方法可行
      • bc330218fc69:楼主,看在我也姓林的份上,能否帮我实现下动画。我有AE导出的Json文件,但是我不会代码实现,想让你帮我看下是否可以实现效果。QQ409026068
      • 唱歌跑调的程序员:楼主,这要是设置视频动画怎么做?

      本文标题:lottie-android 框架使用及源码解析

      本文链接:https://www.haomeiwen.com/subject/qnecgttx.html