1 package com.airbnb.lottie; 2 3 import android.animation.Animator; 4 import android.animation.ValueAnimator; 5 import android.content.Context; 6 import android.content.res.TypedArray; 7 import android.graphics.Bitmap; 8 import android.graphics.Color; 9 import android.graphics.ColorFilter; 10 import android.graphics.drawable.Drawable; 11 import android.os.Build; 12 import android.os.Parcel; 13 import android.os.Parcelable; 14 import android.text.TextUtils; 15 import android.util.AttributeSet; 16 import android.util.Log; 17 import android.view.View; 18 19 import androidx.annotation.DrawableRes; 20 import androidx.annotation.FloatRange; 21 import androidx.annotation.MainThread; 22 import androidx.annotation.NonNull; 23 import androidx.annotation.Nullable; 24 import androidx.annotation.RawRes; 25 import androidx.appcompat.widget.AppCompatImageView; 26 import androidx.core.view.ViewCompat; 27 28 import com.airbnb.lottie.model.KeyPath; 29 import com.airbnb.lottie.parser.moshi.JsonReader; 30 import com.airbnb.lottie.utils.Logger; 31 import com.airbnb.lottie.utils.Utils; 32 import com.airbnb.lottie.value.LottieFrameInfo; 33 import com.airbnb.lottie.value.LottieValueCallback; 34 import com.airbnb.lottie.value.SimpleLottieValueCallback; 35 36 import java.io.ByteArrayInputStream; 37 import java.io.InputStream; 38 import java.util.HashSet; 39 import java.util.List; 40 import java.util.Set; 41 42 import static com.airbnb.lottie.RenderMode.HARDWARE; 43 44 /** 45 * This view will load, deserialize, and display an After Effects animation exported with 46 * bodymovin (https://github.com/bodymovin/bodymovin). 47 * <p> 48 * You may set the animation in one of two ways: 49 * 1) Attrs: {@link R.styleable#LottieAnimationView_lottie_fileName} 50 * 2) Programmatically: 51 * {@link #setAnimation(String)} 52 * {@link #setAnimation(JsonReader, String)} 53 * {@link #setAnimationFromJson(String, String)} 54 * {@link #setAnimationFromUrl(String)} 55 * {@link #setComposition(LottieComposition)} 56 * <p> 57 * You can set a default cache strategy with {@link R.attr#lottie_cacheStrategy}. 58 * <p> 59 * You can manually set the progress of the animation with {@link #setProgress(float)} or 60 * {@link R.attr#lottie_progress} 61 * 62 * @see <a href="http://airbnb.io/lottie">Full Documentation</a> 63 */ 64 @SuppressWarnings({"unused", "WeakerAccess"}) public class LottieAnimationView extends AppCompatImageView { 65 66 private static final String TAG = LottieAnimationView.class.getSimpleName(); 67 private static final LottieListener<Throwable> DEFAULT_FAILURE_LISTENER = new LottieListener<Throwable>() { 68 @Override public void onResult(Throwable throwable) { 69 // By default, fail silently for network errors. 70 if (Utils.isNetworkException(throwable)) { 71 Logger.warning("Unable to load composition.", throwable); 72 return; 73 } 74 throw new IllegalStateException("Unable to parse composition", throwable); 75 } 76 }; 77 78 private final LottieListener<LottieComposition> loadedListener = new LottieListener<LottieComposition>() { 79 @Override public void onResult(LottieComposition composition) { 80 setComposition(composition); 81 } 82 }; 83 84 private final LottieListener<Throwable> wrappedFailureListener = new LottieListener<Throwable>() { 85 @Override 86 public void onResult(Throwable result) { 87 if (fallbackResource != 0) { 88 setImageResource(fallbackResource); 89 } 90 LottieListener<Throwable> l = failureListener == null ? DEFAULT_FAILURE_LISTENER : failureListener; 91 l.onResult(result); 92 } 93 }; 94 @Nullable private LottieListener<Throwable> failureListener; 95 @DrawableRes private int fallbackResource = 0; 96 97 private final LottieDrawable lottieDrawable = new LottieDrawable(); 98 private boolean isInitialized; 99 private String animationName; 100 private @RawRes int animationResId; 101 private boolean wasAnimatingWhenNotShown = false; 102 private boolean wasAnimatingWhenDetached = false; 103 private boolean autoPlay = false; 104 private boolean cacheComposition = true; 105 private RenderMode renderMode = RenderMode.AUTOMATIC; 106 private Set<LottieOnCompositionLoadedListener> lottieOnCompositionLoadedListeners = new HashSet<>(); 107 /** 108 * Prevents a StackOverflowException on 4.4 in which getDrawingCache() calls buildDrawingCache(). 109 * This isn't a great solution but it works and has very little performance overhead. 110 * At some point in the future, the original goal of falling back to hardware rendering when 111 * the animation is set to software rendering but it is too large to fit in a software bitmap 112 * should be reevaluated. 113 */ 114 private int buildDrawingCacheDepth = 0; 115 116 @Nullable private LottieTask<LottieComposition> compositionTask; 117 /** Can be null because it is created async */ 118 @Nullable private LottieComposition composition; 119 LottieAnimationView(Context context)120 public LottieAnimationView(Context context) { 121 super(context); 122 init(null); 123 } 124 LottieAnimationView(Context context, AttributeSet attrs)125 public LottieAnimationView(Context context, AttributeSet attrs) { 126 super(context, attrs); 127 init(attrs); 128 } 129 LottieAnimationView(Context context, AttributeSet attrs, int defStyleAttr)130 public LottieAnimationView(Context context, AttributeSet attrs, int defStyleAttr) { 131 super(context, attrs, defStyleAttr); 132 init(attrs); 133 } 134 init(@ullable AttributeSet attrs)135 private void init(@Nullable AttributeSet attrs) { 136 TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.LottieAnimationView); 137 if (!isInEditMode()) { 138 cacheComposition = ta.getBoolean(R.styleable.LottieAnimationView_lottie_cacheComposition, true); 139 boolean hasRawRes = ta.hasValue(R.styleable.LottieAnimationView_lottie_rawRes); 140 boolean hasFileName = ta.hasValue(R.styleable.LottieAnimationView_lottie_fileName); 141 boolean hasUrl = ta.hasValue(R.styleable.LottieAnimationView_lottie_url); 142 if (hasRawRes && hasFileName) { 143 throw new IllegalArgumentException("lottie_rawRes and lottie_fileName cannot be used at " + 144 "the same time. Please use only one at once."); 145 } else if (hasRawRes) { 146 int rawResId = ta.getResourceId(R.styleable.LottieAnimationView_lottie_rawRes, 0); 147 if (rawResId != 0) { 148 setAnimation(rawResId); 149 } 150 } else if (hasFileName) { 151 String fileName = ta.getString(R.styleable.LottieAnimationView_lottie_fileName); 152 if (fileName != null) { 153 setAnimation(fileName); 154 } 155 } else if (hasUrl) { 156 String url = ta.getString(R.styleable.LottieAnimationView_lottie_url); 157 if (url != null) { 158 setAnimationFromUrl(url); 159 } 160 } 161 162 setFallbackResource(ta.getResourceId(R.styleable.LottieAnimationView_lottie_fallbackRes, 0)); 163 } 164 if (ta.getBoolean(R.styleable.LottieAnimationView_lottie_autoPlay, false)) { 165 wasAnimatingWhenDetached = true; 166 autoPlay = true; 167 } 168 169 if (ta.getBoolean(R.styleable.LottieAnimationView_lottie_loop, false)) { 170 lottieDrawable.setRepeatCount(LottieDrawable.INFINITE); 171 } 172 173 if (ta.hasValue(R.styleable.LottieAnimationView_lottie_repeatMode)) { 174 setRepeatMode(ta.getInt(R.styleable.LottieAnimationView_lottie_repeatMode, 175 LottieDrawable.RESTART)); 176 } 177 178 if (ta.hasValue(R.styleable.LottieAnimationView_lottie_repeatCount)) { 179 setRepeatCount(ta.getInt(R.styleable.LottieAnimationView_lottie_repeatCount, 180 LottieDrawable.INFINITE)); 181 } 182 183 if (ta.hasValue(R.styleable.LottieAnimationView_lottie_speed)) { 184 setSpeed(ta.getFloat(R.styleable.LottieAnimationView_lottie_speed, 1f)); 185 } 186 187 setImageAssetsFolder(ta.getString(R.styleable.LottieAnimationView_lottie_imageAssetsFolder)); 188 setProgress(ta.getFloat(R.styleable.LottieAnimationView_lottie_progress, 0)); 189 enableMergePathsForKitKatAndAbove(ta.getBoolean( 190 R.styleable.LottieAnimationView_lottie_enableMergePathsForKitKatAndAbove, false)); 191 if (ta.hasValue(R.styleable.LottieAnimationView_lottie_colorFilter)) { 192 SimpleColorFilter filter = new SimpleColorFilter( 193 ta.getColor(R.styleable.LottieAnimationView_lottie_colorFilter, Color.TRANSPARENT)); 194 KeyPath keyPath = new KeyPath("**"); 195 LottieValueCallback<ColorFilter> callback = new LottieValueCallback<ColorFilter>(filter); 196 addValueCallback(keyPath, LottieProperty.COLOR_FILTER, callback); 197 } 198 if (ta.hasValue(R.styleable.LottieAnimationView_lottie_scale)) { 199 lottieDrawable.setScale(ta.getFloat(R.styleable.LottieAnimationView_lottie_scale, 1f)); 200 } 201 202 if (ta.hasValue(R.styleable.LottieAnimationView_lottie_renderMode)) { 203 int renderModeOrdinal = ta.getInt(R.styleable.LottieAnimationView_lottie_renderMode, RenderMode.AUTOMATIC.ordinal()); 204 if (renderModeOrdinal >= RenderMode.values().length) { 205 renderModeOrdinal = RenderMode.AUTOMATIC.ordinal(); 206 } 207 setRenderMode(RenderMode.values()[renderModeOrdinal]); 208 } 209 210 if (getScaleType() != null) { 211 lottieDrawable.setScaleType(getScaleType()); 212 } 213 ta.recycle(); 214 215 lottieDrawable.setSystemAnimationsAreEnabled(Utils.getAnimationScale(getContext()) != 0f); 216 217 enableOrDisableHardwareLayer(); 218 isInitialized = true; 219 } 220 setImageResource(int resId)221 @Override public void setImageResource(int resId) { 222 cancelLoaderTask(); 223 super.setImageResource(resId); 224 } 225 setImageDrawable(Drawable drawable)226 @Override public void setImageDrawable(Drawable drawable) { 227 cancelLoaderTask(); 228 super.setImageDrawable(drawable); 229 } 230 setImageBitmap(Bitmap bm)231 @Override public void setImageBitmap(Bitmap bm) { 232 cancelLoaderTask(); 233 super.setImageBitmap(bm); 234 } 235 invalidateDrawable(@onNull Drawable dr)236 @Override public void invalidateDrawable(@NonNull Drawable dr) { 237 if (getDrawable() == lottieDrawable) { 238 // We always want to invalidate the root drawable so it redraws the whole drawable. 239 // Eventually it would be great to be able to invalidate just the changed region. 240 super.invalidateDrawable(lottieDrawable); 241 } else { 242 // Otherwise work as regular ImageView 243 super.invalidateDrawable(dr); 244 } 245 } 246 onSaveInstanceState()247 @Override protected Parcelable onSaveInstanceState() { 248 Parcelable superState = super.onSaveInstanceState(); 249 SavedState ss = new SavedState(superState); 250 ss.animationName = animationName; 251 ss.animationResId = animationResId; 252 ss.progress = lottieDrawable.getProgress(); 253 ss.isAnimating = lottieDrawable.isAnimating() || (!ViewCompat.isAttachedToWindow(this) && wasAnimatingWhenDetached); 254 ss.imageAssetsFolder = lottieDrawable.getImageAssetsFolder(); 255 ss.repeatMode = lottieDrawable.getRepeatMode(); 256 ss.repeatCount = lottieDrawable.getRepeatCount(); 257 return ss; 258 } 259 onRestoreInstanceState(Parcelable state)260 @Override protected void onRestoreInstanceState(Parcelable state) { 261 if (!(state instanceof SavedState)) { 262 super.onRestoreInstanceState(state); 263 return; 264 } 265 266 SavedState ss = (SavedState) state; 267 super.onRestoreInstanceState(ss.getSuperState()); 268 animationName = ss.animationName; 269 if (!TextUtils.isEmpty(animationName)) { 270 setAnimation(animationName); 271 } 272 animationResId = ss.animationResId; 273 if (animationResId != 0) { 274 setAnimation(animationResId); 275 } 276 setProgress(ss.progress); 277 if (ss.isAnimating) { 278 playAnimation(); 279 } 280 lottieDrawable.setImagesAssetsFolder(ss.imageAssetsFolder); 281 setRepeatMode(ss.repeatMode); 282 setRepeatCount(ss.repeatCount); 283 } 284 285 @Override onVisibilityChanged(@onNull View changedView, int visibility)286 protected void onVisibilityChanged(@NonNull View changedView, int visibility) { 287 // This can happen on older versions of Android because onVisibilityChanged gets called from the 288 // constructor of View so this will get called before lottieDrawable gets initialized. 289 // https://github.com/airbnb/lottie-android/issues/1143 290 // A simple null check on lottieDrawable would not work because when using Proguard optimization, a 291 // null check on a final field gets removed. As "usually" final fields cannot be null. 292 // However because this is called by super (View) before the initializer of the LottieAnimationView 293 // is called, it actually can be null here. 294 // Working around this by using a non final boolean that is set to true after the class initializer 295 // has run. 296 if (!isInitialized) { 297 return; 298 } 299 if (isShown()) { 300 if (wasAnimatingWhenNotShown) { 301 resumeAnimation(); 302 wasAnimatingWhenNotShown = false; 303 } 304 } else { 305 if (isAnimating()) { 306 pauseAnimation(); 307 wasAnimatingWhenNotShown = true; 308 } 309 } 310 } 311 onAttachedToWindow()312 @Override protected void onAttachedToWindow() { 313 super.onAttachedToWindow(); 314 if (autoPlay || wasAnimatingWhenDetached) { 315 playAnimation(); 316 // Autoplay from xml should only apply once. 317 autoPlay = false; 318 wasAnimatingWhenDetached = false; 319 } 320 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { 321 // This is needed to mimic newer platform behavior. 322 // https://stackoverflow.com/a/53625860/715633 323 onVisibilityChanged(this, getVisibility()); 324 } 325 } 326 onDetachedFromWindow()327 @Override protected void onDetachedFromWindow() { 328 if (isAnimating()) { 329 cancelAnimation(); 330 wasAnimatingWhenDetached = true; 331 } 332 super.onDetachedFromWindow(); 333 } 334 335 /** 336 * Enable this to get merge path support for devices running KitKat (19) and above. 337 * 338 * Merge paths currently don't work if the the operand shape is entirely contained within the 339 * first shape. If you need to cut out one shape from another shape, use an even-odd fill type 340 * instead of using merge paths. 341 */ enableMergePathsForKitKatAndAbove(boolean enable)342 public void enableMergePathsForKitKatAndAbove(boolean enable) { 343 lottieDrawable.enableMergePathsForKitKatAndAbove(enable); 344 } 345 346 /** 347 * Returns whether merge paths are enabled for KitKat and above. 348 */ isMergePathsEnabledForKitKatAndAbove()349 public boolean isMergePathsEnabledForKitKatAndAbove() { 350 return lottieDrawable.isMergePathsEnabledForKitKatAndAbove(); 351 } 352 353 /** 354 * If set to true, all future compositions that are set will be cached so that they don't need to be parsed 355 * next time they are loaded. This won't apply to compositions that have already been loaded. 356 * 357 * Defaults to true. 358 * 359 * {@link R.attr#lottie_cacheComposition} 360 */ setCacheComposition(boolean cacheComposition)361 public void setCacheComposition(boolean cacheComposition) { 362 this.cacheComposition = cacheComposition; 363 } 364 365 /** 366 * Sets the animation from a file in the raw directory. 367 * This will load and deserialize the file asynchronously. 368 */ setAnimation(@awRes final int rawRes)369 public void setAnimation(@RawRes final int rawRes) { 370 this.animationResId = rawRes; 371 animationName = null; 372 LottieTask<LottieComposition> task = cacheComposition ? 373 LottieCompositionFactory.fromRawRes(getContext(), rawRes) : LottieCompositionFactory.fromRawRes(getContext(), rawRes, null); 374 setCompositionTask(task); 375 } 376 setAnimation(final String assetName)377 public void setAnimation(final String assetName) { 378 this.animationName = assetName; 379 animationResId = 0; 380 LottieTask<LottieComposition> task = cacheComposition ? 381 LottieCompositionFactory.fromAsset(getContext(), assetName) : LottieCompositionFactory.fromAsset(getContext(), assetName, null); 382 setCompositionTask(task); 383 } 384 385 /** 386 * @see #setAnimationFromJson(String, String) 387 */ 388 @Deprecated setAnimationFromJson(String jsonString)389 public void setAnimationFromJson(String jsonString) { 390 setAnimationFromJson(jsonString, null); 391 } 392 393 /** 394 * Sets the animation from json string. This is the ideal API to use when loading an animation 395 * over the network because you can use the raw response body here and a conversion to a 396 * JSONObject never has to be done. 397 */ setAnimationFromJson(String jsonString, @Nullable String cacheKey)398 public void setAnimationFromJson(String jsonString, @Nullable String cacheKey) { 399 setAnimation(new ByteArrayInputStream(jsonString.getBytes()), cacheKey); 400 } 401 402 /** 403 * Sets the animation from an arbitrary InputStream. 404 * This will load and deserialize the file asynchronously. 405 * <p> 406 * This is particularly useful for animations loaded from the network. You can fetch the 407 * bodymovin json from the network and pass it directly here. 408 */ setAnimation(InputStream stream, @Nullable String cacheKey)409 public void setAnimation(InputStream stream, @Nullable String cacheKey) { 410 setCompositionTask(LottieCompositionFactory.fromJsonInputStream(stream, cacheKey)); 411 } 412 413 /** 414 * Load a lottie animation from a url. The url can be a json file or a zip file. Use a zip file if you have images. Simply zip them together and lottie 415 * will unzip and link the images automatically. 416 * 417 * Under the hood, Lottie uses Java HttpURLConnection because it doesn't require any transitive networking dependencies. It will download the file 418 * to the application cache under a temporary name. If the file successfully parses to a composition, it will rename the temporary file to one that 419 * can be accessed immediately for subsequent requests. If the file does not parse to a composition, the temporary file will be deleted. 420 */ setAnimationFromUrl(String url)421 public void setAnimationFromUrl(String url) { 422 LottieTask<LottieComposition> task = cacheComposition ? 423 LottieCompositionFactory.fromUrl(getContext(), url) : LottieCompositionFactory.fromUrl(getContext(), url, null); 424 setCompositionTask(task); 425 } 426 427 /** 428 * Set a default failure listener that will be called if any of the setAnimation APIs fail for any reason. 429 * This can be used to replace the default behavior. 430 * 431 * The default behavior will log any network errors and rethrow all other exceptions. 432 * 433 * If you are loading an animation from the network, errors may occur if your user has no internet. 434 * You can use this listener to retry the download or you can have it default to an error drawable 435 * with {@link #setFallbackResource(int)}. 436 * 437 * Unless you are using {@link #setAnimationFromUrl(String)}, errors are unexpected. 438 * 439 * Set the listener to null to revert to the default behavior. 440 */ setFailureListener(@ullable LottieListener<Throwable> failureListener)441 public void setFailureListener(@Nullable LottieListener<Throwable> failureListener) { 442 this.failureListener = failureListener; 443 } 444 445 /** 446 * Set a drawable that will be rendered if the LottieComposition fails to load for any reason. 447 * Unless you are using {@link #setAnimationFromUrl(String)}, this is an unexpected error and 448 * you should handle it with {@link #setFailureListener(LottieListener)}. 449 * 450 * If this is a network animation, you may use this to show an error to the user or 451 * you can use a failure listener to retry the download. 452 */ setFallbackResource(@rawableRes int fallbackResource)453 public void setFallbackResource(@DrawableRes int fallbackResource) { 454 this.fallbackResource = fallbackResource; 455 } 456 setCompositionTask(LottieTask<LottieComposition> compositionTask)457 private void setCompositionTask(LottieTask<LottieComposition> compositionTask) { 458 clearComposition(); 459 cancelLoaderTask(); 460 this.compositionTask = compositionTask 461 .addListener(loadedListener) 462 .addFailureListener(wrappedFailureListener); 463 } 464 cancelLoaderTask()465 private void cancelLoaderTask() { 466 if (compositionTask != null) { 467 compositionTask.removeListener(loadedListener); 468 compositionTask.removeFailureListener(wrappedFailureListener); 469 } 470 } 471 472 /** 473 * Sets a composition. 474 * You can set a default cache strategy if this view was inflated with xml by 475 * using {@link R.attr#lottie_cacheStrategy}. 476 */ setComposition(@onNull LottieComposition composition)477 public void setComposition(@NonNull LottieComposition composition) { 478 if (L.DBG) { 479 Log.v(TAG, "Set Composition \n" + composition); 480 } 481 lottieDrawable.setCallback(this); 482 483 this.composition = composition; 484 boolean isNewComposition = lottieDrawable.setComposition(composition); 485 enableOrDisableHardwareLayer(); 486 if (getDrawable() == lottieDrawable && !isNewComposition) { 487 // We can avoid re-setting the drawable, and invalidating the view, since the composition 488 // hasn't changed. 489 return; 490 } 491 492 // If you set a different composition on the view, the bounds will not update unless 493 // the drawable is different than the original. 494 setImageDrawable(null); 495 setImageDrawable(lottieDrawable); 496 497 // This is needed to makes sure that the animation is properly played/paused for the current visibility state. 498 // It is possible that the drawable had a lazy composition task to play the animation but this view subsequently 499 // became invisible. Comment this out and run the espresso tests to see a failing test. 500 onVisibilityChanged(this, getVisibility()); 501 502 requestLayout(); 503 504 for (LottieOnCompositionLoadedListener lottieOnCompositionLoadedListener : lottieOnCompositionLoadedListeners) { 505 lottieOnCompositionLoadedListener.onCompositionLoaded(composition); 506 } 507 508 } 509 getComposition()510 @Nullable public LottieComposition getComposition() { 511 return composition; 512 } 513 514 /** 515 * Returns whether or not any layers in this composition has masks. 516 */ hasMasks()517 public boolean hasMasks() { 518 return lottieDrawable.hasMasks(); 519 } 520 521 /** 522 * Returns whether or not any layers in this composition has a matte layer. 523 */ hasMatte()524 public boolean hasMatte() { 525 return lottieDrawable.hasMatte(); 526 } 527 528 /** 529 * Plays the animation from the beginning. If speed is < 0, it will start at the end 530 * and play towards the beginning 531 */ 532 @MainThread playAnimation()533 public void playAnimation() { 534 if (isShown()) { 535 lottieDrawable.playAnimation(); 536 enableOrDisableHardwareLayer(); 537 } else { 538 wasAnimatingWhenNotShown = true; 539 } 540 } 541 542 /** 543 * Continues playing the animation from its current position. If speed < 0, it will play backwards 544 * from the current position. 545 */ 546 @MainThread resumeAnimation()547 public void resumeAnimation() { 548 if (isShown()) { 549 lottieDrawable.resumeAnimation(); 550 enableOrDisableHardwareLayer(); 551 } else { 552 wasAnimatingWhenNotShown = true; 553 } 554 } 555 556 /** 557 * Sets the minimum frame that the animation will start from when playing or looping. 558 */ setMinFrame(int startFrame)559 public void setMinFrame(int startFrame) { 560 lottieDrawable.setMinFrame(startFrame); 561 } 562 563 /** 564 * Returns the minimum frame set by {@link #setMinFrame(int)} or {@link #setMinProgress(float)} 565 */ getMinFrame()566 public float getMinFrame() { 567 return lottieDrawable.getMinFrame(); 568 } 569 570 /** 571 * Sets the minimum progress that the animation will start from when playing or looping. 572 */ setMinProgress(float startProgress)573 public void setMinProgress(float startProgress) { 574 lottieDrawable.setMinProgress(startProgress); 575 } 576 577 /** 578 * Sets the maximum frame that the animation will end at when playing or looping. 579 */ setMaxFrame(int endFrame)580 public void setMaxFrame(int endFrame) { 581 lottieDrawable.setMaxFrame(endFrame); 582 } 583 584 /** 585 * Returns the maximum frame set by {@link #setMaxFrame(int)} or {@link #setMaxProgress(float)} 586 */ getMaxFrame()587 public float getMaxFrame() { 588 return lottieDrawable.getMaxFrame(); 589 } 590 591 /** 592 * Sets the maximum progress that the animation will end at when playing or looping. 593 */ setMaxProgress(@loatRangefrom = 0f, to = 1f) float endProgress)594 public void setMaxProgress(@FloatRange(from = 0f, to = 1f) float endProgress) { 595 lottieDrawable.setMaxProgress(endProgress); 596 } 597 598 /** 599 * Sets the minimum frame to the start time of the specified marker. 600 * @throws IllegalArgumentException if the marker is not found. 601 */ setMinFrame(String markerName)602 public void setMinFrame(String markerName) { 603 lottieDrawable.setMinFrame(markerName); 604 } 605 606 /** 607 * Sets the maximum frame to the start time + duration of the specified marker. 608 * @throws IllegalArgumentException if the marker is not found. 609 */ setMaxFrame(String markerName)610 public void setMaxFrame(String markerName) { 611 lottieDrawable.setMaxFrame(markerName); 612 } 613 614 /** 615 * Sets the minimum and maximum frame to the start time and start time + duration 616 * of the specified marker. 617 * @throws IllegalArgumentException if the marker is not found. 618 */ setMinAndMaxFrame(String markerName)619 public void setMinAndMaxFrame(String markerName) { 620 lottieDrawable.setMinAndMaxFrame(markerName); 621 } 622 623 /** 624 * Sets the minimum and maximum frame to the start marker start and the maximum frame to the end marker start. 625 * playEndMarkerStartFrame determines whether or not to play the frame that the end marker is on. If the end marker 626 * represents the end of the section that you want, it should be true. If the marker represents the beginning of the 627 * next section, it should be false. 628 * 629 * @throws IllegalArgumentException if either marker is not found. 630 */ setMinAndMaxFrame(final String startMarkerName, final String endMarkerName, final boolean playEndMarkerStartFrame)631 public void setMinAndMaxFrame(final String startMarkerName, final String endMarkerName, final boolean playEndMarkerStartFrame) { 632 lottieDrawable.setMinAndMaxFrame(startMarkerName, endMarkerName, playEndMarkerStartFrame); 633 } 634 635 /** 636 * @see #setMinFrame(int) 637 * @see #setMaxFrame(int) 638 */ setMinAndMaxFrame(int minFrame, int maxFrame)639 public void setMinAndMaxFrame(int minFrame, int maxFrame) { 640 lottieDrawable.setMinAndMaxFrame(minFrame, maxFrame); 641 } 642 643 /** 644 * @see #setMinProgress(float) 645 * @see #setMaxProgress(float) 646 */ setMinAndMaxProgress( @loatRangefrom = 0f, to = 1f) float minProgress, @FloatRange(from = 0f, to = 1f) float maxProgress)647 public void setMinAndMaxProgress( 648 @FloatRange(from = 0f, to = 1f) float minProgress, 649 @FloatRange(from = 0f, to = 1f) float maxProgress) { 650 lottieDrawable.setMinAndMaxProgress(minProgress, maxProgress); 651 } 652 653 /** 654 * Reverses the current animation speed. This does NOT play the animation. 655 * @see #setSpeed(float) 656 * @see #playAnimation() 657 * @see #resumeAnimation() 658 */ reverseAnimationSpeed()659 public void reverseAnimationSpeed() { 660 lottieDrawable.reverseAnimationSpeed(); 661 } 662 663 /** 664 * Sets the playback speed. If speed < 0, the animation will play backwards. 665 */ setSpeed(float speed)666 public void setSpeed(float speed) { 667 lottieDrawable.setSpeed(speed); 668 } 669 670 /** 671 * Returns the current playback speed. This will be < 0 if the animation is playing backwards. 672 */ getSpeed()673 public float getSpeed() { 674 return lottieDrawable.getSpeed(); 675 } 676 addAnimatorUpdateListener(ValueAnimator.AnimatorUpdateListener updateListener)677 public void addAnimatorUpdateListener(ValueAnimator.AnimatorUpdateListener updateListener) { 678 lottieDrawable.addAnimatorUpdateListener(updateListener); 679 } 680 removeUpdateListener(ValueAnimator.AnimatorUpdateListener updateListener)681 public void removeUpdateListener(ValueAnimator.AnimatorUpdateListener updateListener) { 682 lottieDrawable.removeAnimatorUpdateListener(updateListener); 683 } 684 removeAllUpdateListeners()685 public void removeAllUpdateListeners() { 686 lottieDrawable.removeAllUpdateListeners(); 687 } 688 addAnimatorListener(Animator.AnimatorListener listener)689 public void addAnimatorListener(Animator.AnimatorListener listener) { 690 lottieDrawable.addAnimatorListener(listener); 691 } 692 removeAnimatorListener(Animator.AnimatorListener listener)693 public void removeAnimatorListener(Animator.AnimatorListener listener) { 694 lottieDrawable.removeAnimatorListener(listener); 695 } 696 removeAllAnimatorListeners()697 public void removeAllAnimatorListeners() { 698 lottieDrawable.removeAllAnimatorListeners(); 699 } 700 701 /** 702 * @see #setRepeatCount(int) 703 */ 704 @Deprecated loop(boolean loop)705 public void loop(boolean loop) { 706 lottieDrawable.setRepeatCount(loop ? ValueAnimator.INFINITE : 0); 707 } 708 709 /** 710 * Defines what this animation should do when it reaches the end. This 711 * setting is applied only when the repeat count is either greater than 712 * 0 or {@link LottieDrawable#INFINITE}. Defaults to {@link LottieDrawable#RESTART}. 713 * 714 * @param mode {@link LottieDrawable#RESTART} or {@link LottieDrawable#REVERSE} 715 */ setRepeatMode(@ottieDrawable.RepeatMode int mode)716 public void setRepeatMode(@LottieDrawable.RepeatMode int mode) { 717 lottieDrawable.setRepeatMode(mode); 718 } 719 720 /** 721 * Defines what this animation should do when it reaches the end. 722 * 723 * @return either one of {@link LottieDrawable#REVERSE} or {@link LottieDrawable#RESTART} 724 */ 725 @LottieDrawable.RepeatMode getRepeatMode()726 public int getRepeatMode() { 727 return lottieDrawable.getRepeatMode(); 728 } 729 730 /** 731 * Sets how many times the animation should be repeated. If the repeat 732 * count is 0, the animation is never repeated. If the repeat count is 733 * greater than 0 or {@link LottieDrawable#INFINITE}, the repeat mode will be taken 734 * into account. The repeat count is 0 by default. 735 * 736 * @param count the number of times the animation should be repeated 737 */ setRepeatCount(int count)738 public void setRepeatCount(int count) { 739 lottieDrawable.setRepeatCount(count); 740 } 741 742 /** 743 * Defines how many times the animation should repeat. The default value 744 * is 0. 745 * 746 * @return the number of times the animation should repeat, or {@link LottieDrawable#INFINITE} 747 */ getRepeatCount()748 public int getRepeatCount() { 749 return lottieDrawable.getRepeatCount(); 750 } 751 isAnimating()752 public boolean isAnimating() { 753 return lottieDrawable.isAnimating(); 754 } 755 756 /** 757 * If you use image assets, you must explicitly specify the folder in assets/ in which they are 758 * located because bodymovin uses the name filenames across all compositions (img_#). 759 * Do NOT rename the images themselves. 760 * 761 * If your images are located in src/main/assets/airbnb_loader/ then call 762 * `setImageAssetsFolder("airbnb_loader/");`. 763 * 764 * Be wary if you are using many images, however. Lottie is designed to work with vector shapes 765 * from After Effects. If your images look like they could be represented with vector shapes, 766 * see if it is possible to convert them to shape layers and re-export your animation. Check 767 * the documentation at http://airbnb.io/lottie for more information about importing shapes from 768 * Sketch or Illustrator to avoid this. 769 */ setImageAssetsFolder(String imageAssetsFolder)770 public void setImageAssetsFolder(String imageAssetsFolder) { 771 lottieDrawable.setImagesAssetsFolder(imageAssetsFolder); 772 } 773 774 @Nullable getImageAssetsFolder()775 public String getImageAssetsFolder() { 776 return lottieDrawable.getImageAssetsFolder(); 777 } 778 779 /** 780 * Allows you to modify or clear a bitmap that was loaded for an image either automatically 781 * through {@link #setImageAssetsFolder(String)} or with an {@link ImageAssetDelegate}. 782 * 783 * @return the previous Bitmap or null. 784 */ 785 @Nullable updateBitmap(String id, @Nullable Bitmap bitmap)786 public Bitmap updateBitmap(String id, @Nullable Bitmap bitmap) { 787 return lottieDrawable.updateBitmap(id, bitmap); 788 } 789 790 /** 791 * Use this if you can't bundle images with your app. This may be useful if you download the 792 * animations from the network or have the images saved to an SD Card. In that case, Lottie 793 * will defer the loading of the bitmap to this delegate. 794 * 795 * Be wary if you are using many images, however. Lottie is designed to work with vector shapes 796 * from After Effects. If your images look like they could be represented with vector shapes, 797 * see if it is possible to convert them to shape layers and re-export your animation. Check 798 * the documentation at http://airbnb.io/lottie for more information about importing shapes from 799 * Sketch or Illustrator to avoid this. 800 */ setImageAssetDelegate(ImageAssetDelegate assetDelegate)801 public void setImageAssetDelegate(ImageAssetDelegate assetDelegate) { 802 lottieDrawable.setImageAssetDelegate(assetDelegate); 803 } 804 805 /** 806 * Use this to manually set fonts. 807 */ setFontAssetDelegate( @uppressWarnings"NullableProblems") FontAssetDelegate assetDelegate)808 public void setFontAssetDelegate( 809 @SuppressWarnings("NullableProblems") FontAssetDelegate assetDelegate) { 810 lottieDrawable.setFontAssetDelegate(assetDelegate); 811 } 812 813 /** 814 * Set this to replace animation text with custom text at runtime 815 */ setTextDelegate(TextDelegate textDelegate)816 public void setTextDelegate(TextDelegate textDelegate) { 817 lottieDrawable.setTextDelegate(textDelegate); 818 } 819 820 /** 821 * Takes a {@link KeyPath}, potentially with wildcards or globstars and resolve it to a list of 822 * zero or more actual {@link KeyPath Keypaths} that exist in the current animation. 823 * 824 * If you want to set value callbacks for any of these values, it is recommended to use the 825 * returned {@link KeyPath} objects because they will be internally resolved to their content 826 * and won't trigger a tree walk of the animation contents when applied. 827 */ resolveKeyPath(KeyPath keyPath)828 public List<KeyPath> resolveKeyPath(KeyPath keyPath) { 829 return lottieDrawable.resolveKeyPath(keyPath); 830 } 831 832 /** 833 * Add a property callback for the specified {@link KeyPath}. This {@link KeyPath} can resolve 834 * to multiple contents. In that case, the callback's value will apply to all of them. 835 * 836 * Internally, this will check if the {@link KeyPath} has already been resolved with 837 * {@link #resolveKeyPath(KeyPath)} and will resolve it if it hasn't. 838 */ addValueCallback(KeyPath keyPath, T property, LottieValueCallback<T> callback)839 public <T> void addValueCallback(KeyPath keyPath, T property, LottieValueCallback<T> callback) { 840 lottieDrawable.addValueCallback(keyPath, property, callback); 841 } 842 843 /** 844 * Overload of {@link #addValueCallback(KeyPath, Object, LottieValueCallback)} that takes an interface. This allows you to use a single abstract 845 * method code block in Kotlin such as: 846 * animationView.addValueCallback(yourKeyPath, LottieProperty.COLOR) { yourColor } 847 */ addValueCallback(KeyPath keyPath, T property, final SimpleLottieValueCallback<T> callback)848 public <T> void addValueCallback(KeyPath keyPath, T property, 849 final SimpleLottieValueCallback<T> callback) { 850 lottieDrawable.addValueCallback(keyPath, property, new LottieValueCallback<T>() { 851 @Override public T getValue(LottieFrameInfo<T> frameInfo) { 852 return callback.getValue(frameInfo); 853 } 854 }); 855 } 856 857 /** 858 * Set the scale on the current composition. The only cost of this function is re-rendering the 859 * current frame so you may call it frequent to scale something up or down. 860 * 861 * The smaller the animation is, the better the performance will be. You may find that scaling an 862 * animation down then rendering it in a larger ImageView and letting ImageView scale it back up 863 * with a scaleType such as centerInside will yield better performance with little perceivable 864 * quality loss. 865 * 866 * You can also use a fixed view width/height in conjunction with the normal ImageView 867 * scaleTypes centerCrop and centerInside. 868 */ setScale(float scale)869 public void setScale(float scale) { 870 lottieDrawable.setScale(scale); 871 if (getDrawable() == lottieDrawable) { 872 setImageDrawable(null); 873 setImageDrawable(lottieDrawable); 874 } 875 } 876 getScale()877 public float getScale() { 878 return lottieDrawable.getScale(); 879 } 880 setScaleType(ScaleType scaleType)881 @Override public void setScaleType(ScaleType scaleType) { 882 super.setScaleType(scaleType); 883 if (lottieDrawable != null) { 884 lottieDrawable.setScaleType(scaleType); 885 } 886 } 887 888 @MainThread cancelAnimation()889 public void cancelAnimation() { 890 wasAnimatingWhenNotShown = false; 891 lottieDrawable.cancelAnimation(); 892 enableOrDisableHardwareLayer(); 893 } 894 895 @MainThread pauseAnimation()896 public void pauseAnimation() { 897 autoPlay = false; 898 wasAnimatingWhenDetached = false; 899 wasAnimatingWhenNotShown = false; 900 lottieDrawable.pauseAnimation(); 901 enableOrDisableHardwareLayer(); 902 } 903 904 /** 905 * Sets the progress to the specified frame. 906 * If the composition isn't set yet, the progress will be set to the frame when 907 * it is. 908 */ setFrame(int frame)909 public void setFrame(int frame) { 910 lottieDrawable.setFrame(frame); 911 } 912 913 /** 914 * Get the currently rendered frame. 915 */ getFrame()916 public int getFrame() { 917 return lottieDrawable.getFrame(); 918 } 919 setProgress(@loatRangefrom = 0f, to = 1f) float progress)920 public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) { 921 lottieDrawable.setProgress(progress); 922 } 923 getProgress()924 @FloatRange(from = 0.0f, to = 1.0f) public float getProgress() { 925 return lottieDrawable.getProgress(); 926 } 927 getDuration()928 public long getDuration() { 929 return composition != null ? (long) composition.getDuration() : 0; 930 } 931 setPerformanceTrackingEnabled(boolean enabled)932 public void setPerformanceTrackingEnabled(boolean enabled) { 933 lottieDrawable.setPerformanceTrackingEnabled(enabled); 934 } 935 936 @Nullable getPerformanceTracker()937 public PerformanceTracker getPerformanceTracker() { 938 return lottieDrawable.getPerformanceTracker(); 939 } 940 clearComposition()941 private void clearComposition() { 942 composition = null; 943 lottieDrawable.clearComposition(); 944 } 945 946 /** 947 * If you are experiencing a device specific crash that happens during drawing, you can set this to true 948 * for those devices. If set to true, draw will be wrapped with a try/catch which will cause Lottie to 949 * render an empty frame rather than crash your app. 950 * 951 * Ideally, you will never need this and the vast majority of apps and animations won't. However, you may use 952 * this for very specific cases if absolutely necessary. 953 * 954 * There is no XML attr for this because it should be set programmatically and only for specific devices that 955 * are known to be problematic. 956 */ setSafeMode(boolean safeMode)957 public void setSafeMode(boolean safeMode) { 958 lottieDrawable.setSafeMode(safeMode); 959 } 960 961 /** 962 * If rendering via software, Android will fail to generate a bitmap if the view is too large. Rather than displaying 963 * nothing, fallback on hardware acceleration which may incur a performance hit. 964 * 965 * @see #setRenderMode(RenderMode) 966 * @see com.airbnb.lottie.LottieDrawable#draw(android.graphics.Canvas) 967 */ 968 @Override buildDrawingCache(boolean autoScale)969 public void buildDrawingCache(boolean autoScale) { 970 L.beginSection("buildDrawingCache"); 971 buildDrawingCacheDepth++; 972 super.buildDrawingCache(autoScale); 973 if (buildDrawingCacheDepth == 1 && getWidth() > 0 && getHeight() > 0 && 974 getLayerType() == LAYER_TYPE_SOFTWARE && getDrawingCache(autoScale) == null) { 975 setRenderMode(HARDWARE); 976 } 977 buildDrawingCacheDepth--; 978 L.endSection("buildDrawingCache"); 979 } 980 981 /** 982 * Call this to set whether or not to render with hardware or software acceleration. 983 * Lottie defaults to Automatic which will use hardware acceleration unless: 984 * 1) There are dash paths and the device is pre-Pie. 985 * 2) There are more than 4 masks and mattes and the device is pre-Pie. 986 * Hardware acceleration is generally faster for those devices unless 987 * there are many large mattes and masks in which case there is a ton 988 * of GPU uploadTexture thrashing which makes it much slower. 989 * 990 * In most cases, hardware rendering will be faster, even if you have mattes and masks. 991 * However, if you have multiple mattes and masks (especially large ones) then you 992 * should test both render modes. You should also test on pre-Pie and Pie+ devices 993 * because the underlying rendering enginge changed significantly. 994 */ setRenderMode(RenderMode renderMode)995 public void setRenderMode(RenderMode renderMode) { 996 this.renderMode = renderMode; 997 enableOrDisableHardwareLayer(); 998 } 999 1000 /** 1001 * Sets whether to apply opacity to the each layer instead of shape. 1002 * <p> 1003 * Opacity is normally applied directly to a shape. In cases where translucent shapes overlap, applying opacity to a layer will be more accurate 1004 * at the expense of performance. 1005 * <p> 1006 * The default value is false. 1007 * <p> 1008 * Note: This process is very expensive. The performance impact will be reduced when hardware acceleration is enabled. 1009 * 1010 * @see #setRenderMode(RenderMode) 1011 */ setApplyingOpacityToLayersEnabled(boolean isApplyingOpacityToLayersEnabled)1012 public void setApplyingOpacityToLayersEnabled(boolean isApplyingOpacityToLayersEnabled) { 1013 lottieDrawable.setApplyingOpacityToLayersEnabled(isApplyingOpacityToLayersEnabled); 1014 } 1015 1016 /** 1017 * Disable the extraScale mode in {@link #draw(Canvas)} function when scaleType is FitXY. It doesn't affect the rendering with other scaleTypes. 1018 * 1019 * <p>When there are 2 animation layout side by side, the default extra scale mode might leave 1 pixel not drawn between 2 animation, and 1020 * disabling the extraScale mode can fix this problem</p> 1021 * 1022 * <b>Attention:</b> Disable the extra scale mode can downgrade the performance and may lead to larger memory footprint. Please only disable this 1023 * mode when using animation with a reasonable dimension (smaller than screen size). 1024 * 1025 * @see LottieDrawable#drawWithNewAspectRatio(Canvas) 1026 */ disableExtraScaleModeInFitXY()1027 public void disableExtraScaleModeInFitXY() { 1028 lottieDrawable.disableExtraScaleModeInFitXY(); 1029 } 1030 enableOrDisableHardwareLayer()1031 private void enableOrDisableHardwareLayer() { 1032 int layerType = LAYER_TYPE_SOFTWARE; 1033 switch (renderMode) { 1034 case HARDWARE: 1035 layerType = LAYER_TYPE_HARDWARE; 1036 break; 1037 case SOFTWARE: 1038 layerType = LAYER_TYPE_SOFTWARE; 1039 break; 1040 case AUTOMATIC: 1041 boolean useHardwareLayer = true; 1042 if (composition != null && composition.hasDashPattern() && Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { 1043 useHardwareLayer = false; 1044 } else if (composition != null && composition.getMaskAndMatteCount() > 4) { 1045 useHardwareLayer = false; 1046 } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 1047 useHardwareLayer = false; 1048 } 1049 layerType = useHardwareLayer ? LAYER_TYPE_HARDWARE : LAYER_TYPE_SOFTWARE; 1050 break; 1051 } 1052 if (layerType != getLayerType()) { 1053 setLayerType(layerType, null); 1054 } 1055 } 1056 addLottieOnCompositionLoadedListener(@onNull LottieOnCompositionLoadedListener lottieOnCompositionLoadedListener)1057 public boolean addLottieOnCompositionLoadedListener(@NonNull LottieOnCompositionLoadedListener lottieOnCompositionLoadedListener) { 1058 LottieComposition composition = this.composition; 1059 if (composition != null) { 1060 lottieOnCompositionLoadedListener.onCompositionLoaded(composition); 1061 } 1062 return lottieOnCompositionLoadedListeners.add(lottieOnCompositionLoadedListener); 1063 } 1064 removeLottieOnCompositionLoadedListener(@onNull LottieOnCompositionLoadedListener lottieOnCompositionLoadedListener)1065 public boolean removeLottieOnCompositionLoadedListener(@NonNull LottieOnCompositionLoadedListener lottieOnCompositionLoadedListener) { 1066 return lottieOnCompositionLoadedListeners.remove(lottieOnCompositionLoadedListener); 1067 } 1068 removeAllLottieOnCompositionLoadedListener()1069 public void removeAllLottieOnCompositionLoadedListener() { 1070 lottieOnCompositionLoadedListeners.clear(); 1071 } 1072 1073 private static class SavedState extends BaseSavedState { 1074 String animationName; 1075 int animationResId; 1076 float progress; 1077 boolean isAnimating; 1078 String imageAssetsFolder; 1079 int repeatMode; 1080 int repeatCount; 1081 SavedState(Parcelable superState)1082 SavedState(Parcelable superState) { 1083 super(superState); 1084 } 1085 SavedState(Parcel in)1086 private SavedState(Parcel in) { 1087 super(in); 1088 animationName = in.readString(); 1089 progress = in.readFloat(); 1090 isAnimating = in.readInt() == 1; 1091 imageAssetsFolder = in.readString(); 1092 repeatMode = in.readInt(); 1093 repeatCount = in.readInt(); 1094 } 1095 1096 @Override writeToParcel(Parcel out, int flags)1097 public void writeToParcel(Parcel out, int flags) { 1098 super.writeToParcel(out, flags); 1099 out.writeString(animationName); 1100 out.writeFloat(progress); 1101 out.writeInt(isAnimating ? 1 : 0); 1102 out.writeString(imageAssetsFolder); 1103 out.writeInt(repeatMode); 1104 out.writeInt(repeatCount); 1105 } 1106 1107 public static final Parcelable.Creator<SavedState> CREATOR = 1108 new Parcelable.Creator<SavedState>() { 1109 @Override 1110 public SavedState createFromParcel(Parcel in) { 1111 return new SavedState(in); 1112 } 1113 1114 @Override 1115 public SavedState[] newArray(int size) { 1116 return new SavedState[size]; 1117 } 1118 }; 1119 } 1120 } 1121