• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.airbnb.lottie;
2 
3 import android.animation.Animator;
4 import android.animation.ValueAnimator;
5 import android.annotation.SuppressLint;
6 import android.content.Context;
7 import android.graphics.Bitmap;
8 import android.graphics.Canvas;
9 import android.graphics.ColorFilter;
10 import android.graphics.Matrix;
11 import android.graphics.Paint;
12 import android.graphics.PixelFormat;
13 import android.graphics.Rect;
14 import android.graphics.RectF;
15 import android.graphics.Typeface;
16 import android.graphics.drawable.Animatable;
17 import android.graphics.drawable.Drawable;
18 import android.os.Build;
19 import android.view.View;
20 import android.view.ViewGroup;
21 import android.view.ViewParent;
22 import android.widget.ImageView;
23 
24 import androidx.annotation.FloatRange;
25 import androidx.annotation.IntDef;
26 import androidx.annotation.IntRange;
27 import androidx.annotation.MainThread;
28 import androidx.annotation.NonNull;
29 import androidx.annotation.Nullable;
30 import androidx.annotation.RequiresApi;
31 import androidx.annotation.RestrictTo;
32 
33 import com.airbnb.lottie.animation.LPaint;
34 import com.airbnb.lottie.manager.FontAssetManager;
35 import com.airbnb.lottie.manager.ImageAssetManager;
36 import com.airbnb.lottie.model.KeyPath;
37 import com.airbnb.lottie.model.Marker;
38 import com.airbnb.lottie.model.layer.CompositionLayer;
39 import com.airbnb.lottie.parser.LayerParser;
40 import com.airbnb.lottie.utils.Logger;
41 import com.airbnb.lottie.utils.LottieValueAnimator;
42 import com.airbnb.lottie.utils.MiscUtils;
43 import com.airbnb.lottie.value.LottieFrameInfo;
44 import com.airbnb.lottie.value.LottieValueCallback;
45 import com.airbnb.lottie.value.SimpleLottieValueCallback;
46 
47 import java.lang.annotation.Retention;
48 import java.lang.annotation.RetentionPolicy;
49 import java.util.ArrayList;
50 import java.util.Collections;
51 import java.util.Iterator;
52 import java.util.List;
53 
54 /**
55  * This can be used to show an lottie animation in any place that would normally take a drawable.
56  *
57  * @see <a href="http://airbnb.io/lottie">Full Documentation</a>
58  */
59 @SuppressWarnings({"WeakerAccess"})
60 public class LottieDrawable extends Drawable implements Drawable.Callback, Animatable {
61   private interface LazyCompositionTask {
run(LottieComposition composition)62     void run(LottieComposition composition);
63   }
64 
65   /**
66    * Internal record keeping of the desired play state when {@link #isVisible()} transitions to or is false.
67    * <p>
68    * If the animation was playing when it becomes invisible or play/pause is called on it while it is invisible, it will
69    * store the state and then take the appropriate action when the drawable becomes visible again.
70    */
71   private enum OnVisibleAction {
72     NONE,
73     PLAY,
74     RESUME,
75   }
76 
77   private LottieComposition composition;
78   private final LottieValueAnimator animator = new LottieValueAnimator();
79 
80   // Call animationsEnabled() instead of using these fields directly.
81   private boolean systemAnimationsEnabled = true;
82   private boolean ignoreSystemAnimationsDisabled = false;
83 
84   private boolean safeMode = false;
85   private OnVisibleAction onVisibleAction = OnVisibleAction.NONE;
86 
87   private final ArrayList<LazyCompositionTask> lazyCompositionTasks = new ArrayList<>();
88   private final ValueAnimator.AnimatorUpdateListener progressUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
89     @Override
90     public void onAnimationUpdate(ValueAnimator animation) {
91       if (compositionLayer != null) {
92         compositionLayer.setProgress(animator.getAnimatedValueAbsolute());
93       }
94     }
95   };
96 
97   /**
98    * ImageAssetManager created automatically by Lottie for views.
99    */
100   @Nullable
101   private ImageAssetManager imageAssetManager;
102   @Nullable
103   private String imageAssetsFolder;
104   @Nullable
105   private ImageAssetDelegate imageAssetDelegate;
106   @Nullable
107   private FontAssetManager fontAssetManager;
108   @Nullable
109   FontAssetDelegate fontAssetDelegate;
110   @Nullable
111   TextDelegate textDelegate;
112   private boolean enableMergePaths;
113   private boolean maintainOriginalImageBounds = false;
114   private boolean clipToCompositionBounds = true;
115   @Nullable
116   private CompositionLayer compositionLayer;
117   private int alpha = 255;
118   private boolean performanceTrackingEnabled;
119   private boolean outlineMasksAndMattes;
120   private boolean isApplyingOpacityToLayersEnabled;
121 
122   private RenderMode renderMode = RenderMode.AUTOMATIC;
123   /**
124    * The actual render mode derived from {@link #renderMode}.
125    */
126   private boolean useSoftwareRendering = false;
127   private final Matrix renderingMatrix = new Matrix();
128   private Bitmap softwareRenderingBitmap;
129   private Canvas softwareRenderingCanvas;
130   private Rect canvasClipBounds;
131   private RectF canvasClipBoundsRectF;
132   private Paint softwareRenderingPaint;
133   private Rect softwareRenderingSrcBoundsRect;
134   private Rect softwareRenderingDstBoundsRect;
135   private RectF softwareRenderingDstBoundsRectF;
136   private RectF softwareRenderingTransformedBounds;
137   private Matrix softwareRenderingOriginalCanvasMatrix;
138   private Matrix softwareRenderingOriginalCanvasMatrixInverse;
139 
140   /**
141    * True if the drawable has not been drawn since the last invalidateSelf.
142    * We can do this to prevent things like bounds from getting recalculated
143    * many times.
144    */
145   private boolean isDirty = false;
146 
147   @IntDef({RESTART, REVERSE})
148   @Retention(RetentionPolicy.SOURCE)
149   public @interface RepeatMode {
150   }
151 
152   /**
153    * When the animation reaches the end and <code>repeatCount</code> is INFINITE
154    * or a positive value, the animation restarts from the beginning.
155    */
156   public static final int RESTART = ValueAnimator.RESTART;
157   /**
158    * When the animation reaches the end and <code>repeatCount</code> is INFINITE
159    * or a positive value, the animation reverses direction on every iteration.
160    */
161   public static final int REVERSE = ValueAnimator.REVERSE;
162   /**
163    * This value used used with the {@link #setRepeatCount(int)} property to repeat
164    * the animation indefinitely.
165    */
166   public static final int INFINITE = ValueAnimator.INFINITE;
167 
LottieDrawable()168   public LottieDrawable() {
169     animator.addUpdateListener(progressUpdateListener);
170   }
171 
172   /**
173    * Returns whether or not any layers in this composition has masks.
174    */
hasMasks()175   public boolean hasMasks() {
176     return compositionLayer != null && compositionLayer.hasMasks();
177   }
178 
179   /**
180    * Returns whether or not any layers in this composition has a matte layer.
181    */
hasMatte()182   public boolean hasMatte() {
183     return compositionLayer != null && compositionLayer.hasMatte();
184   }
185 
enableMergePathsForKitKatAndAbove()186   public boolean enableMergePathsForKitKatAndAbove() {
187     return enableMergePaths;
188   }
189 
190   /**
191    * Enable this to get merge path support for devices running KitKat (19) and above.
192    * <p>
193    * Merge paths currently don't work if the the operand shape is entirely contained within the
194    * first shape. If you need to cut out one shape from another shape, use an even-odd fill type
195    * instead of using merge paths.
196    */
enableMergePathsForKitKatAndAbove(boolean enable)197   public void enableMergePathsForKitKatAndAbove(boolean enable) {
198     if (enableMergePaths == enable) {
199       return;
200     }
201 
202     if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
203       Logger.warning("Merge paths are not supported pre-Kit Kat.");
204       return;
205     }
206     enableMergePaths = enable;
207     if (composition != null) {
208       buildCompositionLayer();
209     }
210   }
211 
isMergePathsEnabledForKitKatAndAbove()212   public boolean isMergePathsEnabledForKitKatAndAbove() {
213     return enableMergePaths;
214   }
215 
216   /**
217    * Sets whether or not Lottie should clip to the original animation composition bounds.
218    *
219    * Defaults to true.
220    */
setClipToCompositionBounds(boolean clipToCompositionBounds)221   public void setClipToCompositionBounds(boolean clipToCompositionBounds) {
222     if (clipToCompositionBounds != this.clipToCompositionBounds) {
223       this.clipToCompositionBounds = clipToCompositionBounds;
224       CompositionLayer compositionLayer = this.compositionLayer;
225       if (compositionLayer != null) {
226         compositionLayer.setClipToCompositionBounds(clipToCompositionBounds);
227       }
228       invalidateSelf();
229     }
230   }
231 
232   /**
233    * Gets whether or not Lottie should clip to the original animation composition bounds.
234    *
235    * Defaults to true.
236    */
getClipToCompositionBounds()237   public boolean getClipToCompositionBounds() {
238     return clipToCompositionBounds;
239   }
240 
241   /**
242    * If you use image assets, you must explicitly specify the folder in assets/ in which they are
243    * located because bodymovin uses the name filenames across all compositions (img_#).
244    * Do NOT rename the images themselves.
245    * <p>
246    * If your images are located in src/main/assets/airbnb_loader/ then call
247    * `setImageAssetsFolder("airbnb_loader/");`.
248    * <p>
249    * <p>
250    * Be wary if you are using many images, however. Lottie is designed to work with vector shapes
251    * from After Effects. If your images look like they could be represented with vector shapes,
252    * see if it is possible to convert them to shape layers and re-export your animation. Check
253    * the documentation at http://airbnb.io/lottie for more information about importing shapes from
254    * Sketch or Illustrator to avoid this.
255    */
setImagesAssetsFolder(@ullable String imageAssetsFolder)256   public void setImagesAssetsFolder(@Nullable String imageAssetsFolder) {
257     this.imageAssetsFolder = imageAssetsFolder;
258   }
259 
260   @Nullable
getImageAssetsFolder()261   public String getImageAssetsFolder() {
262     return imageAssetsFolder;
263   }
264 
265   /**
266    * When true, dynamically set bitmaps will be drawn with the exact bounds of the original animation, regardless of the bitmap size.
267    * When false, dynamically set bitmaps will be drawn at the top left of the original image but with its own bounds.
268    * <p>
269    * Defaults to false.
270    */
setMaintainOriginalImageBounds(boolean maintainOriginalImageBounds)271   public void setMaintainOriginalImageBounds(boolean maintainOriginalImageBounds) {
272     this.maintainOriginalImageBounds = maintainOriginalImageBounds;
273   }
274 
275   /**
276    * When true, dynamically set bitmaps will be drawn with the exact bounds of the original animation, regardless of the bitmap size.
277    * When false, dynamically set bitmaps will be drawn at the top left of the original image but with its own bounds.
278    * <p>
279    * Defaults to false.
280    */
getMaintainOriginalImageBounds()281   public boolean getMaintainOriginalImageBounds() {
282     return maintainOriginalImageBounds;
283   }
284 
285   /**
286    * Create a composition with {@link LottieCompositionFactory}
287    *
288    * @return True if the composition is different from the previously set composition, false otherwise.
289    */
setComposition(LottieComposition composition)290   public boolean setComposition(LottieComposition composition) {
291     if (this.composition == composition) {
292       return false;
293     }
294 
295     isDirty = true;
296     clearComposition();
297     this.composition = composition;
298     buildCompositionLayer();
299     animator.setComposition(composition);
300     setProgress(animator.getAnimatedFraction());
301 
302     // We copy the tasks to a new ArrayList so that if this method is called from multiple threads,
303     // then there won't be two iterators iterating and removing at the same time.
304     Iterator<LazyCompositionTask> it = new ArrayList<>(lazyCompositionTasks).iterator();
305     while (it.hasNext()) {
306       LazyCompositionTask t = it.next();
307       // The task should never be null but it appears to happen in rare cases. Maybe it's an oem-specific or ART bug.
308       // https://github.com/airbnb/lottie-android/issues/1702
309       if (t != null) {
310         t.run(composition);
311       }
312       it.remove();
313     }
314     lazyCompositionTasks.clear();
315 
316     composition.setPerformanceTrackingEnabled(performanceTrackingEnabled);
317     computeRenderMode();
318 
319     // Ensure that ImageView updates the drawable width/height so it can
320     // properly calculate its drawable matrix.
321     Callback callback = getCallback();
322     if (callback instanceof ImageView) {
323       ((ImageView) callback).setImageDrawable(null);
324       ((ImageView) callback).setImageDrawable(this);
325     }
326 
327     return true;
328   }
329 
330   /**
331    * Call this to set whether or not to render with hardware or software acceleration.
332    * Lottie defaults to Automatic which will use hardware acceleration unless:
333    * 1) There are dash paths and the device is pre-Pie.
334    * 2) There are more than 4 masks and mattes and the device is pre-Pie.
335    * Hardware acceleration is generally faster for those devices unless
336    * there are many large mattes and masks in which case there is a lot
337    * of GPU uploadTexture thrashing which makes it much slower.
338    * <p>
339    * In most cases, hardware rendering will be faster, even if you have mattes and masks.
340    * However, if you have multiple mattes and masks (especially large ones), you
341    * should test both render modes. You should also test on pre-Pie and Pie+ devices
342    * because the underlying rendering engine changed significantly.
343    *
344    * @see <a href="https://developer.android.com/guide/topics/graphics/hardware-accel#unsupported">Android Hardware Acceleration</a>
345    */
setRenderMode(RenderMode renderMode)346   public void setRenderMode(RenderMode renderMode) {
347     this.renderMode = renderMode;
348     computeRenderMode();
349   }
350 
351   /**
352    * Returns the actual render mode being used. It will always be {@link RenderMode#HARDWARE} or {@link RenderMode#SOFTWARE}.
353    * When the render mode is set to AUTOMATIC, the value will be derived from {@link RenderMode#useSoftwareRendering(int, boolean, int)}.
354    */
getRenderMode()355   public RenderMode getRenderMode() {
356     return useSoftwareRendering ? RenderMode.SOFTWARE : RenderMode.HARDWARE;
357   }
358 
computeRenderMode()359   private void computeRenderMode() {
360     LottieComposition composition = this.composition;
361     if (composition == null) {
362       return;
363     }
364     useSoftwareRendering = renderMode.useSoftwareRendering(
365         Build.VERSION.SDK_INT, composition.hasDashPattern(), composition.getMaskAndMatteCount());
366   }
367 
setPerformanceTrackingEnabled(boolean enabled)368   public void setPerformanceTrackingEnabled(boolean enabled) {
369     performanceTrackingEnabled = enabled;
370     if (composition != null) {
371       composition.setPerformanceTrackingEnabled(enabled);
372     }
373   }
374 
375   /**
376    * Enable this to debug slow animations by outlining masks and mattes. The performance overhead of the masks and mattes will
377    * be proportional to the surface area of all of the masks/mattes combined.
378    * <p>
379    * DO NOT leave this enabled in production.
380    */
setOutlineMasksAndMattes(boolean outline)381   public void setOutlineMasksAndMattes(boolean outline) {
382     if (outlineMasksAndMattes == outline) {
383       return;
384     }
385     outlineMasksAndMattes = outline;
386     if (compositionLayer != null) {
387       compositionLayer.setOutlineMasksAndMattes(outline);
388     }
389   }
390 
391   @Nullable
getPerformanceTracker()392   public PerformanceTracker getPerformanceTracker() {
393     if (composition != null) {
394       return composition.getPerformanceTracker();
395     }
396     return null;
397   }
398 
399   /**
400    * Sets whether to apply opacity to the each layer instead of shape.
401    * <p>
402    * Opacity is normally applied directly to a shape. In cases where translucent shapes overlap, applying opacity to a layer will be more accurate
403    * at the expense of performance.
404    * <p>
405    * The default value is false.
406    * <p>
407    * Note: This process is very expensive. The performance impact will be reduced when hardware acceleration is enabled.
408    *
409    * @see android.view.View#setLayerType(int, android.graphics.Paint)
410    * @see LottieAnimationView#setRenderMode(RenderMode)
411    */
setApplyingOpacityToLayersEnabled(boolean isApplyingOpacityToLayersEnabled)412   public void setApplyingOpacityToLayersEnabled(boolean isApplyingOpacityToLayersEnabled) {
413     this.isApplyingOpacityToLayersEnabled = isApplyingOpacityToLayersEnabled;
414   }
415 
416   /**
417    * This API no longer has any effect.
418    */
419   @Deprecated
disableExtraScaleModeInFitXY()420   public void disableExtraScaleModeInFitXY() {
421   }
422 
isApplyingOpacityToLayersEnabled()423   public boolean isApplyingOpacityToLayersEnabled() {
424     return isApplyingOpacityToLayersEnabled;
425   }
426 
buildCompositionLayer()427   private void buildCompositionLayer() {
428     LottieComposition composition = this.composition;
429     if (composition == null) {
430       return;
431     }
432     compositionLayer = new CompositionLayer(
433         this, LayerParser.parse(composition), composition.getLayers(), composition);
434     if (outlineMasksAndMattes) {
435       compositionLayer.setOutlineMasksAndMattes(true);
436     }
437     compositionLayer.setClipToCompositionBounds(clipToCompositionBounds);
438   }
439 
clearComposition()440   public void clearComposition() {
441     if (animator.isRunning()) {
442       animator.cancel();
443       if (!isVisible()) {
444         onVisibleAction = OnVisibleAction.NONE;
445       }
446     }
447     composition = null;
448     compositionLayer = null;
449     imageAssetManager = null;
450     animator.clearComposition();
451     invalidateSelf();
452   }
453 
454   /**
455    * If you are experiencing a device specific crash that happens during drawing, you can set this to true
456    * for those devices. If set to true, draw will be wrapped with a try/catch which will cause Lottie to
457    * render an empty frame rather than crash your app.
458    * <p>
459    * Ideally, you will never need this and the vast majority of apps and animations won't. However, you may use
460    * this for very specific cases if absolutely necessary.
461    */
setSafeMode(boolean safeMode)462   public void setSafeMode(boolean safeMode) {
463     this.safeMode = safeMode;
464   }
465 
466   @Override
invalidateSelf()467   public void invalidateSelf() {
468     if (isDirty) {
469       return;
470     }
471     isDirty = true;
472     final Callback callback = getCallback();
473     if (callback != null) {
474       callback.invalidateDrawable(this);
475     }
476   }
477 
478   @Override
setAlpha(@ntRangefrom = 0, to = 255) int alpha)479   public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
480     this.alpha = alpha;
481     invalidateSelf();
482   }
483 
484   @Override
getAlpha()485   public int getAlpha() {
486     return alpha;
487   }
488 
489   @Override
setColorFilter(@ullable ColorFilter colorFilter)490   public void setColorFilter(@Nullable ColorFilter colorFilter) {
491     Logger.warning("Use addColorFilter instead.");
492   }
493 
494   @Override
getOpacity()495   public int getOpacity() {
496     return PixelFormat.TRANSLUCENT;
497   }
498 
499   @Override
draw(@onNull Canvas canvas)500   public void draw(@NonNull Canvas canvas) {
501     L.beginSection("Drawable#draw");
502 
503     if (safeMode) {
504       try {
505         if (useSoftwareRendering) {
506           renderAndDrawAsBitmap(canvas, compositionLayer);
507         } else {
508           drawDirectlyToCanvas(canvas);
509         }
510       } catch (Throwable e) {
511         Logger.error("Lottie crashed in draw!", e);
512       }
513     } else {
514       if (useSoftwareRendering) {
515         renderAndDrawAsBitmap(canvas, compositionLayer);
516       } else {
517         drawDirectlyToCanvas(canvas);
518       }
519     }
520 
521     isDirty = false;
522     L.endSection("Drawable#draw");
523   }
524 
525   /**
526    * To be used by lottie-compose only.
527    */
528   @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
draw(Canvas canvas, Matrix matrix)529   public void draw(Canvas canvas, Matrix matrix) {
530     CompositionLayer compositionLayer = this.compositionLayer;
531     LottieComposition composition = this.composition;
532     if (compositionLayer == null || composition == null) {
533       return;
534     }
535 
536     if (useSoftwareRendering) {
537       canvas.save();
538       canvas.concat(matrix);
539       renderAndDrawAsBitmap(canvas, compositionLayer);
540       canvas.restore();
541     } else {
542       compositionLayer.draw(canvas, matrix, alpha);
543     }
544     isDirty = false;
545   }
546 
547   // <editor-fold desc="animator">
548 
549   @MainThread
550   @Override
start()551   public void start() {
552     Callback callback = getCallback();
553     if (callback instanceof View && ((View) callback).isInEditMode()) {
554       // Don't auto play when in edit mode.
555       return;
556     }
557     playAnimation();
558   }
559 
560   @MainThread
561   @Override
stop()562   public void stop() {
563     endAnimation();
564   }
565 
566   @Override
isRunning()567   public boolean isRunning() {
568     return isAnimating();
569   }
570 
571   /**
572    * Plays the animation from the beginning. If speed is {@literal <} 0, it will start at the end
573    * and play towards the beginning
574    */
575   @MainThread
playAnimation()576   public void playAnimation() {
577     if (compositionLayer == null) {
578       lazyCompositionTasks.add(c -> playAnimation());
579       return;
580     }
581 
582     computeRenderMode();
583     if (animationsEnabled() || getRepeatCount() == 0) {
584       if (isVisible()) {
585         animator.playAnimation();
586         onVisibleAction = OnVisibleAction.NONE;
587       } else {
588         onVisibleAction = OnVisibleAction.PLAY;
589       }
590     }
591     if (!animationsEnabled()) {
592       setFrame((int) (getSpeed() < 0 ? getMinFrame() : getMaxFrame()));
593       animator.endAnimation();
594       if (!isVisible()) {
595         onVisibleAction = OnVisibleAction.NONE;
596       }
597     }
598   }
599 
600   @MainThread
601   public void endAnimation() {
602     lazyCompositionTasks.clear();
603     animator.endAnimation();
604     if (!isVisible()) {
605       onVisibleAction = OnVisibleAction.NONE;
606     }
607   }
608 
609   /**
610    * Continues playing the animation from its current position. If speed {@literal <} 0, it will play backwards
611    * from the current position.
612    */
613   @MainThread
614   public void resumeAnimation() {
615     if (compositionLayer == null) {
616       lazyCompositionTasks.add(c -> resumeAnimation());
617       return;
618     }
619 
620     computeRenderMode();
621     if (animationsEnabled() || getRepeatCount() == 0) {
622       if (isVisible()) {
623         animator.resumeAnimation();
624         onVisibleAction = OnVisibleAction.NONE;
625       } else {
626         onVisibleAction = OnVisibleAction.RESUME;
627       }
628     }
629     if (!animationsEnabled()) {
630       setFrame((int) (getSpeed() < 0 ? getMinFrame() : getMaxFrame()));
631       animator.endAnimation();
632       if (!isVisible()) {
633         onVisibleAction = OnVisibleAction.NONE;
634       }
635     }
636   }
637 
638   /**
639    * Sets the minimum frame that the animation will start from when playing or looping.
640    */
641   public void setMinFrame(final int minFrame) {
642     if (composition == null) {
643       lazyCompositionTasks.add(c -> setMinFrame(minFrame));
644       return;
645     }
646     animator.setMinFrame(minFrame);
647   }
648 
649   /**
650    * Returns the minimum frame set by {@link #setMinFrame(int)} or {@link #setMinProgress(float)}
651    */
getMinFrame()652   public float getMinFrame() {
653     return animator.getMinFrame();
654   }
655 
656   /**
657    * Sets the minimum progress that the animation will start from when playing or looping.
658    */
setMinProgress(final float minProgress)659   public void setMinProgress(final float minProgress) {
660     if (composition == null) {
661       lazyCompositionTasks.add(c -> setMinProgress(minProgress));
662       return;
663     }
664     setMinFrame((int) MiscUtils.lerp(composition.getStartFrame(), composition.getEndFrame(), minProgress));
665   }
666 
667   /**
668    * Sets the maximum frame that the animation will end at when playing or looping.
669    * <p>
670    * The value will be clamped to the composition bounds. For example, setting Integer.MAX_VALUE would result in the same
671    * thing as composition.endFrame.
672    */
setMaxFrame(final int maxFrame)673   public void setMaxFrame(final int maxFrame) {
674     if (composition == null) {
675       lazyCompositionTasks.add(c -> setMaxFrame(maxFrame));
676       return;
677     }
678     animator.setMaxFrame(maxFrame + 0.99f);
679   }
680 
681   /**
682    * Returns the maximum frame set by {@link #setMaxFrame(int)} or {@link #setMaxProgress(float)}
683    */
getMaxFrame()684   public float getMaxFrame() {
685     return animator.getMaxFrame();
686   }
687 
688   /**
689    * Sets the maximum progress that the animation will end at when playing or looping.
690    */
setMaxProgress(@loatRangefrom = 0f, to = 1f) final float maxProgress)691   public void setMaxProgress(@FloatRange(from = 0f, to = 1f) final float maxProgress) {
692     if (composition == null) {
693       lazyCompositionTasks.add(c -> setMaxProgress(maxProgress));
694       return;
695     }
696     animator.setMaxFrame(MiscUtils.lerp(composition.getStartFrame(), composition.getEndFrame(), maxProgress));
697   }
698 
699   /**
700    * Sets the minimum frame to the start time of the specified marker.
701    *
702    * @throws IllegalArgumentException if the marker is not found.
703    */
setMinFrame(final String markerName)704   public void setMinFrame(final String markerName) {
705     if (composition == null) {
706       lazyCompositionTasks.add(c -> setMinFrame(markerName));
707       return;
708     }
709     Marker marker = composition.getMarker(markerName);
710     if (marker == null) {
711       throw new IllegalArgumentException("Cannot find marker with name " + markerName + ".");
712     }
713     setMinFrame((int) marker.startFrame);
714   }
715 
716   /**
717    * Sets the maximum frame to the start time + duration of the specified marker.
718    *
719    * @throws IllegalArgumentException if the marker is not found.
720    */
setMaxFrame(final String markerName)721   public void setMaxFrame(final String markerName) {
722     if (composition == null) {
723       lazyCompositionTasks.add(c -> setMaxFrame(markerName));
724       return;
725     }
726     Marker marker = composition.getMarker(markerName);
727     if (marker == null) {
728       throw new IllegalArgumentException("Cannot find marker with name " + markerName + ".");
729     }
730     setMaxFrame((int) (marker.startFrame + marker.durationFrames));
731   }
732 
733   /**
734    * Sets the minimum and maximum frame to the start time and start time + duration
735    * of the specified marker.
736    *
737    * @throws IllegalArgumentException if the marker is not found.
738    */
setMinAndMaxFrame(final String markerName)739   public void setMinAndMaxFrame(final String markerName) {
740     if (composition == null) {
741       lazyCompositionTasks.add(c -> setMinAndMaxFrame(markerName));
742       return;
743     }
744     Marker marker = composition.getMarker(markerName);
745     if (marker == null) {
746       throw new IllegalArgumentException("Cannot find marker with name " + markerName + ".");
747     }
748     int startFrame = (int) marker.startFrame;
749     setMinAndMaxFrame(startFrame, startFrame + (int) marker.durationFrames);
750   }
751 
752   /**
753    * Sets the minimum and maximum frame to the start marker start and the maximum frame to the end marker start.
754    * playEndMarkerStartFrame determines whether or not to play the frame that the end marker is on. If the end marker
755    * represents the end of the section that you want, it should be true. If the marker represents the beginning of the
756    * next section, it should be false.
757    *
758    * @throws IllegalArgumentException if either marker is not found.
759    */
setMinAndMaxFrame(final String startMarkerName, final String endMarkerName, final boolean playEndMarkerStartFrame)760   public void setMinAndMaxFrame(final String startMarkerName, final String endMarkerName, final boolean playEndMarkerStartFrame) {
761     if (composition == null) {
762       lazyCompositionTasks.add(c -> setMinAndMaxFrame(startMarkerName, endMarkerName, playEndMarkerStartFrame));
763       return;
764     }
765     Marker startMarker = composition.getMarker(startMarkerName);
766     if (startMarker == null) {
767       throw new IllegalArgumentException("Cannot find marker with name " + startMarkerName + ".");
768     }
769     int startFrame = (int) startMarker.startFrame;
770 
771     final Marker endMarker = composition.getMarker(endMarkerName);
772     if (endMarker == null) {
773       throw new IllegalArgumentException("Cannot find marker with name " + endMarkerName + ".");
774     }
775     int endFrame = (int) (endMarker.startFrame + (playEndMarkerStartFrame ? 1f : 0f));
776 
777     setMinAndMaxFrame(startFrame, endFrame);
778   }
779 
780   /**
781    * @see #setMinFrame(int)
782    * @see #setMaxFrame(int)
783    */
setMinAndMaxFrame(final int minFrame, final int maxFrame)784   public void setMinAndMaxFrame(final int minFrame, final int maxFrame) {
785     if (composition == null) {
786       lazyCompositionTasks.add(c -> setMinAndMaxFrame(minFrame, maxFrame));
787       return;
788     }
789     // Adding 0.99 ensures that the maxFrame itself gets played.
790     animator.setMinAndMaxFrames(minFrame, maxFrame + 0.99f);
791   }
792 
793   /**
794    * @see #setMinProgress(float)
795    * @see #setMaxProgress(float)
796    */
setMinAndMaxProgress( @loatRangefrom = 0f, to = 1f) final float minProgress, @FloatRange(from = 0f, to = 1f) final float maxProgress)797   public void setMinAndMaxProgress(
798       @FloatRange(from = 0f, to = 1f) final float minProgress,
799       @FloatRange(from = 0f, to = 1f) final float maxProgress) {
800     if (composition == null) {
801       lazyCompositionTasks.add(c -> setMinAndMaxProgress(minProgress, maxProgress));
802       return;
803     }
804 
805     setMinAndMaxFrame((int) MiscUtils.lerp(composition.getStartFrame(), composition.getEndFrame(), minProgress),
806         (int) MiscUtils.lerp(composition.getStartFrame(), composition.getEndFrame(), maxProgress));
807   }
808 
809   /**
810    * Reverses the current animation speed. This does NOT play the animation.
811    *
812    * @see #setSpeed(float)
813    * @see #playAnimation()
814    * @see #resumeAnimation()
815    */
reverseAnimationSpeed()816   public void reverseAnimationSpeed() {
817     animator.reverseAnimationSpeed();
818   }
819 
820   /**
821    * Sets the playback speed. If speed {@literal <} 0, the animation will play backwards.
822    */
setSpeed(float speed)823   public void setSpeed(float speed) {
824     animator.setSpeed(speed);
825   }
826 
827   /**
828    * Returns the current playback speed. This will be {@literal <} 0 if the animation is playing backwards.
829    */
getSpeed()830   public float getSpeed() {
831     return animator.getSpeed();
832   }
833 
addAnimatorUpdateListener(ValueAnimator.AnimatorUpdateListener updateListener)834   public void addAnimatorUpdateListener(ValueAnimator.AnimatorUpdateListener updateListener) {
835     animator.addUpdateListener(updateListener);
836   }
837 
removeAnimatorUpdateListener(ValueAnimator.AnimatorUpdateListener updateListener)838   public void removeAnimatorUpdateListener(ValueAnimator.AnimatorUpdateListener updateListener) {
839     animator.removeUpdateListener(updateListener);
840   }
841 
removeAllUpdateListeners()842   public void removeAllUpdateListeners() {
843     animator.removeAllUpdateListeners();
844     animator.addUpdateListener(progressUpdateListener);
845   }
846 
addAnimatorListener(Animator.AnimatorListener listener)847   public void addAnimatorListener(Animator.AnimatorListener listener) {
848     animator.addListener(listener);
849   }
850 
removeAnimatorListener(Animator.AnimatorListener listener)851   public void removeAnimatorListener(Animator.AnimatorListener listener) {
852     animator.removeListener(listener);
853   }
854 
removeAllAnimatorListeners()855   public void removeAllAnimatorListeners() {
856     animator.removeAllListeners();
857   }
858 
859   @RequiresApi(api = Build.VERSION_CODES.KITKAT)
addAnimatorPauseListener(Animator.AnimatorPauseListener listener)860   public void addAnimatorPauseListener(Animator.AnimatorPauseListener listener) {
861     animator.addPauseListener(listener);
862   }
863 
864   @RequiresApi(api = Build.VERSION_CODES.KITKAT)
removeAnimatorPauseListener(Animator.AnimatorPauseListener listener)865   public void removeAnimatorPauseListener(Animator.AnimatorPauseListener listener) {
866     animator.removePauseListener(listener);
867   }
868 
869   /**
870    * Sets the progress to the specified frame.
871    * If the composition isn't set yet, the progress will be set to the frame when
872    * it is.
873    */
setFrame(final int frame)874   public void setFrame(final int frame) {
875     if (composition == null) {
876       lazyCompositionTasks.add(c -> setFrame(frame));
877       return;
878     }
879 
880     animator.setFrame(frame);
881   }
882 
883   /**
884    * Get the currently rendered frame.
885    */
getFrame()886   public int getFrame() {
887     return (int) animator.getFrame();
888   }
889 
setProgress(@loatRangefrom = 0f, to = 1f) final float progress)890   public void setProgress(@FloatRange(from = 0f, to = 1f) final float progress) {
891     if (composition == null) {
892       lazyCompositionTasks.add(c -> setProgress(progress));
893       return;
894     }
895     L.beginSection("Drawable#setProgress");
896     animator.setFrame(composition.getFrameForProgress(progress));
897     L.endSection("Drawable#setProgress");
898   }
899 
900   /**
901    * @see #setRepeatCount(int)
902    */
903   @Deprecated
loop(boolean loop)904   public void loop(boolean loop) {
905     animator.setRepeatCount(loop ? ValueAnimator.INFINITE : 0);
906   }
907 
908   /**
909    * Defines what this animation should do when it reaches the end. This
910    * setting is applied only when the repeat count is either greater than
911    * 0 or {@link #INFINITE}. Defaults to {@link #RESTART}.
912    *
913    * @param mode {@link #RESTART} or {@link #REVERSE}
914    */
setRepeatMode(@epeatMode int mode)915   public void setRepeatMode(@RepeatMode int mode) {
916     animator.setRepeatMode(mode);
917   }
918 
919   /**
920    * Defines what this animation should do when it reaches the end.
921    *
922    * @return either one of {@link #REVERSE} or {@link #RESTART}
923    */
924   @SuppressLint("WrongConstant")
925   @RepeatMode
getRepeatMode()926   public int getRepeatMode() {
927     return animator.getRepeatMode();
928   }
929 
930   /**
931    * Sets how many times the animation should be repeated. If the repeat
932    * count is 0, the animation is never repeated. If the repeat count is
933    * greater than 0 or {@link #INFINITE}, the repeat mode will be taken
934    * into account. The repeat count is 0 by default.
935    *
936    * @param count the number of times the animation should be repeated
937    */
setRepeatCount(int count)938   public void setRepeatCount(int count) {
939     animator.setRepeatCount(count);
940   }
941 
942   /**
943    * Defines how many times the animation should repeat. The default value
944    * is 0.
945    *
946    * @return the number of times the animation should repeat, or {@link #INFINITE}
947    */
getRepeatCount()948   public int getRepeatCount() {
949     return animator.getRepeatCount();
950   }
951 
952 
953   @SuppressWarnings("unused")
isLooping()954   public boolean isLooping() {
955     return animator.getRepeatCount() == ValueAnimator.INFINITE;
956   }
957 
isAnimating()958   public boolean isAnimating() {
959     // On some versions of Android, this is called from the LottieAnimationView constructor, before animator was created.
960     // https://github.com/airbnb/lottie-android/issues/1430
961     //noinspection ConstantConditions
962     if (animator == null) {
963       return false;
964     }
965     return animator.isRunning();
966   }
967 
isAnimatingOrWillAnimateOnVisible()968   boolean isAnimatingOrWillAnimateOnVisible() {
969     if (isVisible()) {
970       return animator.isRunning();
971     } else {
972       return onVisibleAction == OnVisibleAction.PLAY || onVisibleAction == OnVisibleAction.RESUME;
973     }
974   }
975 
animationsEnabled()976   private boolean animationsEnabled() {
977     return systemAnimationsEnabled || ignoreSystemAnimationsDisabled;
978   }
979 
980   /**
981    * Tell Lottie that system animations are disabled. When using {@link LottieAnimationView} or Compose {@code LottieAnimation}, this is done
982    * automatically. However, if you are using LottieDrawable on its own, you should set this to false when
983    * {@link com.airbnb.lottie.utils.Utils#getAnimationScale(Context)} is 0.
984    */
setSystemAnimationsAreEnabled(Boolean areEnabled)985   public void setSystemAnimationsAreEnabled(Boolean areEnabled) {
986     systemAnimationsEnabled = areEnabled;
987   }
988 
989 // </editor-fold>
990 
991   /**
992    * Allows ignoring system animations settings, therefore allowing animations to run even if they are disabled.
993    * <p>
994    * Defaults to false.
995    *
996    * @param ignore if true animations will run even when they are disabled in the system settings.
997    */
setIgnoreDisabledSystemAnimations(boolean ignore)998   public void setIgnoreDisabledSystemAnimations(boolean ignore) {
999     ignoreSystemAnimationsDisabled = ignore;
1000   }
1001 
1002   /**
1003    * Use this if you can't bundle images with your app. This may be useful if you download the
1004    * animations from the network or have the images saved to an SD Card. In that case, Lottie
1005    * will defer the loading of the bitmap to this delegate.
1006    * <p>
1007    * Be wary if you are using many images, however. Lottie is designed to work with vector shapes
1008    * from After Effects. If your images look like they could be represented with vector shapes,
1009    * see if it is possible to convert them to shape layers and re-export your animation. Check
1010    * the documentation at http://airbnb.io/lottie for more information about importing shapes from
1011    * Sketch or Illustrator to avoid this.
1012    */
setImageAssetDelegate(ImageAssetDelegate assetDelegate)1013   public void setImageAssetDelegate(ImageAssetDelegate assetDelegate) {
1014     this.imageAssetDelegate = assetDelegate;
1015     if (imageAssetManager != null) {
1016       imageAssetManager.setDelegate(assetDelegate);
1017     }
1018   }
1019 
1020   /**
1021    * Use this to manually set fonts.
1022    */
setFontAssetDelegate(FontAssetDelegate assetDelegate)1023   public void setFontAssetDelegate(FontAssetDelegate assetDelegate) {
1024     this.fontAssetDelegate = assetDelegate;
1025     if (fontAssetManager != null) {
1026       fontAssetManager.setDelegate(assetDelegate);
1027     }
1028   }
1029 
setTextDelegate(@uppressWarnings"NullableProblems") TextDelegate textDelegate)1030   public void setTextDelegate(@SuppressWarnings("NullableProblems") TextDelegate textDelegate) {
1031     this.textDelegate = textDelegate;
1032   }
1033 
1034   @Nullable
getTextDelegate()1035   public TextDelegate getTextDelegate() {
1036     return textDelegate;
1037   }
1038 
useTextGlyphs()1039   public boolean useTextGlyphs() {
1040     return textDelegate == null && composition.getCharacters().size() > 0;
1041   }
1042 
getComposition()1043   public LottieComposition getComposition() {
1044     return composition;
1045   }
1046 
cancelAnimation()1047   public void cancelAnimation() {
1048     lazyCompositionTasks.clear();
1049     animator.cancel();
1050     if (!isVisible()) {
1051       onVisibleAction = OnVisibleAction.NONE;
1052     }
1053   }
1054 
pauseAnimation()1055   public void pauseAnimation() {
1056     lazyCompositionTasks.clear();
1057     animator.pauseAnimation();
1058     if (!isVisible()) {
1059       onVisibleAction = OnVisibleAction.NONE;
1060     }
1061   }
1062 
1063   @FloatRange(from = 0f, to = 1f)
getProgress()1064   public float getProgress() {
1065     return animator.getAnimatedValueAbsolute();
1066   }
1067 
1068   @Override
getIntrinsicWidth()1069   public int getIntrinsicWidth() {
1070     return composition == null ? -1 : composition.getBounds().width();
1071   }
1072 
1073   @Override
getIntrinsicHeight()1074   public int getIntrinsicHeight() {
1075     return composition == null ? -1 : composition.getBounds().height();
1076   }
1077 
1078   /**
1079    * Takes a {@link KeyPath}, potentially with wildcards or globstars and resolve it to a list of
1080    * zero or more actual {@link KeyPath Keypaths} that exist in the current animation.
1081    * <p>
1082    * If you want to set value callbacks for any of these values, it is recommend to use the
1083    * returned {@link KeyPath} objects because they will be internally resolved to their content
1084    * and won't trigger a tree walk of the animation contents when applied.
1085    */
resolveKeyPath(KeyPath keyPath)1086   public List<KeyPath> resolveKeyPath(KeyPath keyPath) {
1087     if (compositionLayer == null) {
1088       Logger.warning("Cannot resolve KeyPath. Composition is not set yet.");
1089       return Collections.emptyList();
1090     }
1091     List<KeyPath> keyPaths = new ArrayList<>();
1092     compositionLayer.resolveKeyPath(keyPath, 0, keyPaths, new KeyPath());
1093     return keyPaths;
1094   }
1095 
1096   /**
1097    * Add an property callback for the specified {@link KeyPath}. This {@link KeyPath} can resolve
1098    * to multiple contents. In that case, the callback's value will apply to all of them.
1099    * <p>
1100    * Internally, this will check if the {@link KeyPath} has already been resolved with
1101    * {@link #resolveKeyPath(KeyPath)} and will resolve it if it hasn't.
1102    */
addValueCallback( final KeyPath keyPath, final T property, @Nullable final LottieValueCallback<T> callback)1103   public <T> void addValueCallback(
1104       final KeyPath keyPath, final T property, @Nullable final LottieValueCallback<T> callback) {
1105     if (compositionLayer == null) {
1106       lazyCompositionTasks.add(c -> addValueCallback(keyPath, property, callback));
1107       return;
1108     }
1109     boolean invalidate;
1110     if (keyPath == KeyPath.COMPOSITION) {
1111       compositionLayer.addValueCallback(property, callback);
1112       invalidate = true;
1113     } else if (keyPath.getResolvedElement() != null) {
1114       keyPath.getResolvedElement().addValueCallback(property, callback);
1115       invalidate = true;
1116     } else {
1117       List<KeyPath> elements = resolveKeyPath(keyPath);
1118 
1119       for (int i = 0; i < elements.size(); i++) {
1120         //noinspection ConstantConditions
1121         elements.get(i).getResolvedElement().addValueCallback(property, callback);
1122       }
1123       invalidate = !elements.isEmpty();
1124     }
1125     if (invalidate) {
1126       invalidateSelf();
1127       if (property == LottieProperty.TIME_REMAP) {
1128         // Time remapping values are read in setProgress. In order for the new value
1129         // to apply, we have to re-set the progress with the current progress so that the
1130         // time remapping can be reapplied.
1131         setProgress(getProgress());
1132       }
1133     }
1134   }
1135 
1136   /**
1137    * Overload of {@link #addValueCallback(KeyPath, Object, LottieValueCallback)} that takes an interface. This allows you to use a single abstract
1138    * method code block in Kotlin such as:
1139    * drawable.addValueCallback(yourKeyPath, LottieProperty.COLOR) { yourColor }
1140    */
addValueCallback(KeyPath keyPath, T property, final SimpleLottieValueCallback<T> callback)1141   public <T> void addValueCallback(KeyPath keyPath, T property,
1142       final SimpleLottieValueCallback<T> callback) {
1143     addValueCallback(keyPath, property, new LottieValueCallback<T>() {
1144       @Override
1145       public T getValue(LottieFrameInfo<T> frameInfo) {
1146         return callback.getValue(frameInfo);
1147       }
1148     });
1149   }
1150 
1151 
1152   /**
1153    * Allows you to modify or clear a bitmap that was loaded for an image either automatically
1154    * through {@link #setImagesAssetsFolder(String)} or with an {@link ImageAssetDelegate}.
1155    *
1156    * @return the previous Bitmap or null.
1157    */
1158   @Nullable
updateBitmap(String id, @Nullable Bitmap bitmap)1159   public Bitmap updateBitmap(String id, @Nullable Bitmap bitmap) {
1160     ImageAssetManager bm = getImageAssetManager();
1161     if (bm == null) {
1162       Logger.warning("Cannot update bitmap. Most likely the drawable is not added to a View " +
1163           "which prevents Lottie from getting a Context.");
1164       return null;
1165     }
1166     Bitmap ret = bm.updateBitmap(id, bitmap);
1167     invalidateSelf();
1168     return ret;
1169   }
1170 
1171   /**
1172    * @deprecated use {@link #getBitmapForId(String)}.
1173    */
1174   @Nullable
1175   @Deprecated
getImageAsset(String id)1176   public Bitmap getImageAsset(String id) {
1177     ImageAssetManager bm = getImageAssetManager();
1178     if (bm != null) {
1179       return bm.bitmapForId(id);
1180     }
1181     LottieImageAsset imageAsset = composition == null ? null : composition.getImages().get(id);
1182     if (imageAsset != null) {
1183       return imageAsset.getBitmap();
1184     }
1185     return null;
1186   }
1187 
1188   /**
1189    * Returns the bitmap that will be rendered for the given id in the Lottie animation file.
1190    * The id is the asset reference id stored in the "id" property of each object in the "assets" array.
1191    * <p>
1192    * The returned bitmap could be from:
1193    * * Embedded in the animation file as a base64 string.
1194    * * In the same directory as the animation file.
1195    * * In the same zip file as the animation file.
1196    * * Returned from an {@link ImageAssetDelegate}.
1197    * or null if the image doesn't exist from any of those places.
1198    */
1199   @Nullable
getBitmapForId(String id)1200   public Bitmap getBitmapForId(String id) {
1201     ImageAssetManager assetManager = getImageAssetManager();
1202     if (assetManager != null) {
1203       return assetManager.bitmapForId(id);
1204     }
1205     return null;
1206   }
1207 
1208   /**
1209    * Returns the {@link LottieImageAsset} that will be rendered for the given id in the Lottie animation file.
1210    * The id is the asset reference id stored in the "id" property of each object in the "assets" array.
1211    * <p>
1212    * The returned bitmap could be from:
1213    * * Embedded in the animation file as a base64 string.
1214    * * In the same directory as the animation file.
1215    * * In the same zip file as the animation file.
1216    * * Returned from an {@link ImageAssetDelegate}.
1217    * or null if the image doesn't exist from any of those places.
1218    */
1219   @Nullable
getLottieImageAssetForId(String id)1220   public LottieImageAsset getLottieImageAssetForId(String id) {
1221     LottieComposition composition = this.composition;
1222     if (composition == null) {
1223       return null;
1224     }
1225     return composition.getImages().get(id);
1226   }
1227 
getImageAssetManager()1228   private ImageAssetManager getImageAssetManager() {
1229     if (getCallback() == null) {
1230       // We can't get a bitmap since we can't get a Context from the callback.
1231       return null;
1232     }
1233 
1234     if (imageAssetManager != null && !imageAssetManager.hasSameContext(getContext())) {
1235       imageAssetManager = null;
1236     }
1237 
1238     if (imageAssetManager == null) {
1239       imageAssetManager = new ImageAssetManager(getCallback(),
1240           imageAssetsFolder, imageAssetDelegate, composition.getImages());
1241     }
1242 
1243     return imageAssetManager;
1244   }
1245 
1246   @Nullable
getTypeface(String fontFamily, String style)1247   public Typeface getTypeface(String fontFamily, String style) {
1248     FontAssetManager assetManager = getFontAssetManager();
1249     if (assetManager != null) {
1250       return assetManager.getTypeface(fontFamily, style);
1251     }
1252     return null;
1253   }
1254 
getFontAssetManager()1255   private FontAssetManager getFontAssetManager() {
1256     if (getCallback() == null) {
1257       // We can't get a bitmap since we can't get a Context from the callback.
1258       return null;
1259     }
1260 
1261     if (fontAssetManager == null) {
1262       fontAssetManager = new FontAssetManager(getCallback(), fontAssetDelegate);
1263     }
1264 
1265     return fontAssetManager;
1266   }
1267 
1268   @Nullable
getContext()1269   private Context getContext() {
1270     Callback callback = getCallback();
1271     if (callback == null) {
1272       return null;
1273     }
1274 
1275     if (callback instanceof View) {
1276       return ((View) callback).getContext();
1277     }
1278     return null;
1279   }
1280 
setVisible(boolean visible, boolean restart)1281   @Override public boolean setVisible(boolean visible, boolean restart) {
1282     // Sometimes, setVisible(false) gets called twice in a row. If we don't check wasNotVisibleAlready, we could
1283     // wind up clearing the onVisibleAction value for the second call.
1284     boolean wasNotVisibleAlready = !isVisible();
1285     boolean ret = super.setVisible(visible, restart);
1286 
1287     if (visible) {
1288       if (onVisibleAction == OnVisibleAction.PLAY) {
1289         playAnimation();
1290       } else if (onVisibleAction == OnVisibleAction.RESUME) {
1291         resumeAnimation();
1292       }
1293     } else {
1294       if (animator.isRunning()) {
1295         pauseAnimation();
1296         onVisibleAction = OnVisibleAction.RESUME;
1297       } else if (!wasNotVisibleAlready) {
1298         onVisibleAction = OnVisibleAction.NONE;
1299       }
1300     }
1301     return ret;
1302   }
1303 
1304   /**
1305    * These Drawable.Callback methods proxy the calls so that this is the drawable that is
1306    * actually invalidated, not a child one which will not pass the view's validateDrawable check.
1307    */
1308   @Override
invalidateDrawable(@onNull Drawable who)1309   public void invalidateDrawable(@NonNull Drawable who) {
1310     Callback callback = getCallback();
1311     if (callback == null) {
1312       return;
1313     }
1314     callback.invalidateDrawable(this);
1315   }
1316 
1317   @Override
scheduleDrawable(@onNull Drawable who, @NonNull Runnable what, long when)1318   public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
1319     Callback callback = getCallback();
1320     if (callback == null) {
1321       return;
1322     }
1323     callback.scheduleDrawable(this, what, when);
1324   }
1325 
1326   @Override
unscheduleDrawable(@onNull Drawable who, @NonNull Runnable what)1327   public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
1328     Callback callback = getCallback();
1329     if (callback == null) {
1330       return;
1331     }
1332     callback.unscheduleDrawable(this, what);
1333   }
1334 
1335   /**
1336    * Hardware accelerated render path.
1337    */
drawDirectlyToCanvas(Canvas canvas)1338   private void drawDirectlyToCanvas(Canvas canvas) {
1339     CompositionLayer compositionLayer = this.compositionLayer;
1340     LottieComposition composition = this.composition;
1341     if (compositionLayer == null || composition == null) {
1342       return;
1343     }
1344 
1345     renderingMatrix.reset();
1346     Rect bounds = getBounds();
1347     if (!bounds.isEmpty()) {
1348       // In fitXY mode, the scale doesn't take effect.
1349       float scaleX = bounds.width() / (float) composition.getBounds().width();
1350       float scaleY = bounds.height() / (float) composition.getBounds().height();
1351 
1352       renderingMatrix.preScale(scaleX, scaleY);
1353     }
1354     compositionLayer.draw(canvas, renderingMatrix, alpha);
1355   }
1356 
1357   /**
1358    * Software accelerated render path.
1359    *
1360    * This draws the animation to an internally managed bitmap and then draws the bitmap to the original canvas.
1361    *
1362    * @see LottieAnimationView#setRenderMode(RenderMode)
1363    */
renderAndDrawAsBitmap(Canvas originalCanvas, CompositionLayer compositionLayer)1364   private void renderAndDrawAsBitmap(Canvas originalCanvas, CompositionLayer compositionLayer) {
1365     if (composition == null || compositionLayer == null) {
1366       return;
1367     }
1368     ensureSoftwareRenderingObjectsInitialized();
1369 
1370     //noinspection deprecation
1371     originalCanvas.getMatrix(softwareRenderingOriginalCanvasMatrix);
1372 
1373     // Get the canvas clip bounds and map it to the coordinate space of canvas with it's current transform.
1374     originalCanvas.getClipBounds(canvasClipBounds);
1375     convertRect(canvasClipBounds, canvasClipBoundsRectF);
1376     softwareRenderingOriginalCanvasMatrix.mapRect(canvasClipBoundsRectF);
1377     convertRect(canvasClipBoundsRectF, canvasClipBounds);
1378 
1379     if (clipToCompositionBounds) {
1380       // Start with the intrinsic bounds. This will later be unioned with the clip bounds to find the
1381       // smallest possible render area.
1382       softwareRenderingTransformedBounds.set(0f, 0f, getIntrinsicWidth(), getIntrinsicHeight());
1383     } else {
1384       // Calculate the full bounds of the animation.
1385       compositionLayer.getBounds(softwareRenderingTransformedBounds, null, false);
1386     }
1387     // Transform the animation bounds to the bounds that they will render to on the canvas.
1388     softwareRenderingOriginalCanvasMatrix.mapRect(softwareRenderingTransformedBounds);
1389 
1390     // The bounds are usually intrinsicWidth x intrinsicHeight. If they are different, an external source is scaling this drawable.
1391     // This is how ImageView.ScaleType.FIT_XY works.
1392     Rect bounds = getBounds();
1393     float scaleX = bounds.width() / (float) getIntrinsicWidth();
1394     float scaleY = bounds.height() / (float) getIntrinsicHeight();
1395     scaleRect(softwareRenderingTransformedBounds, scaleX, scaleY);
1396 
1397     if (!ignoreCanvasClipBounds()) {
1398       softwareRenderingTransformedBounds.intersect(canvasClipBounds.left, canvasClipBounds.top, canvasClipBounds.right, canvasClipBounds.bottom);
1399     }
1400 
1401     int renderWidth = (int) Math.ceil(softwareRenderingTransformedBounds.width());
1402     int renderHeight = (int) Math.ceil(softwareRenderingTransformedBounds.height());
1403 
1404     if (renderWidth == 0 || renderHeight == 0) {
1405       return;
1406     }
1407 
1408     ensureSoftwareRenderingBitmap(renderWidth, renderHeight);
1409 
1410     if (isDirty) {
1411       renderingMatrix.set(softwareRenderingOriginalCanvasMatrix);
1412       renderingMatrix.preScale(scaleX, scaleY);
1413       // We want to render the smallest bitmap possible. If the animation doesn't start at the top left, we translate the canvas and shrink the
1414       // bitmap to avoid allocating and copying the empty space on the left and top. renderWidth and renderHeight take this into account.
1415       renderingMatrix.postTranslate(-softwareRenderingTransformedBounds.left, -softwareRenderingTransformedBounds.top);
1416 
1417       softwareRenderingBitmap.eraseColor(0);
1418       compositionLayer.draw(softwareRenderingCanvas, renderingMatrix, alpha);
1419 
1420       // Calculate the dst bounds.
1421       // We need to map the rendered coordinates back to the canvas's coordinates. To do so, we need to invert the transform
1422       // of the original canvas.
1423       // Take the bounds of the rendered animation and map them to the canvas's coordinates.
1424       // This is similar to the src rect above but the src bound may have a left and top offset.
1425       softwareRenderingOriginalCanvasMatrix.invert(softwareRenderingOriginalCanvasMatrixInverse);
1426       softwareRenderingOriginalCanvasMatrixInverse.mapRect(softwareRenderingDstBoundsRectF, softwareRenderingTransformedBounds);
1427       convertRect(softwareRenderingDstBoundsRectF, softwareRenderingDstBoundsRect);
1428     }
1429 
1430     softwareRenderingSrcBoundsRect.set(0, 0, renderWidth, renderHeight);
1431     originalCanvas.drawBitmap(softwareRenderingBitmap, softwareRenderingSrcBoundsRect, softwareRenderingDstBoundsRect, softwareRenderingPaint);
1432   }
1433 
ensureSoftwareRenderingObjectsInitialized()1434   private void ensureSoftwareRenderingObjectsInitialized() {
1435     if (softwareRenderingCanvas != null) {
1436       return;
1437     }
1438     softwareRenderingCanvas = new Canvas();
1439     softwareRenderingTransformedBounds = new RectF();
1440     softwareRenderingOriginalCanvasMatrix = new Matrix();
1441     softwareRenderingOriginalCanvasMatrixInverse = new Matrix();
1442     canvasClipBounds = new Rect();
1443     canvasClipBoundsRectF = new RectF();
1444     softwareRenderingPaint = new LPaint();
1445     softwareRenderingSrcBoundsRect = new Rect();
1446     softwareRenderingDstBoundsRect = new Rect();
1447     softwareRenderingDstBoundsRectF = new RectF();
1448   }
1449 
ensureSoftwareRenderingBitmap(int renderWidth, int renderHeight)1450   private void ensureSoftwareRenderingBitmap(int renderWidth, int renderHeight) {
1451     if (softwareRenderingBitmap == null ||
1452         softwareRenderingBitmap.getWidth() < renderWidth ||
1453         softwareRenderingBitmap.getHeight() < renderHeight) {
1454       // The bitmap is larger. We need to create a new one.
1455       softwareRenderingBitmap = Bitmap.createBitmap(renderWidth, renderHeight, Bitmap.Config.ARGB_8888);
1456       softwareRenderingCanvas.setBitmap(softwareRenderingBitmap);
1457       isDirty = true;
1458     } else if (softwareRenderingBitmap.getWidth() > renderWidth || softwareRenderingBitmap.getHeight() > renderHeight) {
1459       // The bitmap is smaller. Take subset of the original.
1460       softwareRenderingBitmap = Bitmap.createBitmap(softwareRenderingBitmap, 0, 0, renderWidth, renderHeight);
1461       softwareRenderingCanvas.setBitmap(softwareRenderingBitmap);
1462       isDirty = true;
1463     }
1464   }
1465 
1466   /**
1467    * Convert a RectF to a Rect
1468    */
convertRect(RectF src, Rect dst)1469   private void convertRect(RectF src, Rect dst) {
1470     dst.set(
1471         (int) Math.floor(src.left),
1472         (int) Math.floor(src.top),
1473         (int) Math.ceil(src.right),
1474         (int) Math.ceil(src.bottom)
1475     );
1476   }
1477 
1478   /**
1479    * Convert a Rect to a RectF
1480    */
convertRect(Rect src, RectF dst)1481   private void convertRect(Rect src, RectF dst) {
1482     dst.set(
1483         src.left,
1484         src.top,
1485         src.right,
1486         src.bottom);
1487   }
1488 
scaleRect(RectF rect, float scaleX, float scaleY)1489   private void scaleRect(RectF rect, float scaleX, float scaleY) {
1490     rect.set(
1491         rect.left * scaleX,
1492         rect.top * scaleY,
1493         rect.right * scaleX,
1494         rect.bottom * scaleY
1495     );
1496   }
1497 
1498   /**
1499    * When a View's parent has clipChildren set to false, it doesn't affect the clipBound
1500    * of its child canvases so we should explicitly check for it and draw the full animation
1501    * bounds instead.
1502    */
ignoreCanvasClipBounds()1503   private boolean ignoreCanvasClipBounds() {
1504     Callback callback = getCallback();
1505     if (!(callback instanceof View)) {
1506       // If the callback isn't a view then respect the canvas's clip bounds.
1507       return false;
1508     }
1509     ViewParent parent = ((View) callback).getParent();
1510     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && parent instanceof ViewGroup) {
1511       return !((ViewGroup) parent).getClipChildren();
1512     }
1513     // Unlikely to ever happen. If the callback is a View, its parent should be a ViewGroup.
1514     return false;
1515   }
1516 }
1517