| 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); |
| } |
| } |