官方Lottie库能力增强实现

简介: 背景Lottie提供了播放复杂、酷炫动能力画,在移动端被广泛利用。在我们的应用中也被频繁、大量使用。它使用简单,仅需几行代码就能播放设计师设计的动画,帮助开发节省了时间成本。也正因为使用频繁,在使用过程中我们遇到了一些相关的问题。使用Lottie支持加载本地文件播放,也支持远程下载zip,json文件进行播放。这俩者在我们平时开发中都有使用到。本地播放本地播放比较简单。可以直接在xml实现,也可以

背景

Lottie提供了播放复杂、酷炫动能力画,在移动端被广泛利用。在我们的应用中也被频繁、大量使用。它使用简单,仅需几行代码就能播放设计师设计的动画,帮助开发节省了时间成本。也正因为使用频繁,在使用过程中我们遇到了一些相关的问题。

使用

Lottie支持加载本地文件播放,也支持远程下载zip,json文件进行播放。这俩者在我们平时开发中都有使用到。

本地播放

本地播放比较简单。可以直接在xml实现,也可以通过代码加载Lottie文件。

xml实现:

<com.airbnb.lottie.LottieAnimationView
	android:id="@+id/lottie_test"
	android:layout_width="match_parent"
	android:layout_height="wrap_content"
	app:lottie_rawRes="@raw/data"
	app:lottie_autoPlay="true"
	app:lottie_loop="true" />

代码实现:

findViewById<LottieAnimationView>(R.id.lottie_test).setAnimation(R.raw.data)

远程播放

远程播放相对来说比较复杂,首先要通过网络从远端下载Lottie文件,接着解析下载到本地的文件,最后才是播放。这其中但凡有一个环节出了问题,最终都会导致播放失败,而且还存在crash,需要进行必要的保护。代码实现很简单,仅需一行便可实现远程播放,但其实这背后都是官方帮我们把整个过程做了处理。

findViewById<LottieAnimationView>(R.id.lottie_test).setAnimationFromUrl(lottieUrl)

问题

对于本地播放来说,基本没什么问题,只要能正常播放,上线后表现也是一致的。但对于远程播放来说,都一些不确定的因素,例如网络环境、文件格式、异常处理等。在我们使用过程中出现了类似问题,其中出现问题次数最多的就是Lottie文件格式的问题,最终表现就是导致Lottie无法正常播放,这其中也伴随着crash的产生,当然因为我们早期已加了保护,所以不会导致应用出现崩溃。

实现

文件格式

Lottie主要支持俩种文件格式:json、zip。根据这俩种格式,可以细分出多种表现形式。

                                                                     

后缀为.json

这是官方支持的标准格式,对于第一种纯json的格式来说,可以再次细分出俩种形式:图片格式为base64的json、通过原生canvas实现的json。

图片格式为base64的json:

{"id":"image_1","w":1125,"h":735,"u":"","p":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD...}

原生canvas实现的json:

{"v":"5.4.4","fr":30,"ip":0,"op":20,"w":144,"h":144,"nm":"common loading_Color_json","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"centre controller","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0],"e":[90]},{"t":20}],"ix":10},"p":{"a":0,"k":[72,72,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[100,100,100],"e":[90,90,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[90,90,100],"e":[100,100,100]},{"t":20}],"ix":6}},"ao":0,"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"huaban 4","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[125,345,0],"ix":1},"s":{"a":0,"k":[20,20,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-69.036,0],[0,-69.036],[69.035,0],[0,0],[0,12.081],[0,0]],"o":[[69.035,0],[0,69.035],[0,0],[-12.081,0],[0,0],[0,-69.036]],"v":[[0,-125],[125,0],[0,125],[-103.125,125],[-125,103.125],[-125,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"bm":0,"g":{"p":5,"k":{"a":0,"k":[0,1,0.514,0.235,0.25,1,0.402,0.341,0.5,1,0.29,0.447,0.75,1,0.243,0.524,1,1,0.196,0.6],"ix":9}},"s":{"a":0,"k":[-102,102],"ix":5},"e":{"a":0,"k":[88,-88],"ix":6},"t":1,"nm":"Gradient Fill 1","mn":"ADBE Vector Graphic - G-Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[125,125],"e":[125,150],"to":[0,0],"ti":[0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[125,150],"e":[125,125],"to":[0,0],"ti":[0,0]},{"t":20}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-45,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"huaban 3","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":90,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[125,345,0],"ix":1},"s":{"a":0,"k":[20,20,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-69.036,0],[0,-69.036],[69.035,0],[0,0],[0,12.081],[0,0]],"o":[[69.035,0],[0,69.035],[0,0],[-12.081,0],[0,0],[0,-69.036]],"v":[[0,-125],[125,0],[0,125],[-103.125,125],[-125,103.125],[-125,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"bm":0,"g":{"p":5,"k":{"a":0,"k":[0,1,0.514,0.235,0.25,1,0.402,0.341,0.5,1,0.29,0.447,0.75,1,0.243,0.524,1,1,0.196,0.6],"ix":9}},"s":{"a":0,"k":[-102,102],"ix":5},"e":{"a":0,"k":[88,-88],"ix":6},"t":1,"nm":"Gradient Fill 1","mn":"ADBE Vector Graphic - G-Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[125,125],"e":[125,150],"to":[0,0],"ti":[0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[125,150],"e":[125,125],"to":[0,0],"ti":[0,0]},{"t":20}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-45,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"huaban 2","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":180,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[125,345,0],"ix":1},"s":{"a":0,"k":[20,20,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-69.036,0],[0,-69.036],[69.035,0],[0,0],[0,12.081],[0,0]],"o":[[69.035,0],[0,69.035],[0,0],[-12.081,0],[0,0],[0,-69.036]],"v":[[0,-125],[125,0],[0,125],[-103.125,125],[-125,103.125],[-125,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"bm":0,"g":{"p":5,"k":{"a":0,"k":[0,1,0.514,0.235,0.25,1,0.402,0.341,0.5,1,0.29,0.447,0.75,1,0.243,0.524,1,1,0.196,0.6],"ix":9}},"s":{"a":0,"k":[-102,102],"ix":5},"e":{"a":0,"k":[88,-88],"ix":6},"t":1,"nm":"Gradient Fill 1","mn":"ADBE Vector Graphic - G-Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[125,125],"e":[125,150],"to":[0,0],"ti":[0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[125,150],"e":[125,125],"to":[0,0],"ti":[0,0]},{"t":20}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-45,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"huaban 1","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":270,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[125,345,0],"ix":1},"s":{"a":0,"k":[20,20,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-69.036,0],[0,-69.036],[69.035,0],[0,0],[0,12.081],[0,0]],"o":[[69.035,0],[0,69.035],[0,0],[-12.081,0],[0,0],[0,-69.036]],"v":[[0,-125],[125,0],[0,125],[-103.125,125],[-125,103.125],[-125,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"bm":0,"g":{"p":5,"k":{"a":0,"k":[0,1,0.514,0.235,0.25,1,0.402,0.341,0.5,1,0.29,0.447,0.75,1,0.243,0.524,1,1,0.196,0.6],"ix":9}},"s":{"a":0,"k":[-102,102],"ix":5},"e":{"a":0,"k":[88,-88],"ix":6},"t":1,"nm":"Gradient Fill 1","mn":"ADBE Vector Graphic - G-Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[125,125],"e":[125,150],"to":[0,0],"ti":[0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[125,150],"e":[125,125],"to":[0,0],"ti":[0,0]},{"t":20}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-45,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":20,"st":0,"bm":0}],"markers":[]}

后缀为.zip

后缀是zip的可以分为只有json文件、包含json文件和image文件夹俩种。

  

只包含josn文件

第一种是当文件为zip且压缩包里面只有json文件,这种情况官方Lottie库是不支持的,最终会播放失败且会抛出异常。

IllegalStateException("There is no image for " + entry.getValue().getFileName())

包含json文件及image文件夹

这种是官方认定的标准格式,就是当lottie文件的后缀为zip时,这时包含一个json文件和一个image文件夹,且json文件里的图片格式不能是base64,必须是指向image文件夹里图片的路径。如果UED同学给出的Lottie文件是按这个规则生成的,就可以正常播放。但UED同学有时给出的却不是这个格式,给出的是json文件里的图片格式是base64,导致无法正常播放。

优化

鉴于以上俩种无法播放的case,我拉取了官方的源码,进行了修改优化。

后缀为.zip只包含json文件

对于这个case,不能播放的原因是lottie源码对于后缀为zip格式的文件,会读取image文件夹里面的图片,然后将图片decode转换成bitmao,但此时并没有image文件夹,所以获取到的bitmap会为null。具体代码:

// Ensure that all bitmaps have been set.
for (Map.Entry<String, LottieImageAsset> entry : composition.getImages().entrySet()) {
  if (entry.getValue().getBitmap() == null) {
    return new LottieResult<>(new IllegalStateException("There is no image for " + entry.getValue().getFileName()));
  }
}

生成bitmap的代码在这里,由于现在没有image文件夹,没有图片,故无法生成bitmap,此时就会抛出上面的异常。

for (Map.Entry<String, Bitmap> e : images.entrySet()) {
   LottieImageAsset imageAsset = findImageAssetForFileName(composition, e.getKey());
   if (imageAsset != null) {
     imageAsset.setBitmap(Utils.resizeBitmapIfNeeded(e.getValue(), imageAsset.getWidth(), imageAsset.getHeight()));
   }
}

针对上面的case,我对官方源码进行了如下修改:

    if (images.isEmpty()) {
      for (Map.Entry<String, LottieImageAsset> entry : composition.getImages().entrySet()) {
        LottieImageAsset asset = entry.getValue();
        if (asset == null) {
          return null;
        }
        String filename = asset.getFileName();
        BitmapFactory.Options opts = new BitmapFactory.Options();
        opts.inScaled = true;
        opts.inDensity = 160;

        if (filename.startsWith("data:") && filename.indexOf("base64,") > 0) {
          // Contents look like a base64 data URI, with the format data:image/png;base64,<data>.
          byte[] data;
          try {
            data = Base64.decode(filename.substring(filename.indexOf(',') + 1), Base64.DEFAULT);
          } catch (IllegalArgumentException e) {
            Logger.warning("data URL did not have correct base64 format.", e);
            return null;
          }
          asset.setBitmap(BitmapFactory.decodeByteArray(data, 0, data.length, opts));
        }
      }
    }

当images这个集合为空时,读取json里的图片信息,如果此时图片格式为base64,则通过decode将数据转换为bitmap放到composition里的对象里,此时就不会抛出异常,能够正常播放。此修改已被作者合入官方master分支

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

后缀为.zip包含josn文件和image文件夹,json文件里的图片格式为base64

这个case其实主要是UED同学通过压缩工具压缩Lottie的时候,操作不当,出现了这种情况,那这个case通过技术手段可以支持吗?也是可以的。在这种情况下,如果image文件夹里有图片的话,此时images的size是大于0的,但是也无法正常播放,原因为此时json文件里的图片不是文件路径,而是base64的图片数据,所以匹配时还是会失败:

for (Map.Entry<String, Bitmap> e : images.entrySet()) {
  LottieImageAsset imageAsset = findImageAssetForFileName(composition, e.getKey());
  if (imageAsset != null) {
    imageAsset.setBitmap(Utils.resizeBitmapIfNeeded(e.getValue(), imageAsset.getWidth(), imageAsset.getHeight()));
  }
}

这里拿到的imageAsset为null,因为没有匹配的filenName:

private static LottieImageAsset findImageAssetForFileName(LottieComposition composition, String fileName) {
  for (LottieImageAsset asset : composition.getImages().values()) {
    if (asset.getFileName().equals(fileName)) {
      return asset;
    }
  }
  return null;
}

针对这个case做了如下修改,此修改可以把上面提到的这俩个case都cover住:

    for (Map.Entry<String, LottieImageAsset> entry : composition.getImages().entrySet()) {
      if (entry.getValue().getBitmap() == null) {
        LottieImageAsset asset = entry.getValue();
        if (asset == null) {
          return null;
        }
        String filename = asset.getFileName();
        BitmapFactory.Options opts = new BitmapFactory.Options();
        opts.inScaled = true;
        opts.inDensity = 160;

        if (filename.startsWith("data:") && filename.indexOf("base64,") > 0) {
          // Contents look like a base64 data URI, with the format data:image/png;base64,<data>.
          byte[] data;
          try {
            data = Base64.decode(filename.substring(filename.indexOf(',') + 1), Base64.DEFAULT);
          } catch (IllegalArgumentException e) {
            Logger.warning("data URL did not have correct base64 format.", e);
            return null;
          }
          asset.setBitmap(BitmapFactory.decodeByteArray(data, 0, data.length, opts));
        }
      }
    }

目前我们app使用的就是通过自己打包Lottie代码,支持了以上base,为什么要支持呢?因为我们app大量使用动态配置Lottie功能,尤其是我们首页顶部的氛围模块,是运营经常会用到的一个资源位,很多时候配置的Lottie无法正常播放,需要开发及时排查。经过总结经验来看,大都是Lottie格式错误导致的。每次出现这种情况,需要耗费人力去排查,这样频繁的发生,是一种资源损耗,如果能通过技术手段解决掉这个问题,也是实现了降本提效。所以这是一个值得花时间研究解决的问题。

总结

此次优化,增强了官方Lottie的功能,对于我们业务来说更是有很大的帮助,做到了降本提效。对于后一次的优化,我会提交pr到官方Lottie库,跟作者沟通merge到master分支,这样对于整个行业内使用Lottie库的业务来说,都是一种帮助。

目录
相关文章
|
存储 Java Android开发
Android插件化动态加载apk
支付宝作为一个宿主apk提前将要集成的apk作为一个插件(plugin)下载到本地,然后当使用该plugin(apk)的时候再去加载对应plugin(apk)的资源文件以及对应的native页面。就是不去安装plugin(apk)就可以直接运行该plugin(apk)中的页面。
1151 0
|
9月前
|
人工智能 文字识别 异构计算
SmolDocling:256M多模态小模型秒转文档!开源OCR效率提升10倍
SmolDocling 是一款轻量级的多模态文档处理模型,能够将图像文档高效转换为结构化文本,支持文本、公式、图表等多种元素识别,适用于学术论文、技术报告等多类型文档。
891 1
SmolDocling:256M多模态小模型秒转文档!开源OCR效率提升10倍
|
7月前
|
缓存 编解码 Android开发
Android内存优化之图片优化
本文主要探讨Android开发中的图片优化问题,包括图片优化的重要性、OOM错误的成因及解决方法、Android支持的图片格式及其特点。同时介绍了图片储存优化的三种方式:尺寸优化、质量压缩和内存重用,并详细讲解了相关的实现方法与属性。此外,还分析了图片加载优化策略,如异步加载、缓存机制、懒加载等,并结合多级缓存流程提升性能。最后对比了几大主流图片加载框架(Universal ImageLoader、Picasso、Glide、Fresco)的特点与适用场景,重点推荐Fresco在处理大图、动图时的优异表现。这些内容为开发者提供了全面的图片优化解决方案。
305 1
|
JSON 前端开发 JavaScript
前端使用lottie-web,使用AE导出的JSON动画贴心教程
前端使用lottie-web,使用AE导出的JSON动画贴心教程
1928 2
|
数据可视化 定位技术
地图可视化开发技巧:geojson转svg后再转emf格式插入ppt实现编辑的解决方案
地图可视化开发技巧:geojson转svg后再转emf格式插入ppt实现编辑的解决方案
594 0
|
JSON 前端开发 Android开发
Lottie 动画里有图片?有动画效果但图片加载不出来?
Lottie 动画里有图片?有动画效果但图片加载不出来?
2522 0
|
JSON 小程序 数据格式
【经验分享】支付宝小程序lottie动画尝鲜
【经验分享】支付宝小程序lottie动画尝鲜
599 6
|
安全 Linux 数据安全/隐私保护
【SPI协议】了解ARM平台上的SPI的基本应用
【SPI协议】了解ARM平台上的SPI的基本应用
1398 0
|
机器学习/深度学习 存储 弹性计算
阿里云GPU服务器价格多少钱?2024年阿里云GPU服务器价格配置及性能测评
2024年阿里云GPU服务器是一款高性能的计算服务器,基于GPU应用的计算服务,多适用于视频解码、图形渲染、深度学习、科学计算等应用场景。阿里云GPU服务器具有超强的计算能力、网络性能出色、购买方式灵活、高性能实例存储等特点。 阿里云提供了多种配置的GPU服务器,包括gn6v、gn6i、vgn6i-vws和gn6e等,这些服务器配备了不同型号的GPU计算卡、不同规格的内存和存储空间,可以满足不同用户的计算需求。同时,阿里云还为新用户提供了特惠价格,包年购买更是低至3折起,使得用户可以更加经济地购买到高性能的GPU服务器。
783 0