blob: 98915f02a696c530faf735cdbef2cd104ac51971 [file] [log] [blame]
package com.airbnb.lottie;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.util.LongSparseArray;
import android.view.View;
import android.view.animation.LinearInterpolator;
import java.util.ArrayList;
import java.util.List;
/**
* This can be used to show an lottie animation in any place that would normally take a drawable.
* If there are masks or mattes, then you MUST call {@link #recycleBitmaps()} when you are done
* or else you will leak bitmaps.
* <p>
* It is preferable to use {@link com.airbnb.lottie.LottieAnimationView} when possible because it
* handles bitmap recycling and asynchronous loading
* of compositions.
*/
public class LottieDrawable extends AnimatableLayer implements Drawable.Callback {
private LottieComposition composition;
private final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
private float speed = 1f;
private final CanvasPool canvasPool = new CanvasPool();
@Nullable private ImageAssetBitmapManager imageAssetBitmapManager;
@Nullable private String imageAssetsFolder;
private boolean playAnimationWhenLayerAdded;
private boolean reverseAnimationWhenLayerAdded;
private boolean systemAnimationsAreDisabled;
@SuppressWarnings("WeakerAccess") public LottieDrawable() {
super(null);
animator.setRepeatCount(0);
animator.setInterpolator(new LinearInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override public void onAnimationUpdate(ValueAnimator animation) {
if (systemAnimationsAreDisabled) {
animator.cancel();
setProgress(1f);
} else {
setProgress((float) animation.getAnimatedValue());
}
}
});
}
/**
* Returns whether or not any layers in this composition has masks.
*/
@SuppressWarnings("unused") public boolean hasMasks() {
for (AnimatableLayer layer : layers) {
if (!(layer instanceof LayerView)) {
continue;
}
if (((LayerView) layer).hasMasks()){
return true;
}
}
return false;
}
/**
* Returns whether or not any layers in this composition has a matte layer.
*/
@SuppressWarnings("unused") public boolean hasMatte() {
for (AnimatableLayer layer : layers) {
if (!(layer instanceof LayerView)) {
continue;
}
if (((LayerView) layer).hasMatte()){
return true;
}
}
return false;
}
/**
* If you use image assets, you must explicitly specify the folder in assets/ in which they are
* located because bodymovin uses the name filenames across all compositions (img_#).
* Do NOT rename the images themselves.
*
* If your images are located in src/main/assets/airbnb_loader/ then call
* `setImageAssetsFolder("airbnb_loader/");`.
*
*
* If you use LottieDrawable directly, you MUST call {@link #recycleBitmaps()} when you
* are done. Calling {@link #recycleBitmaps()} doesn't have to be final and {@link LottieDrawable}
* will recreate the bitmaps if needed but they will leak if you don't recycle them.
*/
@SuppressWarnings("WeakerAccess") public void setImagesAssetsFolder(@Nullable String imageAssetsFolder) {
this.imageAssetsFolder = imageAssetsFolder;
}
/**
* If you have image assets and use {@link LottieDrawable} directly, you must call this yourself.
*
* Calling {@link #recycleBitmaps()} doesn't have to be final and {@link LottieDrawable}
* will recreate the bitmaps if needed but they will leak if you don't recycle them.
*
*/
@SuppressWarnings("WeakerAccess") public void recycleBitmaps() {
canvasPool.recycleBitmaps();
if (imageAssetBitmapManager != null) {
imageAssetBitmapManager.recycleBitmaps();
}
}
/**
* @return True if the composition is different from the previously set composition, false otherwise.
*/
boolean setComposition(LottieComposition composition) {
if (getCallback() == null) {
throw new IllegalStateException(
"You or your view must set a Drawable.Callback before setting the composition. This " +
"gets done automatically when added to an ImageView. " +
"Either call ImageView.setImageDrawable() before setComposition() or call " +
"setCallback(yourView.getCallback()) first.");
}
if (this.composition == composition) {
return false;
}
clearComposition();
this.composition = composition;
setSpeed(speed);
setBounds(0, 0, composition.getBounds().width(), composition.getBounds().height());
buildLayersForComposition(composition);
setProgress(getProgress());
return true;
}
private void clearComposition() {
recycleBitmaps();
clearLayers();
imageAssetBitmapManager = null;
}
private void buildLayersForComposition(LottieComposition composition) {
if (composition == null) {
throw new IllegalStateException("Composition is null");
}
LongSparseArray<LayerView> layerMap = new LongSparseArray<>(composition.getLayers().size());
List<LayerView> layers = new ArrayList<>(composition.getLayers().size());
LayerView mattedLayer = null;
for (int i = composition.getLayers().size() - 1; i >= 0; i--) {
Layer layer = composition.getLayers().get(i);
LayerView layerView;
layerView = new LayerView(layer, composition, this, canvasPool);
layerMap.put(layerView.getId(), layerView);
if (mattedLayer != null) {
mattedLayer.setMatteLayer(layerView);
mattedLayer = null;
} else {
layers.add(layerView);
if (layer.getMatteType() == Layer.MatteType.Add) {
mattedLayer = layerView;
} else if (layer.getMatteType() == Layer.MatteType.Invert) {
mattedLayer = layerView;
}
}
}
for (int i = 0; i < layers.size(); i++) {
LayerView layerView = layers.get(i);
addLayer(layerView);
}
for (int i = 0; i < layerMap.size(); i++) {
long key = layerMap.keyAt(i);
LayerView layerView = layerMap.get(key);
LayerView parentLayer = layerMap.get(layerView.getLayerModel().getParentId());
if (parentLayer != null) {
layerView.setParentLayer(parentLayer);
}
}
}
@Override public void invalidateSelf() {
final Callback callback = getCallback();
if (callback != null) {
callback.invalidateDrawable(this);
}
}
@Override public void draw(@NonNull Canvas canvas) {
if (composition == null) {
return;
}
Rect bounds = getBounds();
Rect compBounds = composition.getBounds();
int saveCount = canvas.save();
if (!bounds.equals(compBounds)) {
float scaleX = bounds.width() / (float) compBounds.width();
float scaleY = bounds.height() / (float) compBounds.height();
canvas.scale(scaleX, scaleY);
}
canvas.clipRect(getBounds());
super.draw(canvas);
canvas.restoreToCount(saveCount);
}
void systemAnimationsAreDisabled() {
systemAnimationsAreDisabled = true;
}
void loop(boolean loop) {
animator.setRepeatCount(loop ? ValueAnimator.INFINITE : 0);
}
boolean isLooping() {
return animator.getRepeatCount() == ValueAnimator.INFINITE;
}
boolean isAnimating() {
return animator.isRunning();
}
void playAnimation() {
if (layers.isEmpty()) {
playAnimationWhenLayerAdded = true;
reverseAnimationWhenLayerAdded = false;
return;
}
animator.setCurrentPlayTime((long) (getProgress() * animator.getDuration()));
animator.start();
}
void reverseAnimation() {
if (layers.isEmpty()) {
playAnimationWhenLayerAdded = false;
reverseAnimationWhenLayerAdded = true;
return;
}
animator.setCurrentPlayTime((long) (getProgress() * animator.getDuration()));
animator.reverse();
}
void setSpeed(float speed) {
this.speed = speed;
if (speed < 0) {
animator.setFloatValues(1f, 0f);
} else {
animator.setFloatValues(0f, 1f);
}
if (composition != null) {
animator.setDuration((long) (composition.getDuration() / Math.abs(speed)));
}
}
void cancelAnimation() {
playAnimationWhenLayerAdded = false;
reverseAnimationWhenLayerAdded = false;
animator.cancel();
}
@Override
void addLayer(AnimatableLayer layer) {
super.addLayer(layer);
if (playAnimationWhenLayerAdded) {
playAnimationWhenLayerAdded = false;
playAnimation();
}
if (reverseAnimationWhenLayerAdded) {
reverseAnimationWhenLayerAdded = false;
reverseAnimation();
}
}
void addAnimatorUpdateListener(ValueAnimator.AnimatorUpdateListener updateListener) {
animator.addUpdateListener(updateListener);
}
void removeAnimatorUpdateListener(ValueAnimator.AnimatorUpdateListener updateListener) {
animator.removeUpdateListener(updateListener);
}
void addAnimatorListener(Animator.AnimatorListener listener) {
animator.addListener(listener);
}
void removeAnimatorListener(Animator.AnimatorListener listener) {
animator.removeListener(listener);
}
@Override public int getIntrinsicWidth() {
return composition == null ? -1 : composition.getBounds().width();
}
@Override public int getIntrinsicHeight() {
return composition == null ? -1 : composition.getBounds().height();
}
Bitmap getImageAsset(String id) {
return getImageAssetBitmapManager().bitmapForId(id);
}
private ImageAssetBitmapManager getImageAssetBitmapManager() {
if (imageAssetBitmapManager != null && !imageAssetBitmapManager.hasSameContext(getContext())) {
imageAssetBitmapManager.recycleBitmaps();
imageAssetBitmapManager = null;
}
if (imageAssetBitmapManager == null) {
imageAssetBitmapManager = new ImageAssetBitmapManager(getCallback(),
imageAssetsFolder, composition.getImages());
}
return imageAssetBitmapManager;
}
private @Nullable Context getContext() {
Callback callback = getCallback();
if (callback == null) {
return null;
}
if (callback instanceof View) {
return ((View) callback).getContext();
}
return null;
}
/**
* These Drawable.Callback methods proxy the calls so that this is the drawable that is
* actually invalidated, not a child one which will not pass the view's validateDrawable check.
*/
@Override public void invalidateDrawable(Drawable who) {
Callback callback = getCallback();
if (callback == null) {
return;
}
callback.invalidateDrawable(this);
}
@Override public void scheduleDrawable(Drawable who, Runnable what, long when) {
Callback callback = getCallback();
if (callback == null) {
return;
}
callback.scheduleDrawable(this, what, when);
}
@Override public void unscheduleDrawable(Drawable who, Runnable what) {
Callback callback = getCallback();
if (callback == null) {
return;
}
callback.unscheduleDrawable(this, what);
}
}