| package com.airbnb.lottie; |
| |
| import android.animation.Animator; |
| import android.animation.ValueAnimator; |
| import android.graphics.Bitmap; |
| import android.graphics.Canvas; |
| import android.graphics.Rect; |
| import android.support.annotation.NonNull; |
| import android.support.annotation.Nullable; |
| import android.support.annotation.VisibleForTesting; |
| import android.support.v4.util.LongSparseArray; |
| 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 { |
| private LottieComposition composition; |
| private final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); |
| |
| @Nullable private Bitmap mainBitmap = null; |
| @Nullable private Bitmap maskBitmap = null; |
| @Nullable private Bitmap matteBitmap = null; |
| @Nullable private Bitmap mainBitmapForMatte = null; |
| @Nullable private Bitmap maskBitmapForMatte = null; |
| private boolean playAnimationWhenLayerAdded; |
| private boolean reverseAnimationWhenLayerAdded; |
| private boolean systemAnimationsAreDisabled; |
| |
| 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(animation.getAnimatedFraction()); |
| } |
| } |
| }); |
| } |
| |
| void 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."); |
| } |
| clearComposition(); |
| this.composition = composition; |
| animator.setDuration(composition.getDuration()); |
| setBounds(0, 0, composition.getBounds().width(), composition.getBounds().height()); |
| buildLayersForComposition(composition); |
| |
| getCallback().invalidateDrawable(this); |
| } |
| |
| private void clearComposition() { |
| recycleBitmaps(); |
| clearLayers(); |
| } |
| |
| private void buildLayersForComposition(LottieComposition composition) { |
| if (composition == null) { |
| throw new IllegalStateException("Composition is null"); |
| } |
| Rect bounds = composition.getBounds(); |
| if (composition.hasMasks() || composition.hasMattes()) { |
| mainBitmap = Bitmap.createBitmap(bounds.width(), bounds.height(), Bitmap.Config.ARGB_8888); |
| } |
| if (composition.hasMasks()) { |
| maskBitmap = Bitmap.createBitmap(bounds.width(), bounds.height(), Bitmap.Config.ALPHA_8); |
| } |
| if (composition.hasMattes()) { |
| matteBitmap = Bitmap.createBitmap(bounds.width(), bounds.height(), Bitmap.Config.ARGB_8888); |
| } |
| LongSparseArray<LayerView> layerMap = new LongSparseArray<>(composition.getLayers().size()); |
| List<LayerView> layers = new ArrayList<>(composition.getLayers().size()); |
| LayerView maskedLayer = null; |
| for (int i = composition.getLayers().size() - 1; i >= 0; i--) { |
| Layer layer = composition.getLayers().get(i); |
| LayerView layerView; |
| if (maskedLayer == null) { |
| layerView = |
| new LayerView(layer, composition, getCallback(), mainBitmap, maskBitmap, matteBitmap); |
| } else { |
| if (mainBitmapForMatte == null) { |
| mainBitmapForMatte = |
| Bitmap.createBitmap(bounds.width(), bounds.height(), Bitmap.Config.ALPHA_8); |
| } |
| if (maskBitmapForMatte == null && !layer.getMasks().isEmpty()) { |
| maskBitmapForMatte = |
| Bitmap.createBitmap(bounds.width(), bounds.height(), Bitmap.Config.ALPHA_8); |
| } |
| |
| layerView = |
| new LayerView(layer, composition, getCallback(), mainBitmapForMatte, maskBitmapForMatte, |
| null); |
| } |
| layerMap.put(layerView.getId(), layerView); |
| if (maskedLayer != null) { |
| maskedLayer.setMatteLayer(layerView); |
| maskedLayer = null; |
| } else { |
| layers.add(layerView); |
| if (layer.getMatteType() == Layer.MatteType.Add) { |
| maskedLayer = 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); |
| } |
| super.draw(canvas); |
| canvas.clipRect(getBounds()); |
| 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 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(); |
| } |
| |
| @VisibleForTesting |
| void recycleBitmaps() { |
| if (mainBitmap != null) { |
| mainBitmap.recycle(); |
| mainBitmap = null; |
| } |
| if (maskBitmap != null) { |
| maskBitmap.recycle(); |
| maskBitmap = null; |
| } |
| if (matteBitmap != null) { |
| matteBitmap.recycle(); |
| matteBitmap = null; |
| } |
| if (mainBitmapForMatte != null) { |
| mainBitmapForMatte.recycle(); |
| mainBitmapForMatte = null; |
| } |
| if (maskBitmapForMatte != null) { |
| maskBitmapForMatte.recycle(); |
| maskBitmapForMatte = null; |
| } |
| } |
| } |