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