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