欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Lottie动画

程序员文章站 2024-03-26 11:41:17
...

lottie的使用通读一遍官方文档基本就可以拿来用了,也可以看看我之前的Lottie使用;但今天要说的是另外一种情况,就是通过网络拿到动画资源的zip包,再来加载动画;
使用场景就是,zip包中含有动画的json文件以及动画需要的素材文件;
首先估计下会遇到的问题可能有如下:

  • json文件和素材文件都放在Asset目录下,LottieAnimationView可以直接加载,那么如何加载指定目录的资源(为解决加载网络资源).
  • 通过网络拿到的动画zip包怎么拆分出动画json文件和素材文件

解决第一个问题,我好奇的点进了setAnimation这个方法,想知道LottieAnimationView是如何把指定json文件转为动画实现的,发现它只是调用了同名方法setAnimation(animationName, defaultCacheStrategy)
通过参数,可以看出第二个参数是缓存策略;接着往下看:

 /**
   * Sets the animation from a file in the assets directory.
   * This will load and deserialize the file asynchronously.
   * <p>
   * You may also specify a cache strategy. Specifying {@link CacheStrategy#Strong} will hold a
   * strong reference to the composition once it is loaded
   * and deserialized. {@link CacheStrategy#Weak} will hold a weak reference to said composition.
   */
public void setAnimation(final String animationName, final CacheStrategy cacheStrategy) {
    this.animationName = animationName;
    animationResId = 0;
    if (ASSET_WEAK_REF_CACHE.containsKey(animationName)) {
      WeakReference<LottieComposition> compRef = ASSET_WEAK_REF_CACHE.get(animationName);
      LottieComposition ref = compRef.get();
      if (ref != null) {
        setComposition(ref);
        return;
      }
    } else if (ASSET_STRONG_REF_CACHE.containsKey(animationName)) {
      setComposition(ASSET_STRONG_REF_CACHE.get(animationName));
      return;
    }

    clearComposition();
    cancelLoaderTask();
    compositionLoader = LottieComposition.Factory.fromAssetFileName(getContext(), animationName,
        new OnCompositionLoadedListener() {
          @Override public void onCompositionLoaded(LottieComposition composition) {
            if (cacheStrategy == CacheStrategy.Strong) {
              ASSET_STRONG_REF_CACHE.put(animationName, composition);
            } else if (cacheStrategy == CacheStrategy.Weak) {
              ASSET_WEAK_REF_CACHE.put(animationName, new WeakReference<>(composition));
            }

            setComposition(composition);
          }
        });
  }

可以看到缓存策略相关包括强引用缓存,弱引用缓存,和无缓存模式,Json动画文件最终会通过LottieComposition.Factory.fromAssetFileName异步转化为Composition对象;继续跟进这个方法:

   /**
     * Loads a composition from a file stored in /assets.
     */
    public static Cancellable fromAssetFileName(
        Context context, String fileName, OnCompositionLoadedListener listener) {
      InputStream stream;
      try {
        stream = context.getAssets().open(fileName);
      } catch (IOException e) {
        throw new IllegalArgumentException("Unable to find file " + fileName, e);
      }
      return fromInputStream(stream, listener);
    }

通过context.getAssets().open(fileName);将传入的文件以流的方式处理,跟到这里,估计就知道如何加载其他目录的文件。
通过流的方式拿到指定目录,并通过LottieComposition.Factory.fromAssetFileName创建Composition对象,然后再set给LottieAnimationView;
在加载包含素材文件的动画文件的时候,需要注意一点,稍不注意,就一定会遇到下面这个错误:

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

怎么解决尼?得看你是在那种场景遇到的这个问题,

  • 如果是本地加载,且动画文件包和素材文件都在assets下的时候,只需要在调用playAnimation之前调用setImageAssetsFolder("你的素材文件名")
  • 本地加载,动画文件和素材文件不在assets下的时候,那么就有点复杂了;
    在第二种情况的时候,跟进setImageAssetsFolder方法;
 public void setImageAssetsFolder(String imageAssetsFolder) {
    lottieDrawable.setImagesAssetsFolder(imageAssetsFolder);
  }

继续,

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

将其赋值给了imageAssetsFolder,查看imageAssetsFolder的引用处,

private ImageAssetManager getImageAssetManager() {
    if (getCallback() == null) {
      // We can't get a bitmap since we can't get a Context from the callback.
      return null;
    }

    if (imageAssetManager != null && !imageAssetManager.hasSameContext(getContext())) {
      imageAssetManager.recycleBitmaps();
      imageAssetManager = null;
    }

    if (imageAssetManager == null) {
      imageAssetManager = new ImageAssetManager(getCallback(),
          imageAssetsFolder, imageAssetDelegate, composition.getImages());
    }

    return imageAssetManager;
  }

被当做参数传入了ImageAssetManager,跟进这个类,继续查找,最终发现被用到的地方

@Nullable public Bitmap bitmapForId(String id) {
    Bitmap bitmap = bitmaps.get(id);
    if (bitmap != null) {
      return bitmap;
    }

    LottieImageAsset imageAsset = imageAssets.get(id);
    if (imageAsset == null) {
      return null;
    }

    if (delegate != null) {
      bitmap = delegate.fetchBitmap(imageAsset);
      if (bitmap != null) {
        putBitmap(id, bitmap);
      }
      return bitmap;
    }

    String filename = imageAsset.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) {
        Log.w(L.TAG, "data URL did not have correct base64 format.", e);
        return null;
      }
      bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, opts);
      return putBitmap(id, 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 + filename);
    } catch (IOException e) {
      Log.w(L.TAG, "Unable to open asset.", e);
      return null;
    }
    bitmap = BitmapFactory.decodeStream(is, null, opts);
    return putBitmap(id, 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)拿到之前的assets,Json动画文件解析的图片都存放到imageAssets中,id是当前需要加载的图片素材名,通过get获取到对应的LottieImageAsset对象;
通过

   if (delegate != null) {
      bitmap = delegate.fetchBitmap(imageAsset);
      if (bitmap != null) {
        putBitmap(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 + filename);
    } catch (IOException e) {
      Log.w(L.TAG, "Unable to open asset.", e);
      return null;
    }

可以看出,当delegate == null的情况下,它会从Asset的imagesFolder目录下找素材文件。但是我们如果没有设置assetDelegate,而我们加载的动画json为其他目录,素材也并不是在Asset的imagesFolder目录下,所以就获取不到bitmap对象,也就会看到上面哪个错误

throw new IllegalStateException("You must set an images folder before loading an image." +" Set it with LottieComposition#setImagesFolder or LottieDrawable#setImagesFolder")**;

现在我们就需要知道ImageAssetDelegate是个什么东西了,发现在LottieDrawable中有这样一个方法

 public void setImageAssetDelegate(
      @SuppressWarnings("NullableProblems") ImageAssetDelegate assetDelegate) {
    this.imageAssetDelegate = assetDelegate;
    if (imageAssetManager != null) {
      imageAssetManager.setDelegate(assetDelegate);
    }
  }

继续查看引用,会在熟悉的LottieAnimationView中发现

 public void setImageAssetDelegate(ImageAssetDelegate assetDelegate) {
    lottieDrawable.setImageAssetDelegate(assetDelegate);
  }

我们可以看到ImageAssetDelegate是一个接口

public interface ImageAssetDelegate {
  Bitmap fetchBitmap(LottieImageAsset asset);
}

因此可以在调用setImageAssetsFolder的时候,调用setImageAssetDelegate,传入实现的ImageAssetDelegate,即可解决上面的第二种情况遇到的问题;

加载网络zip包的动画文件方式

最简单的是不含素材文件的zip包:

其次是包含素材文件的zip包:

发表点一些可能遇到的问题:

  • 空指针问题:
@Nullable public Bitmap bitmapForId(String id) {
    Bitmap bitmap = bitmaps.get(id);
    if (bitmap != null) {
      return bitmap;
    }

    LottieImageAsset imageAsset = imageAssets.get(id);
    if (imageAsset == null) {
      return null;
    }

    if (delegate != null) {
      bitmap = delegate.fetchBitmap(imageAsset);
      if (bitmap != null) {
        putBitmap(id, bitmap);
      }
      return bitmap;
    }

    String filename = imageAsset.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) {
        Log.w(L.TAG, "data URL did not have correct base64 format.", e);
        return null;
      }
      bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, opts);
      return putBitmap(id, 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 + filename);
    } catch (IOException e) {
      Log.w(L.TAG, "Unable to open asset.", e);
      return null;
    }
    bitmap = BitmapFactory.decodeStream(is, null, opts);
    return putBitmap(id, bitmap);
  }

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

在回收的时候,就会爆发;我们可以在加载的时候自己给兜住;

  • 动画View的设置,在LottieAnimationView设置宽高的时候应该遵循json文件中的宽高比;
  • 加载自己指定的文件动画时候,可能出现动画偏大或者偏小
    可以在设置
    通过设置ImageAssetDelegate的时候
@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);                                     
    }

改变opts.inDensity的值;