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