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