Android•Lottie动画库填坑记

作者: 负了时光不负卿 | 来源:发表于2017-10-17 23:56 被阅读1107次

    1. 入坑背景

    由于从事直播软件开发的缘故,本猿在版本迭代过程中一期不落的接触到各式各样动画效果。最早的时候,苦逼的用Android原生动画做直播间全屏礼物,反复的看着美工给的Flash效果图,不断的拼凑素材图片,调整控制动画播放的属性值,各个动画代码都很类似,但却无法套用,一连两三天下来,基本上脑海中除了动画就一片空白...不过后来采用spine礼物框架以后,也就告别这样的悲惨人生。然而就在上一版本中,产品因为...的原因,让不同的用户进入房间有不一样的效果,其中就包括文字背景带粒子效果,对于这样的效果,Android原生动画显然无能为力,如果采用帧动画,由于大量素材文件的引入带来最直接的不良影响就是安装包体积过大。经过评估之后,决定使用三方动画框架,从服务器下载动画资源,在特定时间对不同资源文件进行播放,最终采用相对比较成熟的Lottie框架。

    2. 踩坑准备

    熟悉一个新的框架最快的方式就是查看官方文档,因为官方文档中一般都会给出一个Demo,果不其然,Lottie也是!文档的阅读量不是很大,通篇下来介绍了:

    • 播放本地Assets目录下的Json动画文件
    • 通过Json数据播放动画
    • 如何对动画进行监听以及动画进度调节
    • Lottie动画数据的预加载和缓存
    • 为Assets目录下的Json动画文件配置动画所需要的素材

    3. 开始入坑

    然而,他介绍了这么多,并没有一款适合我的。因为服务器下发不是简单的Json数据,是一个动画压缩包,里面包括了动画文件和播放动画需要的素材文件,而且解压后的文件也不在Asset目录下。于是,只好跟踪animationView.setAnimation("hello-world.json")源码,看看最终到底做了什么事!

      public void setAnimation(String animationName) {
        setAnimation(animationName, defaultCacheStrategy);
      }
    

    一个参数调用两个参数同名方法,只好接着往下看!

      public void setAnimation(final String animationName, final CacheStrategy cacheStrategy) {
        this.animationName = animationName;
        if (weakRefCache.containsKey(animationName)) {
          WeakReference<LottieComposition> compRef = weakRefCache.get(animationName);
          if (compRef.get() != null) {
            setComposition(compRef.get());
            return;
          }
        } else if (strongRefCache.containsKey(animationName)) {
          setComposition(strongRefCache.get(animationName));
          return;
        }
    
        this.animationName = animationName;
        lottieDrawable.cancelAnimation();
        cancelLoaderTask();
        compositionLoader = LottieComposition.Factory.fromAssetFileName(getContext(), animationName,
            new OnCompositionLoadedListener() {
              @Override
              public void onCompositionLoaded(LottieComposition composition) {
                if (cacheStrategy == CacheStrategy.Strong) {
                  strongRefCache.put(animationName, composition);
                } else if (cacheStrategy == CacheStrategy.Weak) {
                  weakRefCache.put(animationName, new WeakReference<>(composition));
                }
    
                setComposition(composition);
              }
            });
      }
    

    从这里可以看到官方文档中说的缓存,包括强引用缓存,弱引用缓存,和无缓存模式,而且知道Json动画文件最终会转化为Composition对象,而Compostion对象是通过LottieComposition.Factory.fromAssetFileName(...)的方法异步获取的,于是我们只好接着往下跟踪。

     public static Cancellable fromAssetFileName(Context context, String fileName,
            OnCompositionLoadedListener loadedListener) {
          InputStream stream;
          try {
            stream = context.getAssets().open(fileName);
          } catch (IOException e) {
            throw new IllegalStateException("Unable to find file " + fileName, e);
          }
          return fromInputStream(context, stream, loadedListener);
        }
    

    看到这里我们这就明白,当初传入的文件名,最终还是通过getAssets().open(fileName)的方法,以流的方式进行处理了,于是我们可以这样加载放在其他目录下的Json动画文件。

     public static void loadAnimationByFile(File file, final OnLoadAnimationListener listener) {
            if (file == null || !file.exists()) {
                if (listener != null) {
                    listener.onFinished(null);
                }
                return;
            }
            FileInputStream fins = null;
            try {
                fins = new FileInputStream(file);
                LottieComposition.Factory.fromInputStream(GlobalContext.getAppContext(), fins, new OnCompositionLoadedListener() {
                    @Override
                    public void onCompositionLoaded(LottieComposition composition) {
                        if (listener != null) {
                            listener.onFinished(composition);
                        }
                    }
                });
            } catch (IOException e) {
                e.printStackTrace();
                if (listener != null) {
                    listener.onFinished(null);
                }
                if (fins != null) {
                    try {
                        fins.close();
                    } catch (IOException e1) {
                        e1.printStackTrace();
                    }
                }
            }
        }
    

    异步的方式获取Composition对象,因为不使用setAnimation(final String animationName, final CacheStrategy cacheStrategy)方法,所以我们没法使用框架提供的缓存,为了下次播放时不需要重新解析动画文件,使动画的加载速度更快,我们也需要重新做一套缓冲处理,如下

     LocalLottieAnimUtil.loadAnimationByFile(animFile, new LocalLottieAnimUtil.OnLoadAnimationListener() {
         @Override
        public void onFinished(LottieComposition lottieComposition) {
               if (lottieComposition != null) {
                     mCenter.putLottieComposition(id, lottieComposition);  // 使用
                } else {
                    GiftFileUtils.deleteFile(getAnimFolder(link));  //删除动画文件目录,省的下次加载依然失败,而是重新去下载资源压缩包
             }
    
    
    public class EnterRoomResCenter {
        private SparseArray<LottieComposition> lottieCompositions = new SparseArray<>();  //缓存Composition
     
        public void putLottieComposition(int id, LottieComposition composition) {
            lottieCompositions.put(id, composition);
        }
    
        public LottieComposition getAnimComposition(int id) {
            return mCenter.getLottieComposition(id);
        }
    }
    

    完成了Json动画文件的加载,接下来就是播放动画。正如源码方法中 setAnimation(final String animationName, final CacheStrategy cacheStrategy) 一样,我们也需要对LottieAnimationView进行setComposition(composition)处理,然后调用LottieAnimationView.playAnimation()就可以进行动画播放了,于是我这样做了:

      public static void playAnimation(LottieAnimationView animationView,LottieComposition composition) {
            animationView.setComposition(composition);
            animationView.playAnimation();
        }
    

    想想这个需求马上就要搞定,于是我抿抿嘴偷偷笑了,这也太轻松了吧!于是端起茶杯去接了杯水,并运行了项目,准备回来看到那绚丽的动画。然而,事与愿违,等待我的是一片血红的“大姨妈”。

    java.lang.IllegalStateException: 
    You must set an images folder before loading an image. Set it with LottieComposition#setImagesFolder or LottieDrawable#setImagesFolder
    

    看到这个错误,想起官方文档上面有说,如何为动画配置播放动画所需要的素材,而且错误提示也特别的明显,看了看给的资源包的目录,似乎发现了什么!于是我按照官方《为Assets目录下的Json动画文件设置播放动画所需要的资源》一样,改了一下代码:


    动画资源层级.PNG
      public static void playAnimation(LottieAnimationView animationView,String imageFolder, LottieComposition composition) {
            animationView.setComposition(composition);
           animationView.setImageAssetsFolder(imageFolder);   // 新添加的
            animationView.playAnimation();
        }
    

    想着异常信息都提示这么明显了,而且官方文档给的模板也是这样写的,我更加确定这次动画播放绝对的没有问题。然而,动画最终还是没有播放出来!没办法,只好继续翻源码,既然Assets目录下setImageAssetsFolder(String folder)能生效,那我们只好从这个方法切入,看看folder变量最终是怎么样被使用的。

      @SuppressWarnings("WeakerAccess") public void setImageAssetsFolder(String imageAssetsFolder) {
        lottieDrawable.setImagesAssetsFolder(imageAssetsFolder);
      }
    

    没有什么头绪只好继续往下看:

     @SuppressWarnings("WeakerAccess") public void setImagesAssetsFolder(@Nullable String imageAssetsFolder) {
        this.imageAssetsFolder = imageAssetsFolder;
      }
    

    这个变量被设置成类属性了,那么我们只需要在这个类下搜索怎么样被使用就可以马上定位出原因,发现有这么一行:

     imageAssetBitmapManager = new ImageAssetBitmapManager(getCallback(),
              imageAssetsFolder, imageAssetDelegate, composition.getImages());
        }
    

    我擦,变量被传递到一个ImageAssetBitmapManager对象里面去了,只好进这个类继续跟踪,最终定位到这样一个方法:

    Bitmap bitmapForId(String id) {
        Bitmap bitmap = bitmaps.get(id);
        if (bitmap == null) {
          LottieImageAsset imageAsset = imageAssets.get(id);
          if (imageAsset == null) {
            return null;
          }
          if (assetDelegate != null) {
            bitmap = assetDelegate.fetchBitmap(imageAsset);
            bitmaps.put(id, bitmap);
            return bitmap;
          }
    
          InputStream is;
          try {
            if (TextUtils.isEmpty(imagesFolder)) {
              throw new IllegalStateException("You must set an images folder before loading an image." +
                  " Set it with LottieComposition#setImagesFolder or LottieDrawable#setImagesFolder");
            }
            is = context.getAssets().open(imagesFolder + imageAsset.getFileName());
          } catch (IOException e) {
            Log.w(L.TAG, "Unable to open asset.", e);
            return null;
          }
          BitmapFactory.Options opts = new BitmapFactory.Options();
          opts.inScaled = true;
          opts.inDensity = 160;
          bitmap = BitmapFactory.decodeStream(is, null, opts);
          bitmaps.put(id, bitmap);
        }
        return bitmap;
      }
    

    播放动画所需要的图片资源都通过这个方法获取,传入一个图片文件名称,然后通过流获取Bitmap对象并返回。这里需要介绍一下:
    如果Json动画文件使用了图片素材,里面的Json数据必然会声明该图片文件名。在Composition.Factory进行解析为Composition时,里面使用的图片都以键值对的方式存放到Composition的
    private final Map<String, LottieImageAsset> images = new HashMap<>()中,LottieAnimationView.setCompostion(Compostion)最终落实到LottieDrawable.setCompostion(Compostion),LottieDrawable为了获取动画里面的bitmap对象,Lottie框架封装了ImageAssetBitmapManager对象,在LottieDrawable中创建,将图片的获取转移到imageAssetBitmapManager 中,并暴露public Bitmap bitmapForId(String id)的方法。

    LottieImageAsset imageAsset = imageAssets.get(id);
    

    上面的 bitmapForId(String id) 方法体中有这么一行代码,如上,之前Json动画文件解析的图片都存放到imageAssets中,id是当前需要加载的图片素材名,通过get获取到对应的LottieImageAsset对象,其实里面也就包装了该id值,做这层包装可能为了以后方便扩展吧!

    
          if (assetDelegate != null) {
            bitmap = assetDelegate.fetchBitmap(imageAsset);
            bitmaps.put(id, bitmap);
            return bitmap;
          }
         ...
          is = context.getAssets().open(imagesFolder + imageAsset.getFileName());
         bitmap = BitmapFactory.decodeStream(is, null, opts);
         return bitmap;
        ...
      
    

    同样从 bitmapForId(String id) 方法体中提取出如上代码,从上面可以看出如果assetDelegate == null,它就会从Asset的imagesFolder目录下找素材文件。因为之前我们并没有设置过assetDelegate,而且我们的素材并不是在Asset的imagesFolder目录下,所以获取不到bitmap对象,动画无法播放也是情有可原的,不断的反向追溯assetDelegate来源,找到LottieAnimationView.setImageAssetDelegate(ImageAssetDelegate assetDelegate)方法,所以调整之前的代码,如下:

    public static ImageAssetDelegate imageAssetDelegate = new ImageAssetDelegate() {
            @Override
            public Bitmap fetchBitmap(LottieImageAsset asset) {
                String filePath = currentImgFolder + File.separator + asset.getFileName();
                return BitmapFactory.decodeFile(filePath, opts);
            }
        }
        public static void playAnimation(LottieAnimationView animationView, String imageFolder, ImageAssetDelegate imageAssetDelegate, LottieComposition composition) {
            if (animationView == null || composition == null) {
                return;
            }
            animationView.setComposition(composition);
            animationView.setImageAssetsFolder(imageFolder);
            animationView.setImageAssetDelegate(imageAssetDelegate);
            animationView.playAnimation();
        }
    

    到现在为此,这个动画才能播放出来,这个地方有一点比较坑的就是ImageAssetDelegate的创建:

    public static ImageAssetDelegate imageAssetDelegate = new ImageAssetDelegate() {
            @Override
            public Bitmap fetchBitmap(LottieImageAsset asset) {
                String filePath = currentImgFolder + File.separator + asset.getFileName();
                return BitmapFactory.decodeFile(filePath, opts);
            }
        }
    

    每次使用的时候,我们都需要有这样一个currentImgFolder 变量,维护这个文件所在的父目录的位置,其实框架大可以在ImageAssetBitmapManager中这样调用,将之前我们用setImageFolder(String folder)又重新的回调回来。

    if (assetDelegate != null) {
            bitmap = assetDelegate.fetchBitmap(imagesFolder, imageAsset);    // imagesFolder是新加
            bitmaps.put(id, bitmap);
            return bitmap;
          }
    

    4. Lottie坑点总结

    • 在动画json文件中,有如下类似的数据,其中W 和 H字段声明了整个动画的输出大小,你需要确保你使用的LottieAnimationVIew的宽高比和这个一致。
    {"v":"4.9.0","fr":25,"ip":0,"op":50,"w":1242,"h":128,"nm":"WWW","ddd":0,"assets": ....
    
    • 播放本地动画文件展示的动画偏小或偏大

    注意ImageAssetDelegate的fetBitmap()代码中indensity属性的设置

        @Override
        public Bitmap fetchBitmap(LottieImageAsset asset) {
            String filePath = currentImgFolder + File.separator + asset.getFileName();
            BitmapFactory.Options opts = new BitmapFactory.Options();
            opts.inDensity = 110;                                                                 //请留意这个值的设定
            return BitmapFactory.decodeFile(filePath, opts);                                     //这里还有坑,请往下接着看
        }
    
    • Lottie库回收素材图片bitmap引发的空指针问题
      (1) 先看看Lottie对素材图片进行缓存的方法:
    Bitmap bitmapForId(String id) {
          ...
          if (assetDelegate != null) {
            bitmap = assetDelegate.fetchBitmap(imageAsset);
            bitmaps.put(id, bitmap);                       //将Bitmap进行存储,可能Bitmap对象为null
            return bitmap;
          }
          ...
          BitmapFactory.Options opts = new BitmapFactory.Options();
          opts.inScaled = true;
          opts.inDensity = 160;
          bitmap = BitmapFactory.decodeStream(is, null, opts);
          bitmaps.put(id, bitmap);                         //将Bitmap进行存储,可能Bitmap对象为null
        }
        return bitmap;
      }
    

    (2) 再看看Lottie对缓存图片的回收处理:

      void recycleBitmaps() {
        Iterator<Map.Entry<String, Bitmap>> it = bitmaps.entrySet().iterator();
        while (it.hasNext()) {
          Map.Entry<String, Bitmap> entry = it.next();
          entry.getValue().recycle();
          it.remove();
        }
      }
    

    (3) 结论: 前后对比,有没有发现Lottie对缓存的素材图片bitmap对象并没有做判空处理,就直接回收了(Version 1.5.3)。

    解决办法: 如果是加载本地素材图片(非Assets目录)可以采用如下办法:

      public Bitmap fetchBitmap(LottieImageAsset asset) {
            String filePath = currentImgFolder + File.separator + asset.getFileName();
            Bitmap bitmap = BitmapFactory.decodeFile(filePath, opts);
            if (bitmap == null) {
                bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8);
            }
            return bitmap;
        }
    

    5. 使用总结

    • 播放放置在Asset目录下的动画文件

    设置播放文件: setAnimation("文件名")
    如果动画文件带素材: setImageAssetsFolder("文件夹名")

    • 播放系统目录下的动画文件

    异步获取Compostion对象: LottieComposition.Factory.fromInputStream()
    设置播放的素材: setComposition(composition)
    如果动画文件带素材: setImageAssetsFolder("文件夹名") + setImageAssetDelegate(imageAssetDelegate)

    相关文章

      网友评论

      • Gxinyu:楼主帮我看看,我资源文件都是放在assets下面的,只用这俩方法就可以加载吗
        animationView.setAnimation("home.json");
        animationView.setImageAssetsFolder("Images");
        负了时光不负卿:@Gxinyu 加我QQ吧,1004145468
        Gxinyu:@负了时光不负卿 animationView.setImageAssetsFolder("lsj");
        animationView.setAnimation("data.json");
        animationView.loop(true);
        animationView.playAnimation();
        这样写了还是报那个错误
        负了时光不负卿:对,有什么问题么?
      • martin25:您好,我碰到個問題。我的Json檔案會讀取assets 裡面的image
        第一次讀取都正常。但是我退出activity之後再進來會崩潰。
        com.airbnb.lottie.LottieImageAsset.getBitmap()會報空指針。
        請問這該怎麼處理?
        感謝大佬~
        负了时光不负卿:@martin25 请问你是通过哪种方式加载的,第一次lottie加载json会生成composition对象,同时把json里面涉及图片缓存好。确保退出界面后composition对象置空,否则复用后的composition对象可能会获取不到图片,因为图片软引用在内存
        martin25:@负了时光不负卿 2.6.0-beta19
        负了时光不负卿:@martin25 你的Lottie版本是多少?
      • rivenlee:大佬,在吗,我这有个BUG,在dialogfragment里加载lottie, dialogfragment调用dismiss()函数就会报空指针,方便的话加我下QQ 741547004
        负了时光不负卿:@rivenlee 👌
      • 186a4c612120:楼主,我按照你的方法写了但是动画加载不出来,能给一下源码的地址码?
        186a4c612120:问题有点紧急,如果可以的话加我qq1083900283
        186a4c612120:@负了时光不负卿
        loadAnimationByFile(new File(GetLocalResourceUtil.getEssayStarAnimJsonDir(imgDir, jsonName)),
        new LocalLottieAnimUtil.OnLoadAnimationListener() {
        @Override
        public void onFinished(LottieComposition composition) {
        view.setComposition(composition);
        }
        });
        view.setImageAssetsFolder(GetLocalResourceUtil.getEssayStarAnimImgDir(imgDir));
        view.setImageAssetDelegate(LocalLottieAnimUtil.getImageAssetDelegate(GetLocalResourceUtil.getEssayStarAnimImgDir(imgDir)));
        我按照你说的方式设置了本地的json和image目录但是动画播放不出来。
        负了时光不负卿:@找珍珠的Captain 公司代码不能透露,可以的话,能帮你过一下问题
      • Vivi成长吧:加载出来的动画好大呀
        负了时光不负卿:@JulyMiracle 如果是app内嵌图片直接放在Assert目录下就好了,没有必要自己重新用bitmapfactory加载图片,还有你可以将布局中的宽高直接设定为40dp * 40dp
        Vivi成长吧:@负了时光不负卿 布局里宽高使用的是wrap_content,json文件里"w":80,"h":80 bitmap宽高打印出来是72,72 显示出来时就很大 设置了inDensity也不管用呢 Bitmap bitmap = BitmapFactory.decodeStream(is, null, opts); 跟这个decodeStream方法有关系么?
        负了时光不负卿:@JulyMiracle 首先看一下你设定的lottieAnimationview的大小是不是设定错了,让后再看一下.json文件中w h两个字段是不是偏大
      • PonyCui:这么多坑,要不你试试 SVGA 动画,http://svga.io
      • 间歇性丶神经病患者:坑坑的,上次和公司的美工妹子折腾了快3天才弄了出来... 不过确实比写原生好多了。收藏马克,楼主加油
        负了时光不负卿:@间歇性丶神经病患者 希望能对后面入坑的有些帮助:smile::smile:
      • MISSGY_:没有demo或者源码吗
        负了时光不负卿:@可我就是爱她啊 哈哈,我讲得是思路,能看的懂的
        MISSGY_:@涯上月灬指香 没 想下你的demo跑一下再按你的文章看看
        负了时光不负卿:@可我就是爱她啊 官网上面有简单的demo,这里讲的注意点,遇到什么问题了吗?

      本文标题:Android•Lottie动画库填坑记

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