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