blob: 6375d1a63790ab5fafb431b048720602682b9536 [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.ColorFilter;
import android.graphics.Matrix;
import android.graphics.PixelFormat;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.annotation.FloatRange;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.View;
import android.view.animation.LinearInterpolator;
/**
* 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 Drawable implements Drawable.Callback {
private static final String TAG = LottieDrawable.class.getSimpleName();
private final Matrix matrix = new Matrix();
private LottieComposition composition;
private final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
private float speed = 1f;
private float scale = 1f;
private float progress = 0f;
@Nullable private ImageAssetBitmapManager imageAssetBitmapManager;
@Nullable private String imageAssetsFolder;
@Nullable private ImageAssetDelegate imageAssetDelegate;
private boolean playAnimationWhenCompositionAdded;
private boolean reverseAnimationWhenCompositionAdded;
private boolean systemAnimationsAreDisabled;
private boolean enableMergePaths;
@Nullable private CompositionLayer compositionLayer;
private int alpha = 255;
@SuppressWarnings("WeakerAccess") public LottieDrawable() {
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", "WeakerAccess"}) public boolean hasMasks() {
return compositionLayer != null && compositionLayer.hasMasks();
}
/**
* Returns whether or not any layers in this composition has a matte layer.
*/
@SuppressWarnings({"unused", "WeakerAccess"}) public boolean hasMatte() {
return compositionLayer != null && compositionLayer.hasMatte();
}
boolean enableMergePathsForKitKatAndAbove() {
return enableMergePaths;
}
/**
* Enable this to get merge path support for devices running KitKat (19) and above.
*
* Merge paths currently don't work if the the operand shape is entirely contained within the
* first shape. If you need to cut out one shape from another shape, use an even-odd fill type
* instead of using merge paths.
*/
@SuppressWarnings("WeakerAccess") public void enableMergePathsForKitKatAndAbove(boolean enable) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
Log.w(TAG, "Merge paths are not supported pre-Kit Kat.");
return;
}
enableMergePaths = enable;
if (composition != null) {
buildCompositionLayer();
}
}
/**
* 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 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() {
if (imageAssetBitmapManager != null) {
imageAssetBitmapManager.recycleBitmaps();
}
}
/**
* @return True if the composition is different from the previously set composition, false otherwise.
*/
@SuppressWarnings("WeakerAccess") public 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);
setScale(1f);
updateBounds();
buildCompositionLayer();
setProgress(progress);
if (playAnimationWhenCompositionAdded) {
playAnimationWhenCompositionAdded = false;
playAnimation();
}
if (reverseAnimationWhenCompositionAdded) {
reverseAnimationWhenCompositionAdded = false;
reverseAnimation();
}
return true;
}
private void buildCompositionLayer() {
compositionLayer = new CompositionLayer(
this, Layer.Factory.newInstance(composition), composition.getLayers(), composition);
}
private void clearComposition() {
recycleBitmaps();
compositionLayer = null;
imageAssetBitmapManager = null;
invalidateSelf();
}
@Override public void invalidateSelf() {
final Callback callback = getCallback();
if (callback != null) {
callback.invalidateDrawable(this);
}
}
@Override public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
this.alpha = alpha;
}
@Override public int getAlpha() {
return alpha;
}
@Override public void setColorFilter(@Nullable ColorFilter colorFilter) {
// Do nothing.
}
@Override public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
@Override public void draw(@NonNull Canvas canvas) {
if (compositionLayer == null) {
return;
}
matrix.reset();
matrix.preScale(scale, scale);
compositionLayer.draw(canvas, matrix, alpha);
}
void systemAnimationsAreDisabled() {
systemAnimationsAreDisabled = true;
}
@SuppressWarnings("WeakerAccess") public void loop(boolean loop) {
animator.setRepeatCount(loop ? ValueAnimator.INFINITE : 0);
}
@SuppressWarnings("WeakerAccess") public boolean isLooping() {
return animator.getRepeatCount() == ValueAnimator.INFINITE;
}
@SuppressWarnings("WeakerAccess") public boolean isAnimating() {
return animator.isRunning();
}
@SuppressWarnings("WeakerAccess") public void playAnimation() {
playAnimation(false);
}
@SuppressWarnings("WeakerAccess") public void resumeAnimation() {
playAnimation(true);
}
private void playAnimation(boolean setStartTime) {
if (compositionLayer == null) {
playAnimationWhenCompositionAdded = true;
reverseAnimationWhenCompositionAdded = false;
return;
}
if (setStartTime) {
animator.setCurrentPlayTime((long) (progress * animator.getDuration()));
}
animator.start();
}
@SuppressWarnings({"unused", "WeakerAccess"}) public void resumeReverseAnimation() {
reverseAnimation(true);
}
@SuppressWarnings("WeakerAccess") public void reverseAnimation() {
reverseAnimation(false);
}
private void reverseAnimation(boolean setStartTime) {
if (compositionLayer == null) {
playAnimationWhenCompositionAdded = false;
reverseAnimationWhenCompositionAdded = true;
return;
}
if (setStartTime) {
animator.setCurrentPlayTime((long) (progress * animator.getDuration()));
}
animator.reverse();
}
@SuppressWarnings("WeakerAccess") public 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)));
}
}
public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
this.progress = progress;
if (compositionLayer != null) {
compositionLayer.setProgress(progress);
}
}
public float getProgress() {
return progress;
}
@SuppressWarnings("WeakerAccess") public void setScale(float scale) {
this.scale = scale;
updateBounds();
}
/**
* Use this if you can't bundle images with your app. This may be useful if you download the
* animations from the network or have the images saved to an SD Card. In that case, Lottie
* will defer the loading of the bitmap to this delegate.
*/
@SuppressWarnings({"unused", "WeakerAccess"}) public void setImageAssetDelegate(
@SuppressWarnings("NullableProblems") ImageAssetDelegate assetDelegate) {
this.imageAssetDelegate = assetDelegate;
if (imageAssetBitmapManager != null) {
imageAssetBitmapManager.setAssetDelegate(assetDelegate);
}
}
@SuppressWarnings("WeakerAccess") public float getScale() {
return scale;
}
@SuppressWarnings("WeakerAccess") public LottieComposition getComposition() {
return composition;
}
private void updateBounds() {
if (composition == null) {
return;
}
setBounds(0, 0, (int) (composition.getBounds().width() * scale),
(int) (composition.getBounds().height() * scale));
}
@SuppressWarnings("WeakerAccess") public void cancelAnimation() {
playAnimationWhenCompositionAdded = false;
reverseAnimationWhenCompositionAdded = false;
animator.cancel();
}
@SuppressWarnings("WeakerAccess") public void addAnimatorUpdateListener(ValueAnimator.AnimatorUpdateListener updateListener) {
animator.addUpdateListener(updateListener);
}
@SuppressWarnings("WeakerAccess") public void removeAnimatorUpdateListener(ValueAnimator.AnimatorUpdateListener updateListener) {
animator.removeUpdateListener(updateListener);
}
@SuppressWarnings("WeakerAccess") public void addAnimatorListener(Animator.AnimatorListener listener) {
animator.addListener(listener);
}
@SuppressWarnings("WeakerAccess") public void removeAnimatorListener(Animator.AnimatorListener listener) {
animator.removeListener(listener);
}
@Override public int getIntrinsicWidth() {
return composition == null ? -1 : (int) (composition.getBounds().width() * scale);
}
@Override public int getIntrinsicHeight() {
return composition == null ? -1 : (int) (composition.getBounds().height() * scale);
}
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, imageAssetDelegate, 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(@NonNull Drawable who) {
Callback callback = getCallback();
if (callback == null) {
return;
}
callback.invalidateDrawable(this);
}
@Override public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
Callback callback = getCallback();
if (callback == null) {
return;
}
callback.scheduleDrawable(this, what, when);
}
@Override public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
Callback callback = getCallback();
if (callback == null) {
return;
}
callback.unscheduleDrawable(this, what);
}
}