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