官方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库的业务来说,都是一种帮助。

目录
相关文章
|
XML JSON 前端开发
官方Lottie库能力增强实现
背景Lottie提供了播放复杂、酷炫动能力画,在移动端被广泛利用。在我们的应用中也被频繁、大量使用。它使用简单,仅需几行代码就能播放设计师设计的动画,帮助开发节省了时间成本。也正因为使用频繁,在使用过程中我们遇到了一些相关的问题。使用Lottie支持加载本地文件播放,也支持远程下载zip,json文件进行播放。这俩者在我们平时开发中都有使用到。本地播放本地播放比较简单。可以直接在xml实现,也可以
1179 0
官方Lottie库能力增强实现
|
前端开发 JavaScript 小程序
7 款最棒的开源 React UI 库测评 - 特别针对国内使用场景推荐
优秀的 React UI 组件库,帮我们节省开发时间,提高开发效率,统一设计语言。更棒的是内置的功能复杂,我们自己很难处理的常用组件,比如表格、表单、富文本编辑器、时间日期选择器、实时拖拽组件等,再进一步,还有帮我们把组件的轮子装好的 React admin 后台管理系统。本文推荐 7 款适用于中文使用者习惯的开源 React UI 库,特别针对国内使用场景推荐。
1042 0
|
15小时前
|
UED
鸿蒙next版开发:音频并发策略扩展(ArkTS)
在HarmonyOS 5.0中,音频并发策略通过ArkTS的AudioSessionManager接口管理多个音频流的交互和优先级。本文介绍了如何自定义音频焦点策略,包括激活、停用音频会话及注册回调函数,并提供了示例代码。适用于多媒体、通信和游戏应用。
19 4
|
29天前
|
数据可视化 搜索推荐
重磅更新-UniApp自定义字体可视化设计
重磅更新-UniApp自定义字体可视化设计
39 0
|
6月前
|
移动开发 小程序 IDE
11月开发者日回顾|IDE支持版本管理、JSAPI新增预拉取能力、开发者高频问题详解
11月开发者日回顾|IDE支持版本管理、JSAPI新增预拉取能力、开发者高频问题详解
62 11
|
6月前
|
监控 Linux iOS开发
如何使用克魔开发助手优化iOS应用性能
如何使用克魔开发助手优化iOS应用性能
68 1
|
6月前
|
vr&ar 开发工具 图形学
Unity引擎:收费模式和服务升级,为游戏开发带来更多可能性
Unity引擎:收费模式和服务升级,为游戏开发带来更多可能性
119 0
|
存储 缓存 iOS开发
iOS 轻量化动态图像下载缓存框架实现
日常开发过程中,图片的下载会占用大量的带宽,图片的加载会消耗大量的性能和内存,正确的使用图片显得尤为重要。 同样也经常需要在各类型控件上读取网络图片和处理本地图片,例如:UIImageView、UIBtton、NSImageView、NSButton等等。
iOS 轻量化动态图像下载缓存框架实现
|
存储 JSON 数据可视化
移动端可视化引擎 F2 架构设计之: 为什么要选用 JSX
移动端可视化引擎 F2 架构设计之: 为什么要选用 JSX
144 0
|
数据管理 测试技术 C#
一个近乎完美的Unity全平台原生c#热更方案
HybridCLR是一个特性完整、零成本、高性能、低内存的近乎完美的Unity全平台原生c#热更方案。
608 0
一个近乎完美的Unity全平台原生c#热更方案