• 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.content.Context;
6 import android.graphics.Bitmap;
7 import android.graphics.Canvas;
8 import android.graphics.ColorFilter;
9 import android.graphics.Matrix;
10 import android.graphics.PixelFormat;
11 import android.graphics.Rect;
12 import android.graphics.Typeface;
13 import android.graphics.drawable.Animatable;
14 import android.graphics.drawable.Drawable;
15 import android.os.Build;
16 import android.view.View;
17 import android.widget.ImageView;
18 
19 import androidx.annotation.FloatRange;
20 import androidx.annotation.IntDef;
21 import androidx.annotation.IntRange;
22 import androidx.annotation.MainThread;
23 import androidx.annotation.NonNull;
24 import androidx.annotation.Nullable;
25 
26 import com.airbnb.lottie.manager.FontAssetManager;
27 import com.airbnb.lottie.manager.ImageAssetManager;
28 import com.airbnb.lottie.model.KeyPath;
29 import com.airbnb.lottie.model.Marker;
30 import com.airbnb.lottie.model.layer.CompositionLayer;
31 import com.airbnb.lottie.parser.LayerParser;
32 import com.airbnb.lottie.utils.Logger;
33 import com.airbnb.lottie.utils.LottieValueAnimator;
34 import com.airbnb.lottie.utils.MiscUtils;
35 import com.airbnb.lottie.value.LottieFrameInfo;
36 import com.airbnb.lottie.value.LottieValueCallback;
37 import com.airbnb.lottie.value.SimpleLottieValueCallback;
38 
39 import java.lang.annotation.Retention;
40 import java.lang.annotation.RetentionPolicy;
41 import java.util.ArrayList;
42 import java.util.Collections;
43 import java.util.HashSet;
44 import java.util.Iterator;
45 import java.util.List;
46 import java.util.Set;
47 
48 /**
49  * This can be used to show an lottie animation in any place that would normally take a drawable.
50  *
51  * @see <a href="http://airbnb.io/lottie">Full Documentation</a>
52  */
53 @SuppressWarnings({"WeakerAccess", "unused"})
54 public class LottieDrawable extends Drawable implements Drawable.Callback, Animatable {
55   private static final String TAG = LottieDrawable.class.getSimpleName();
56 
57   private interface LazyCompositionTask {
run(LottieComposition composition)58     void run(LottieComposition composition);
59   }
60 
61   private final Matrix matrix = new Matrix();
62   private LottieComposition composition;
63   private final LottieValueAnimator animator = new LottieValueAnimator();
64   private float scale = 1f;
65   private boolean systemAnimationsEnabled = true;
66   private boolean safeMode = false;
67 
68   private final Set<ColorFilterData> colorFilterData = new HashSet<>();
69   private final ArrayList<LazyCompositionTask> lazyCompositionTasks = new ArrayList<>();
70   private final ValueAnimator.AnimatorUpdateListener  progressUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
71     @Override
72     public void onAnimationUpdate(ValueAnimator animation) {
73       if (compositionLayer != null) {
74         compositionLayer.setProgress(animator.getAnimatedValueAbsolute());
75       }
76     }
77   };
78   @Nullable
79   private ImageView.ScaleType scaleType;
80   @Nullable
81   private ImageAssetManager imageAssetManager;
82   @Nullable
83   private String imageAssetsFolder;
84   @Nullable
85   private ImageAssetDelegate imageAssetDelegate;
86   @Nullable
87   private FontAssetManager fontAssetManager;
88   @Nullable
89   FontAssetDelegate fontAssetDelegate;
90   @Nullable
91   TextDelegate textDelegate;
92   private boolean enableMergePaths;
93   @Nullable
94   private CompositionLayer compositionLayer;
95   private int alpha = 255;
96   private boolean performanceTrackingEnabled;
97   private boolean isApplyingOpacityToLayersEnabled;
98   private boolean isExtraScaleEnabled = true;
99   /**
100    * True if the drawable has not been drawn since the last invalidateSelf.
101    * We can do this to prevent things like bounds from getting recalculated
102    * many times.
103    */
104   private boolean isDirty = false;
105 
106   @IntDef({RESTART, REVERSE})
107   @Retention(RetentionPolicy.SOURCE)
108   public @interface RepeatMode {
109   }
110 
111   /**
112    * When the animation reaches the end and <code>repeatCount</code> is INFINITE
113    * or a positive value, the animation restarts from the beginning.
114    */
115   public static final int RESTART = ValueAnimator.RESTART;
116   /**
117    * When the animation reaches the end and <code>repeatCount</code> is INFINITE
118    * or a positive value, the animation reverses direction on every iteration.
119    */
120   public static final int REVERSE = ValueAnimator.REVERSE;
121   /**
122    * This value used used with the {@link #setRepeatCount(int)} property to repeat
123    * the animation indefinitely.
124    */
125   public static final int INFINITE = ValueAnimator.INFINITE;
126 
LottieDrawable()127   public LottieDrawable() {
128     animator.addUpdateListener(progressUpdateListener);
129   }
130 
131   /**
132    * Returns whether or not any layers in this composition has masks.
133    */
hasMasks()134   public boolean hasMasks() {
135     return compositionLayer != null && compositionLayer.hasMasks();
136   }
137 
138   /**
139    * Returns whether or not any layers in this composition has a matte layer.
140    */
hasMatte()141   public boolean hasMatte() {
142     return compositionLayer != null && compositionLayer.hasMatte();
143   }
144 
enableMergePathsForKitKatAndAbove()145   public boolean enableMergePathsForKitKatAndAbove() {
146     return enableMergePaths;
147   }
148 
149   /**
150    * Enable this to get merge path support for devices running KitKat (19) and above.
151    * <p>
152    * Merge paths currently don't work if the the operand shape is entirely contained within the
153    * first shape. If you need to cut out one shape from another shape, use an even-odd fill type
154    * instead of using merge paths.
155    */
enableMergePathsForKitKatAndAbove(boolean enable)156   public void enableMergePathsForKitKatAndAbove(boolean enable) {
157     if (enableMergePaths == enable) {
158       return;
159     }
160 
161     if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
162       Logger.warning("Merge paths are not supported pre-Kit Kat.");
163       return;
164     }
165     enableMergePaths = enable;
166     if (composition != null) {
167       buildCompositionLayer();
168     }
169   }
170 
isMergePathsEnabledForKitKatAndAbove()171   public boolean isMergePathsEnabledForKitKatAndAbove() {
172     return enableMergePaths;
173   }
174 
175   /**
176    * If you use image assets, you must explicitly specify the folder in assets/ in which they are
177    * located because bodymovin uses the name filenames across all compositions (img_#).
178    * Do NOT rename the images themselves.
179    * <p>
180    * If your images are located in src/main/assets/airbnb_loader/ then call
181    * `setImageAssetsFolder("airbnb_loader/");`.
182    * <p>
183    * <p>
184    * If you use LottieDrawable directly, you MUST call {@link #recycleBitmaps()} when you
185    * are done. Calling {@link #recycleBitmaps()} doesn't have to be final and {@link LottieDrawable}
186    * will recreate the bitmaps if needed but they will leak if you don't recycle them.
187    * <p>
188    * Be wary if you are using many images, however. Lottie is designed to work with vector shapes
189    * from After Effects. If your images look like they could be represented with vector shapes,
190    * see if it is possible to convert them to shape layers and re-export your animation. Check
191    * the documentation at http://airbnb.io/lottie for more information about importing shapes from
192    * Sketch or Illustrator to avoid this.
193    */
setImagesAssetsFolder(@ullable String imageAssetsFolder)194   public void setImagesAssetsFolder(@Nullable String imageAssetsFolder) {
195     this.imageAssetsFolder = imageAssetsFolder;
196   }
197 
198   @Nullable
getImageAssetsFolder()199   public String getImageAssetsFolder() {
200     return imageAssetsFolder;
201   }
202 
203   /**
204    * Create a composition with {@link LottieCompositionFactory}
205    *
206    * @return True if the composition is different from the previously set composition, false otherwise.
207    */
setComposition(LottieComposition composition)208   public boolean setComposition(LottieComposition composition) {
209     if (this.composition == composition) {
210       return false;
211     }
212 
213     isDirty = false;
214     clearComposition();
215     this.composition = composition;
216     buildCompositionLayer();
217     animator.setComposition(composition);
218     setProgress(animator.getAnimatedFraction());
219     setScale(scale);
220     updateBounds();
221 
222     // We copy the tasks to a new ArrayList so that if this method is called from multiple threads,
223     // then there won't be two iterators iterating and removing at the same time.
224     Iterator<LazyCompositionTask> it = new ArrayList<>(lazyCompositionTasks).iterator();
225     while (it.hasNext()) {
226       LazyCompositionTask t = it.next();
227       t.run(composition);
228       it.remove();
229     }
230     lazyCompositionTasks.clear();
231 
232     composition.setPerformanceTrackingEnabled(performanceTrackingEnabled);
233 
234     return true;
235   }
236 
setPerformanceTrackingEnabled(boolean enabled)237   public void setPerformanceTrackingEnabled(boolean enabled) {
238     performanceTrackingEnabled = enabled;
239     if (composition != null) {
240       composition.setPerformanceTrackingEnabled(enabled);
241     }
242   }
243 
244   @Nullable
getPerformanceTracker()245   public PerformanceTracker getPerformanceTracker() {
246     if (composition != null) {
247       return composition.getPerformanceTracker();
248     }
249     return null;
250   }
251 
252   /**
253    * Sets whether to apply opacity to the each layer instead of shape.
254    * <p>
255    * Opacity is normally applied directly to a shape. In cases where translucent shapes overlap, applying opacity to a layer will be more accurate
256    * at the expense of performance.
257    * <p>
258    * The default value is false.
259    * <p>
260    * Note: This process is very expensive. The performance impact will be reduced when hardware acceleration is enabled.
261    *
262    * @see android.view.View#setLayerType(int, android.graphics.Paint)
263    * @see LottieAnimationView#setRenderMode(RenderMode)
264    */
setApplyingOpacityToLayersEnabled(boolean isApplyingOpacityToLayersEnabled)265   public void setApplyingOpacityToLayersEnabled(boolean isApplyingOpacityToLayersEnabled) {
266     this.isApplyingOpacityToLayersEnabled = isApplyingOpacityToLayersEnabled;
267   }
268 
269   /**
270    * Disable the extraScale mode in {@link #draw(Canvas)} function when scaleType is FitXY. It doesn't affect the rendering with other scaleTypes.
271    *
272    * <p>When there are 2 animation layout side by side, the default extra scale mode might leave 1 pixel not drawn between 2 animation, and
273    * disabling the extraScale mode can fix this problem</p>
274    *
275    * <b>Attention:</b> Disable the extra scale mode can downgrade the performance and may lead to larger memory footprint. Please only disable this
276    * mode when using animation with a reasonable dimension (smaller than screen size).
277    *
278    * @see #drawWithNewAspectRatio(Canvas)
279    */
disableExtraScaleModeInFitXY()280   public void disableExtraScaleModeInFitXY() {
281     this.isExtraScaleEnabled = false;
282   }
283 
isApplyingOpacityToLayersEnabled()284   public boolean isApplyingOpacityToLayersEnabled() {
285     return isApplyingOpacityToLayersEnabled;
286   }
287 
buildCompositionLayer()288   private void buildCompositionLayer() {
289     compositionLayer = new CompositionLayer(
290         this, LayerParser.parse(composition), composition.getLayers(), composition);
291   }
292 
clearComposition()293   public void clearComposition() {
294     if (animator.isRunning()) {
295       animator.cancel();
296     }
297     composition = null;
298     compositionLayer = null;
299     imageAssetManager = null;
300     animator.clearComposition();
301     invalidateSelf();
302   }
303 
304   /**
305    * If you are experiencing a device specific crash that happens during drawing, you can set this to true
306    * for those devices. If set to true, draw will be wrapped with a try/catch which will cause Lottie to
307    * render an empty frame rather than crash your app.
308    *
309    * Ideally, you will never need this and the vast majority of apps and animations won't. However, you may use
310    * this for very specific cases if absolutely necessary.
311    */
setSafeMode(boolean safeMode)312   public void setSafeMode(boolean safeMode) {
313     this.safeMode = safeMode;
314   }
315 
316   @Override
invalidateSelf()317   public void invalidateSelf() {
318     if (isDirty) {
319       return;
320     }
321     isDirty = true;
322     final Callback callback = getCallback();
323     if (callback != null) {
324       callback.invalidateDrawable(this);
325     }
326   }
327 
328   @Override
setAlpha(@ntRangefrom = 0, to = 255) int alpha)329   public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
330     this.alpha = alpha;
331     invalidateSelf();
332   }
333 
334   @Override
getAlpha()335   public int getAlpha() {
336     return alpha;
337   }
338 
339   @Override
setColorFilter(@ullable ColorFilter colorFilter)340   public void setColorFilter(@Nullable ColorFilter colorFilter) {
341     Logger.warning("Use addColorFilter instead.");
342   }
343 
344   @Override
getOpacity()345   public int getOpacity() {
346     return PixelFormat.TRANSLUCENT;
347   }
348 
349   @Override
draw(@onNull Canvas canvas)350   public void draw(@NonNull Canvas canvas) {
351     isDirty = false;
352 
353     L.beginSection("Drawable#draw");
354 
355     if (safeMode) {
356       try {
357         drawInternal(canvas);
358       } catch (Throwable e) {
359         Logger.error("Lottie crashed in draw!", e);
360       }
361     } else {
362       drawInternal(canvas);
363     }
364 
365     L.endSection("Drawable#draw");
366   }
367 
drawInternal(@onNull Canvas canvas)368   private void drawInternal(@NonNull Canvas canvas) {
369     if (ImageView.ScaleType.FIT_XY == scaleType) {
370       drawWithNewAspectRatio(canvas);
371     } else {
372       drawWithOriginalAspectRatio(canvas);
373     }
374   }
375 
376 // <editor-fold desc="animator">
377 
378   @MainThread
379   @Override
start()380   public void start() {
381     playAnimation();
382   }
383 
384   @MainThread
385   @Override
stop()386   public void stop() {
387     endAnimation();
388   }
389 
390   @Override
isRunning()391   public boolean isRunning() {
392     return isAnimating();
393   }
394 
395   /**
396    * Plays the animation from the beginning. If speed is < 0, it will start at the end
397    * and play towards the beginning
398    */
399   @MainThread
playAnimation()400   public void playAnimation() {
401     if (compositionLayer == null) {
402       lazyCompositionTasks.add(new LazyCompositionTask() {
403         @Override
404         public void run(LottieComposition composition) {
405           playAnimation();
406         }
407       });
408       return;
409     }
410 
411     if (systemAnimationsEnabled || getRepeatCount() == 0) {
412       animator.playAnimation();
413     }
414     if (!systemAnimationsEnabled) {
415       setFrame((int) (getSpeed() < 0 ? getMinFrame() : getMaxFrame()));
416       animator.endAnimation();
417     }
418   }
419 
420   @MainThread
421   public void endAnimation() {
422     lazyCompositionTasks.clear();
423     animator.endAnimation();
424   }
425 
426   /**
427    * Continues playing the animation from its current position. If speed < 0, it will play backwards
428    * from the current position.
429    */
430   @MainThread
431   public void resumeAnimation() {
432     if (compositionLayer == null) {
433       lazyCompositionTasks.add(new LazyCompositionTask() {
434         @Override
435         public void run(LottieComposition composition) {
436           resumeAnimation();
437         }
438       });
439       return;
440     }
441 
442     if (systemAnimationsEnabled || getRepeatCount() == 0) {
443       animator.resumeAnimation();
444     }
445     if (!systemAnimationsEnabled) {
446       setFrame((int) (getSpeed() < 0 ? getMinFrame() : getMaxFrame()));
447       animator.endAnimation();
448     }
449   }
450 
451   /**
452    * Sets the minimum frame that the animation will start from when playing or looping.
453    */
454   public void setMinFrame(final int minFrame) {
455     if (composition == null) {
456       lazyCompositionTasks.add(new LazyCompositionTask() {
457         @Override
458         public void run(LottieComposition composition) {
459           setMinFrame(minFrame);
460         }
461       });
462       return;
463     }
464     animator.setMinFrame(minFrame);
465   }
466 
467   /**
468    * Returns the minimum frame set by {@link #setMinFrame(int)} or {@link #setMinProgress(float)}
469    */
470   public float getMinFrame() {
471     return animator.getMinFrame();
472   }
473 
474   /**
475    * Sets the minimum progress that the animation will start from when playing or looping.
476    */
477   public void setMinProgress(final float minProgress) {
478     if (composition == null) {
479       lazyCompositionTasks.add(new LazyCompositionTask() {
480         @Override
481         public void run(LottieComposition composition) {
482           setMinProgress(minProgress);
483         }
484       });
485       return;
486     }
487     setMinFrame((int) MiscUtils.lerp(composition.getStartFrame(), composition.getEndFrame(), minProgress));
488   }
489 
490   /**
491    * Sets the maximum frame that the animation will end at when playing or looping.
492    */
493   public void setMaxFrame(final int maxFrame) {
494     if (composition == null) {
495       lazyCompositionTasks.add(new LazyCompositionTask() {
496         @Override
497         public void run(LottieComposition composition) {
498           setMaxFrame(maxFrame);
499         }
500       });
501       return;
502     }
503     animator.setMaxFrame(maxFrame + 0.99f);
504   }
505 
506   /**
507    * Returns the maximum frame set by {@link #setMaxFrame(int)} or {@link #setMaxProgress(float)}
508    */
509   public float getMaxFrame() {
510     return animator.getMaxFrame();
511   }
512 
513   /**
514    * Sets the maximum progress that the animation will end at when playing or looping.
515    */
516   public void setMaxProgress(@FloatRange(from = 0f, to = 1f) final float maxProgress) {
517     if (composition == null) {
518       lazyCompositionTasks.add(new LazyCompositionTask() {
519         @Override
520         public void run(LottieComposition composition) {
521           setMaxProgress(maxProgress);
522         }
523       });
524       return;
525     }
526     setMaxFrame((int) MiscUtils.lerp(composition.getStartFrame(), composition.getEndFrame(), maxProgress));
527   }
528 
529   /**
530    * Sets the minimum frame to the start time of the specified marker.
531    * @throws IllegalArgumentException if the marker is not found.
532    */
533   public void setMinFrame(final String markerName) {
534     if (composition == null) {
535       lazyCompositionTasks.add(new LazyCompositionTask() {
536         @Override
537         public void run(LottieComposition composition) {
538           setMinFrame(markerName);
539         }
540       });
541       return;
542     }
543     Marker marker = composition.getMarker(markerName);
544     if (marker == null) {
545       throw new IllegalArgumentException("Cannot find marker with name " + markerName + ".");
546     }
547     setMinFrame((int) marker.startFrame);
548   }
549 
550   /**
551    * Sets the maximum frame to the start time + duration of the specified marker.
552    * @throws IllegalArgumentException if the marker is not found.
553    */
554   public void setMaxFrame(final String markerName) {
555     if (composition == null) {
556       lazyCompositionTasks.add(new LazyCompositionTask() {
557         @Override
558         public void run(LottieComposition composition) {
559           setMaxFrame(markerName);
560         }
561       });
562       return;
563     }
564     Marker marker = composition.getMarker(markerName);
565     if (marker == null) {
566       throw new IllegalArgumentException("Cannot find marker with name " + markerName + ".");
567     }
568     setMaxFrame((int) (marker.startFrame + marker.durationFrames));
569   }
570 
571   /**
572    * Sets the minimum and maximum frame to the start time and start time + duration
573    * of the specified marker.
574    * @throws IllegalArgumentException if the marker is not found.
575    */
576   public void setMinAndMaxFrame(final String markerName) {
577     if (composition == null) {
578       lazyCompositionTasks.add(new LazyCompositionTask() {
579         @Override
580         public void run(LottieComposition composition) {
581           setMinAndMaxFrame(markerName);
582         }
583       });
584       return;
585     }
586     Marker marker = composition.getMarker(markerName);
587     if (marker == null) {
588       throw new IllegalArgumentException("Cannot find marker with name " + markerName + ".");
589     }
590     int startFrame = (int) marker.startFrame;
591     setMinAndMaxFrame(startFrame, startFrame + (int) marker.durationFrames);
592   }
593 
594   /**
595    * Sets the minimum and maximum frame to the start marker start and the maximum frame to the end marker start.
596    * playEndMarkerStartFrame determines whether or not to play the frame that the end marker is on. If the end marker
597    * represents the end of the section that you want, it should be true. If the marker represents the beginning of the
598    * next section, it should be false.
599    *
600    * @throws IllegalArgumentException if either marker is not found.
601    */
602   public void setMinAndMaxFrame(final String startMarkerName, final String endMarkerName, final boolean playEndMarkerStartFrame) {
603     if (composition == null) {
604       lazyCompositionTasks.add(new LazyCompositionTask() {
605         @Override
606         public void run(LottieComposition composition) {
607           setMinAndMaxFrame(startMarkerName, endMarkerName, playEndMarkerStartFrame);
608         }
609       });
610       return;
611     }
612     Marker startMarker = composition.getMarker(startMarkerName);
613     if (startMarker == null) {
614       throw new IllegalArgumentException("Cannot find marker with name " + startMarkerName + ".");
615     }
616     int startFrame = (int) startMarker.startFrame;
617 
618     Marker endMarker = composition.getMarker(endMarkerName);
619     if (endMarkerName == null) {
620       throw new IllegalArgumentException("Cannot find marker with name " + endMarkerName + ".");
621     }
622     int endFrame = (int) (endMarker.startFrame + (playEndMarkerStartFrame ? 1f : 0f));
623 
624     setMinAndMaxFrame(startFrame, endFrame);
625   }
626 
627   /**
628    * @see #setMinFrame(int)
629    * @see #setMaxFrame(int)
630    */
631   public void setMinAndMaxFrame(final int minFrame, final int maxFrame) {
632     if (composition == null) {
633       lazyCompositionTasks.add(new LazyCompositionTask() {
634         @Override
635         public void run(LottieComposition composition) {
636           setMinAndMaxFrame(minFrame, maxFrame);
637         }
638       });
639       return;
640     }
641     // Adding 0.99 ensures that the maxFrame itself gets played.
642     animator.setMinAndMaxFrames(minFrame, maxFrame + 0.99f);
643   }
644 
645   /**
646    * @see #setMinProgress(float)
647    * @see #setMaxProgress(float)
648    */
649   public void setMinAndMaxProgress(
650       @FloatRange(from = 0f, to = 1f) final float minProgress,
651       @FloatRange(from = 0f, to = 1f) final float maxProgress) {
652     if (composition == null) {
653       lazyCompositionTasks.add(new LazyCompositionTask() {
654         @Override
655         public void run(LottieComposition composition) {
656           setMinAndMaxProgress(minProgress, maxProgress);
657         }
658       });
659       return;
660     }
661 
662     setMinAndMaxFrame((int) MiscUtils.lerp(composition.getStartFrame(), composition.getEndFrame(), minProgress),
663         (int) MiscUtils.lerp(composition.getStartFrame(), composition.getEndFrame(), maxProgress));
664   }
665 
666   /**
667    * Reverses the current animation speed. This does NOT play the animation.
668    *
669    * @see #setSpeed(float)
670    * @see #playAnimation()
671    * @see #resumeAnimation()
672    */
673   public void reverseAnimationSpeed() {
674     animator.reverseAnimationSpeed();
675   }
676 
677   /**
678    * Sets the playback speed. If speed < 0, the animation will play backwards.
679    */
680   public void setSpeed(float speed) {
681     animator.setSpeed(speed);
682   }
683 
684   /**
685    * Returns the current playback speed. This will be < 0 if the animation is playing backwards.
686    */
687   public float getSpeed() {
688     return animator.getSpeed();
689   }
690 
691   public void addAnimatorUpdateListener(ValueAnimator.AnimatorUpdateListener updateListener) {
692     animator.addUpdateListener(updateListener);
693   }
694 
695   public void removeAnimatorUpdateListener(ValueAnimator.AnimatorUpdateListener updateListener) {
696     animator.removeUpdateListener(updateListener);
697   }
698 
699   public void removeAllUpdateListeners() {
700     animator.removeAllUpdateListeners();
701     animator.addUpdateListener(progressUpdateListener);
702   }
703 
704   public void addAnimatorListener(Animator.AnimatorListener listener) {
705     animator.addListener(listener);
706   }
707 
708   public void removeAnimatorListener(Animator.AnimatorListener listener) {
709     animator.removeListener(listener);
710   }
711 
712   public void removeAllAnimatorListeners() {
713     animator.removeAllListeners();
714   }
715 
716   /**
717    * Sets the progress to the specified frame.
718    * If the composition isn't set yet, the progress will be set to the frame when
719    * it is.
720    */
721   public void setFrame(final int frame) {
722     if (composition == null) {
723       lazyCompositionTasks.add(new LazyCompositionTask() {
724         @Override
725         public void run(LottieComposition composition) {
726           setFrame(frame);
727         }
728       });
729       return;
730     }
731 
732     animator.setFrame(frame);
733   }
734 
735   /**
736    * Get the currently rendered frame.
737    */
738   public int getFrame() {
739     return (int) animator.getFrame();
740   }
741 
742   public void setProgress(@FloatRange(from = 0f, to = 1f) final float progress) {
743     if (composition == null) {
744       lazyCompositionTasks.add(new LazyCompositionTask() {
745         @Override
746         public void run(LottieComposition composition) {
747           setProgress(progress);
748         }
749       });
750       return;
751     }
752     L.beginSection("Drawable#setProgress");
753     animator.setFrame(MiscUtils.lerp(composition.getStartFrame(), composition.getEndFrame(), progress));
754     L.endSection("Drawable#setProgress");
755   }
756 
757   /**
758    * @see #setRepeatCount(int)
759    */
760   @Deprecated
761   public void loop(boolean loop) {
762     animator.setRepeatCount(loop ? ValueAnimator.INFINITE : 0);
763   }
764 
765   /**
766    * Defines what this animation should do when it reaches the end. This
767    * setting is applied only when the repeat count is either greater than
768    * 0 or {@link #INFINITE}. Defaults to {@link #RESTART}.
769    *
770    * @param mode {@link #RESTART} or {@link #REVERSE}
771    */
772   public void setRepeatMode(@RepeatMode int mode) {
773     animator.setRepeatMode(mode);
774   }
775 
776   /**
777    * Defines what this animation should do when it reaches the end.
778    *
779    * @return either one of {@link #REVERSE} or {@link #RESTART}
780    */
781   @RepeatMode
782   public int getRepeatMode() {
783     return animator.getRepeatMode();
784   }
785 
786   /**
787    * Sets how many times the animation should be repeated. If the repeat
788    * count is 0, the animation is never repeated. If the repeat count is
789    * greater than 0 or {@link #INFINITE}, the repeat mode will be taken
790    * into account. The repeat count is 0 by default.
791    *
792    * @param count the number of times the animation should be repeated
793    */
794   public void setRepeatCount(int count) {
795     animator.setRepeatCount(count);
796   }
797 
798   /**
799    * Defines how many times the animation should repeat. The default value
800    * is 0.
801    *
802    * @return the number of times the animation should repeat, or {@link #INFINITE}
803    */
804   public int getRepeatCount() {
805     return animator.getRepeatCount();
806   }
807 
808 
809   public boolean isLooping() {
810     return animator.getRepeatCount() == ValueAnimator.INFINITE;
811   }
812 
813   public boolean isAnimating() {
814     // On some versions of Android, this is called from the LottieAnimationView constructor, before animator was created.
815     // https://github.com/airbnb/lottie-android/issues/1430
816     if (animator == null) {
817       return false;
818     }
819     return animator.isRunning();
820   }
821 
822   void setSystemAnimationsAreEnabled(Boolean areEnabled) {
823     systemAnimationsEnabled = areEnabled;
824   }
825 
826 // </editor-fold>
827 
828   /**
829    * Set the scale on the current composition. The only cost of this function is re-rendering the
830    * current frame so you may call it frequent to scale something up or down.
831    * <p>
832    * The smaller the animation is, the better the performance will be. You may find that scaling an
833    * animation down then rendering it in a larger ImageView and letting ImageView scale it back up
834    * with a scaleType such as centerInside will yield better performance with little perceivable
835    * quality loss.
836    * <p>
837    * You can also use a fixed view width/height in conjunction with the normal ImageView
838    * scaleTypes centerCrop and centerInside.
839    */
840   public void setScale(float scale) {
841     this.scale = scale;
842     updateBounds();
843   }
844 
845   /**
846    * Use this if you can't bundle images with your app. This may be useful if you download the
847    * animations from the network or have the images saved to an SD Card. In that case, Lottie
848    * will defer the loading of the bitmap to this delegate.
849    * <p>
850    * Be wary if you are using many images, however. Lottie is designed to work with vector shapes
851    * from After Effects. If your images look like they could be represented with vector shapes,
852    * see if it is possible to convert them to shape layers and re-export your animation. Check
853    * the documentation at http://airbnb.io/lottie for more information about importing shapes from
854    * Sketch or Illustrator to avoid this.
855    */
856   public void setImageAssetDelegate(
857       @SuppressWarnings("NullableProblems") ImageAssetDelegate assetDelegate) {
858     this.imageAssetDelegate = assetDelegate;
859     if (imageAssetManager != null) {
860       imageAssetManager.setDelegate(assetDelegate);
861     }
862   }
863 
864   /**
865    * Use this to manually set fonts.
866    */
867   public void setFontAssetDelegate(
868       @SuppressWarnings("NullableProblems") FontAssetDelegate assetDelegate) {
869     this.fontAssetDelegate = assetDelegate;
870     if (fontAssetManager != null) {
871       fontAssetManager.setDelegate(assetDelegate);
872     }
873   }
874 
875   public void setTextDelegate(@SuppressWarnings("NullableProblems") TextDelegate textDelegate) {
876     this.textDelegate = textDelegate;
877   }
878 
879   @Nullable
880   public TextDelegate getTextDelegate() {
881     return textDelegate;
882   }
883 
884   public boolean useTextGlyphs() {
885     return textDelegate == null && composition.getCharacters().size() > 0;
886   }
887 
888   public float getScale() {
889     return scale;
890   }
891 
892   public LottieComposition getComposition() {
893     return composition;
894   }
895 
896   private void updateBounds() {
897     if (composition == null) {
898       return;
899     }
900     float scale = getScale();
901     setBounds(0, 0, (int) (composition.getBounds().width() * scale),
902         (int) (composition.getBounds().height() * scale));
903   }
904 
905   public void cancelAnimation() {
906     lazyCompositionTasks.clear();
907     animator.cancel();
908   }
909 
910   public void pauseAnimation() {
911     lazyCompositionTasks.clear();
912     animator.pauseAnimation();
913   }
914 
915   @FloatRange(from = 0f, to = 1f)
916   public float getProgress() {
917     return animator.getAnimatedValueAbsolute();
918   }
919 
920   @Override
921   public int getIntrinsicWidth() {
922     return composition == null ? -1 : (int) (composition.getBounds().width() * getScale());
923   }
924 
925   @Override
926   public int getIntrinsicHeight() {
927     return composition == null ? -1 : (int) (composition.getBounds().height() * getScale());
928   }
929 
930   /**
931    * Takes a {@link KeyPath}, potentially with wildcards or globstars and resolve it to a list of
932    * zero or more actual {@link KeyPath Keypaths} that exist in the current animation.
933    * <p>
934    * If you want to set value callbacks for any of these values, it is recommend to use the
935    * returned {@link KeyPath} objects because they will be internally resolved to their content
936    * and won't trigger a tree walk of the animation contents when applied.
937    */
938   public List<KeyPath> resolveKeyPath(KeyPath keyPath) {
939     if (compositionLayer == null) {
940       Logger.warning("Cannot resolve KeyPath. Composition is not set yet.");
941       return Collections.emptyList();
942     }
943     List<KeyPath> keyPaths = new ArrayList<>();
944     compositionLayer.resolveKeyPath(keyPath, 0, keyPaths, new KeyPath());
945     return keyPaths;
946   }
947 
948   /**
949    * Add an property callback for the specified {@link KeyPath}. This {@link KeyPath} can resolve
950    * to multiple contents. In that case, the callbacks's value will apply to all of them.
951    * <p>
952    * Internally, this will check if the {@link KeyPath} has already been resolved with
953    * {@link #resolveKeyPath(KeyPath)} and will resolve it if it hasn't.
954    */
955   public <T> void addValueCallback(
956       final KeyPath keyPath, final T property, final LottieValueCallback<T> callback) {
957     if (compositionLayer == null) {
958       lazyCompositionTasks.add(new LazyCompositionTask() {
959         @Override
960         public void run(LottieComposition composition) {
961           addValueCallback(keyPath, property, callback);
962         }
963       });
964       return;
965     }
966     boolean invalidate;
967     if (keyPath.getResolvedElement() != null) {
968       keyPath.getResolvedElement().addValueCallback(property, callback);
969       invalidate = true;
970     } else {
971       List<KeyPath> elements = resolveKeyPath(keyPath);
972 
973       for (int i = 0; i < elements.size(); i++) {
974         //noinspection ConstantConditions
975         elements.get(i).getResolvedElement().addValueCallback(property, callback);
976       }
977       invalidate = !elements.isEmpty();
978     }
979     if (invalidate) {
980       invalidateSelf();
981       if (property == LottieProperty.TIME_REMAP) {
982         // Time remapping values are read in setProgress. In order for the new value
983         // to apply, we have to re-set the progress with the current progress so that the
984         // time remapping can be reapplied.
985         setProgress(getProgress());
986       }
987     }
988   }
989 
990   /**
991    * Overload of {@link #addValueCallback(KeyPath, Object, LottieValueCallback)} that takes an interface. This allows you to use a single abstract
992    * method code block in Kotlin such as:
993    * drawable.addValueCallback(yourKeyPath, LottieProperty.COLOR) { yourColor }
994    */
995   public <T> void addValueCallback(KeyPath keyPath, T property,
996                                    final SimpleLottieValueCallback<T> callback) {
997     addValueCallback(keyPath, property, new LottieValueCallback<T>() {
998       @Override
999       public T getValue(LottieFrameInfo<T> frameInfo) {
1000         return callback.getValue(frameInfo);
1001       }
1002     });
1003   }
1004 
1005 
1006   /**
1007    * Allows you to modify or clear a bitmap that was loaded for an image either automatically
1008    * through {@link #setImagesAssetsFolder(String)} or with an {@link ImageAssetDelegate}.
1009    *
1010    * @return the previous Bitmap or null.
1011    */
1012   @Nullable
1013   public Bitmap updateBitmap(String id, @Nullable Bitmap bitmap) {
1014     ImageAssetManager bm = getImageAssetManager();
1015     if (bm == null) {
1016       Logger.warning("Cannot update bitmap. Most likely the drawable is not added to a View " +
1017           "which prevents Lottie from getting a Context.");
1018       return null;
1019     }
1020     Bitmap ret = bm.updateBitmap(id, bitmap);
1021     invalidateSelf();
1022     return ret;
1023   }
1024 
1025   @Nullable
1026   public Bitmap getImageAsset(String id) {
1027     ImageAssetManager bm = getImageAssetManager();
1028     if (bm != null) {
1029       return bm.bitmapForId(id);
1030     }
1031     return null;
1032   }
1033 
1034   private ImageAssetManager getImageAssetManager() {
1035     if (getCallback() == null) {
1036       // We can't get a bitmap since we can't get a Context from the callback.
1037       return null;
1038     }
1039 
1040     if (imageAssetManager != null && !imageAssetManager.hasSameContext(getContext())) {
1041       imageAssetManager = null;
1042     }
1043 
1044     if (imageAssetManager == null) {
1045       imageAssetManager = new ImageAssetManager(getCallback(),
1046           imageAssetsFolder, imageAssetDelegate, composition.getImages());
1047     }
1048 
1049     return imageAssetManager;
1050   }
1051 
1052   @Nullable
1053   public Typeface getTypeface(String fontFamily, String style) {
1054     FontAssetManager assetManager = getFontAssetManager();
1055     if (assetManager != null) {
1056       return assetManager.getTypeface(fontFamily, style);
1057     }
1058     return null;
1059   }
1060 
1061   private FontAssetManager getFontAssetManager() {
1062     if (getCallback() == null) {
1063       // We can't get a bitmap since we can't get a Context from the callback.
1064       return null;
1065     }
1066 
1067     if (fontAssetManager == null) {
1068       fontAssetManager = new FontAssetManager(getCallback(), fontAssetDelegate);
1069     }
1070 
1071     return fontAssetManager;
1072   }
1073 
1074   @Nullable
1075   private Context getContext() {
1076     Callback callback = getCallback();
1077     if (callback == null) {
1078       return null;
1079     }
1080 
1081     if (callback instanceof View) {
1082       return ((View) callback).getContext();
1083     }
1084     return null;
1085   }
1086 
1087   /**
1088    * These Drawable.Callback methods proxy the calls so that this is the drawable that is
1089    * actually invalidated, not a child one which will not pass the view's validateDrawable check.
1090    */
1091   @Override
1092   public void invalidateDrawable(@NonNull Drawable who) {
1093     Callback callback = getCallback();
1094     if (callback == null) {
1095       return;
1096     }
1097     callback.invalidateDrawable(this);
1098   }
1099 
1100   @Override
1101   public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
1102     Callback callback = getCallback();
1103     if (callback == null) {
1104       return;
1105     }
1106     callback.scheduleDrawable(this, what, when);
1107   }
1108 
1109   @Override
1110   public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
1111     Callback callback = getCallback();
1112     if (callback == null) {
1113       return;
1114     }
1115     callback.unscheduleDrawable(this, what);
1116   }
1117 
1118   void setScaleType(ImageView.ScaleType scaleType) {
1119     this.scaleType = scaleType;
1120   }
1121 
1122   /**
1123    * If the composition is larger than the canvas, we have to use a different method to scale it up.
1124    * See the comments in {@link #draw(Canvas)} for more info.
1125    */
1126   private float getMaxScale(@NonNull Canvas canvas) {
1127     float maxScaleX = canvas.getWidth() / (float) composition.getBounds().width();
1128     float maxScaleY = canvas.getHeight() / (float) composition.getBounds().height();
1129     return Math.min(maxScaleX, maxScaleY);
1130   }
1131 
1132   private void drawWithNewAspectRatio(Canvas canvas) {
1133     if (compositionLayer == null) {
1134       return;
1135     }
1136 
1137     int saveCount = -1;
1138     Rect bounds = getBounds();
1139     // In fitXY mode, the scale doesn't take effect.
1140     float scaleX = bounds.width() / (float) composition.getBounds().width();
1141     float scaleY = bounds.height() / (float) composition.getBounds().height();
1142 
1143     if (isExtraScaleEnabled) {
1144       float maxScale = Math.min(scaleX, scaleY);
1145       float extraScale = 1f;
1146       if (maxScale < 1f) {
1147         extraScale = extraScale / maxScale;
1148         scaleX = scaleX / extraScale;
1149         scaleY = scaleY / extraScale;
1150       }
1151 
1152       if (extraScale > 1) {
1153         saveCount = canvas.save();
1154         float halfWidth = bounds.width() / 2f;
1155         float halfHeight = bounds.height() / 2f;
1156         float scaledHalfWidth = halfWidth * maxScale;
1157         float scaledHalfHeight = halfHeight * maxScale;
1158 
1159         canvas.translate(
1160             halfWidth - scaledHalfWidth,
1161             halfHeight - scaledHalfHeight);
1162         canvas.scale(extraScale, extraScale, scaledHalfWidth, scaledHalfHeight);
1163       }
1164     }
1165 
1166     matrix.reset();
1167     matrix.preScale(scaleX, scaleY);
1168     compositionLayer.draw(canvas, matrix, alpha);
1169 
1170     if (saveCount > 0) {
1171       canvas.restoreToCount(saveCount);
1172     }
1173   }
1174 
1175   private void drawWithOriginalAspectRatio(Canvas canvas) {
1176     if (compositionLayer == null) {
1177       return;
1178     }
1179 
1180     float scale = this.scale;
1181     float extraScale = 1f;
1182     float maxScale = getMaxScale(canvas);
1183     if (scale > maxScale) {
1184       scale = maxScale;
1185       extraScale = this.scale / scale;
1186     }
1187 
1188     int saveCount = -1;
1189     if (extraScale > 1) {
1190       // This is a bit tricky...
1191       // We can't draw on a canvas larger than ViewConfiguration.get(context).getScaledMaximumDrawingCacheSize()
1192       // which works out to be roughly the size of the screen because Android can't generate a
1193       // bitmap large enough to render to.
1194       // As a result, we cap the scale such that it will never be wider/taller than the screen
1195       // and then only render in the top left corner of the canvas. We then use extraScale
1196       // to scale up the rest of the scale. However, since we rendered the animation to the top
1197       // left corner, we need to scale up and translate the canvas to zoom in on the top left
1198       // corner.
1199       saveCount = canvas.save();
1200       float halfWidth = composition.getBounds().width() / 2f;
1201       float halfHeight = composition.getBounds().height() / 2f;
1202       float scaledHalfWidth = halfWidth * scale;
1203       float scaledHalfHeight = halfHeight * scale;
1204 
1205       canvas.translate(
1206           getScale() * halfWidth - scaledHalfWidth,
1207           getScale() * halfHeight - scaledHalfHeight);
1208       canvas.scale(extraScale, extraScale, scaledHalfWidth, scaledHalfHeight);
1209     }
1210 
1211     matrix.reset();
1212     matrix.preScale(scale, scale);
1213     compositionLayer.draw(canvas, matrix, alpha);
1214 
1215     if (saveCount > 0) {
1216       canvas.restoreToCount(saveCount);
1217     }
1218   }
1219 
1220   private static class ColorFilterData {
1221 
1222     final String layerName;
1223     @Nullable
1224     final String contentName;
1225     @Nullable
1226     final ColorFilter colorFilter;
1227 
ColorFilterData(@ullable String layerName, @Nullable String contentName, @Nullable ColorFilter colorFilter)1228     ColorFilterData(@Nullable String layerName, @Nullable String contentName,
1229                     @Nullable ColorFilter colorFilter) {
1230       this.layerName = layerName;
1231       this.contentName = contentName;
1232       this.colorFilter = colorFilter;
1233     }
1234 
1235     @Override
hashCode()1236     public int hashCode() {
1237       int hashCode = 17;
1238       if (layerName != null) {
1239         hashCode = hashCode * 31 * layerName.hashCode();
1240       }
1241 
1242       if (contentName != null) {
1243         hashCode = hashCode * 31 * contentName.hashCode();
1244       }
1245       return hashCode;
1246     }
1247 
1248     @Override
equals(Object obj)1249     public boolean equals(Object obj) {
1250       if (this == obj) {
1251         return true;
1252       }
1253 
1254       if (!(obj instanceof ColorFilterData)) {
1255         return false;
1256       }
1257 
1258       final ColorFilterData other = (ColorFilterData) obj;
1259 
1260       return hashCode() == other.hashCode() && colorFilter == other.colorFilter;
1261 
1262     }
1263   }
1264 }
1265