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

目录
相关文章
|
缓存 Ubuntu JavaScript
踩坑记录:QML加载图片资源
踩坑记录:QML加载图片资源
2876 0
|
Java Maven Spring
|
6月前
|
人工智能 安全 Linux
从“养虾”到“赚钱”:OpenClaw实战变现手册(阿里云/本地部署配置API+6大盈利路径+避坑指南)
2026年开年,开源AI智能体OpenClaw(昵称“小龙虾”)以现象级姿态席卷全网,GitHub星标数飙升至25.3万+,贡献者超880人,实现10+平台集成,仅用3周就达成了Linux操作系统30年的普及规模。这只“能替人干活的AI员工”不仅重构了办公与生活流程,更催生了“养虾人”这一全新群体——有人靠上门代部署日入2400元,有人靠技能插件售卖月入1.2万+,甚至有创业者靠行业定制化解决方案创收180万美元。
2645 4
|
12月前
|
Ubuntu Java 物联网
Java原生结合MQTTX,完成心跳对话
简介:本文带你用Java结合MQTT协议与EMQX服务器,在Ubuntu上实现两个程序的“隔空传话”。通过搭建消息代理、编写发送/接收代码,让Java应用实现实时通信,附完整源码与调试技巧,轻松掌握物联网通信核心技能。✨
586 2
|
机器学习/深度学习 人工智能 自动驾驶
企业内训|AI大模型在汽车行业的前沿应用研修-某汽车集团
本课程是TsingtaoAI为某汽车集团高级项目经理设计研发,课程全面系统地解析AI的发展历程、技术基础及其在汽车行业的深度应用。通过深入浅出的理论讲解、丰富的行业案例分析以及实战项目训练,学员将全面掌握机器学习、深度学习、NLP与CV等核心技术,了解自动驾驶、智能制造、车联网与智能营销等关键应用场景,洞悉AI技术对企业战略布局的深远影响。
1213 97
|
人工智能 Java API
阿里云开源 AI 应用开发框架:Spring AI Alibaba
近期,阿里云重磅发布了首款面向 Java 开发者的开源 AI 应用开发框架:Spring AI Alibaba(项目 Github 仓库地址:alibaba/spring-ai-alibaba),Spring AI Alibaba 项目基于 Spring AI 构建,是阿里云通义系列模型及服务在 Java AI 应用开发领域的最佳实践,提供高层次的 AI API 抽象与云原生基础设施集成方案,帮助开发者快速构建 AI 应用。本文将详细介绍 Spring AI Alibaba 的核心特性,并通过「智能机票助手」的示例直观的展示 Spring AI Alibaba 开发 AI 应用的便利性。示例源
14134 144
|
API 容器
Flutter UI组件库(JUI)
Flutter UI组件库(JUI)
3688 17
|
SQL 关系型数据库 MySQL
【错误】mysql 出现 “1067 - Invalid default value for ‘UPDATE_TIME‘ “ 错误提示的解决办法
【错误】mysql 出现 “1067 - Invalid default value for ‘UPDATE_TIME‘ “ 错误提示的解决办法
1256 1
cocos creator+vscode安装及配置介绍
cocos creator+vscode安装及配置介绍
1369 0