package com.airbnb.lottie; import android.animation.Animator; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.provider.Settings; import android.support.annotation.FloatRange; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.support.v7.widget.AppCompatImageView; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import org.json.JSONObject; import java.lang.ref.WeakReference; import java.util.HashMap; import java.util.Map; /** * This view will load, deserialize, and display an After Effects animation exported with * bodymovin (https://github.com/bodymovin/bodymovin). * <p> * You may set the animation in one of two ways: * 1) Attrs: {@link R.styleable#LottieAnimationView_lottie_fileName} * 2) Programatically: {@link #setAnimation(String)}, {@link #setComposition(LottieComposition)}, * or {@link #setAnimation(JSONObject)}. * <p> * You can set a default cache strategy with {@link R.attr#lottie_cacheStrategy}. * <p> * You can manually set the progress of the animation with {@link #setProgress(float)} or * {@link R.attr#lottie_progress} */ public class LottieAnimationView extends AppCompatImageView { private static final String TAG = LottieAnimationView.class.getSimpleName(); /** * Caching strategy for compositions that will be reused frequently. * Weak or Strong indicates the GC reference strength of the composition in the cache. */ @SuppressWarnings("WeakerAccess") public enum CacheStrategy { None, Weak, Strong } private static final Map<String, LottieComposition> strongRefCache = new HashMap<>(); private static final Map<String, WeakReference<LottieComposition>> weakRefCache = new HashMap<>(); private final OnCompositionLoadedListener loadedListener = new OnCompositionLoadedListener() { @Override public void onCompositionLoaded(LottieComposition composition) { setComposition(composition); compositionLoader = null; } }; private final LottieDrawable lottieDrawable = new LottieDrawable(); private CacheStrategy defaultCacheStrategy; private String animationName; private boolean wasAnimatingWhenDetached = false; @Nullable private Cancellable compositionLoader; /** * Can be null because it is created async */ @Nullable private LottieComposition composition; public LottieAnimationView(Context context) { super(context); init(null); } public LottieAnimationView(Context context, AttributeSet attrs) { super(context, attrs); init(attrs); } public LottieAnimationView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(attrs); } private void init(@Nullable AttributeSet attrs) { TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.LottieAnimationView); String fileName = ta.getString(R.styleable.LottieAnimationView_lottie_fileName); if (!isInEditMode() && fileName != null) { setAnimation(fileName); } if (ta.getBoolean(R.styleable.LottieAnimationView_lottie_autoPlay, false)) { lottieDrawable.playAnimation(); } lottieDrawable.loop(ta.getBoolean(R.styleable.LottieAnimationView_lottie_loop, false)); setImageAssetsFolder(ta.getString(R.styleable.LottieAnimationView_lottie_imageAssetsFolder)); setProgress(ta.getFloat(R.styleable.LottieAnimationView_lottie_progress, 0)); int cacheStrategy = ta.getInt( R.styleable.LottieAnimationView_lottie_cacheStrategy, CacheStrategy.None.ordinal()); defaultCacheStrategy = CacheStrategy.values()[cacheStrategy]; ta.recycle(); setLayerType(LAYER_TYPE_SOFTWARE, null); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { float systemAnimationScale = Settings.Global.getFloat(getContext().getContentResolver(), Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f); if (systemAnimationScale == 0f) { lottieDrawable.systemAnimationsAreDisabled(); } } } @Override public void setImageResource(int resId) { super.setImageResource(resId); recycleBitmaps(); } @Override public void setImageDrawable(Drawable drawable) { if (drawable != lottieDrawable) { recycleBitmaps(); } super.setImageDrawable(drawable); } @Override public void invalidateDrawable(@NonNull Drawable dr) { if (getDrawable() == lottieDrawable) { // We always want to invalidate the root drawable to it redraws the whole drawable. // Eventually it would be great to be able to invalidate just the changed region. super.invalidateDrawable(lottieDrawable); } else { // Otherwise work as regular ImageView super.invalidateDrawable(dr); } } @Override protected Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); SavedState ss = new SavedState(superState); ss.animationName = animationName; ss.progress = lottieDrawable.getProgress(); ss.isAnimating = lottieDrawable.isAnimating(); ss.isLooping = lottieDrawable.isLooping(); return ss; } @Override protected void onRestoreInstanceState(Parcelable state) { if (!(state instanceof SavedState)) { super.onRestoreInstanceState(state); return; } SavedState ss = (SavedState) state; super.onRestoreInstanceState(ss.getSuperState()); this.animationName = ss.animationName; if (!TextUtils.isEmpty(animationName)) { setAnimation(animationName); } setProgress(ss.progress); loop(ss.isLooping); if (ss.isAnimating) { playAnimation(); } } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); if (wasAnimatingWhenDetached) { playAnimation(); } } @Override protected void onDetachedFromWindow() { if (isAnimating()) { cancelAnimation(); wasAnimatingWhenDetached = true; } recycleBitmaps(); super.onDetachedFromWindow(); } @VisibleForTesting void recycleBitmaps() { lottieDrawable.recycleBitmaps(); } /** * Enable hardware acceleration for this view. * READ THIS BEFORE ENABLING HARDWARE ACCELERATION: * 1) Test your animation on the minimum API level you support. Some drawing features such as * dashes and stroke caps have min api levels * (https://developer.android.com/guide/topics/graphics/hardware-accel.html#unsupported) * 2) Enabling hardware acceleration is not always more performant. Check it with your specific * animation only if you are having performance issues with software rendering. * 3) Software rendering is safer and will be consistent across devices. Manufacturers can * potentially break hardware rendering with bugs in their SKIA engine. Lottie cannot do * anything about that. */ @SuppressWarnings("WeakerAccess") public void useExperimentalHardwareAcceleration() { setLayerType(LAYER_TYPE_HARDWARE, null); } /** * Sets the animation from a file in the assets directory. * This will load and deserialize the file asynchronously. * <p> * Will not cache the composition once loaded. */ @SuppressWarnings("WeakerAccess") public void setAnimation(String animationName) { setAnimation(animationName, defaultCacheStrategy); } /** * Sets the animation from a file in the assets directory. * This will load and deserialize the file asynchronously. * <p> * You may also specify a cache strategy. Specifying {@link CacheStrategy#Strong} will hold a * strong reference to the composition once it is loaded * and deserialized. {@link CacheStrategy#Weak} will hold a weak reference to said composition. */ @SuppressWarnings("WeakerAccess") public void setAnimation(final String animationName, final CacheStrategy cacheStrategy) { this.animationName = animationName; if (weakRefCache.containsKey(animationName)) { WeakReference<LottieComposition> compRef = weakRefCache.get(animationName); if (compRef.get() != null) { setComposition(compRef.get()); return; } } else if (strongRefCache.containsKey(animationName)) { setComposition(strongRefCache.get(animationName)); return; } this.animationName = animationName; lottieDrawable.cancelAnimation(); cancelLoaderTask(); compositionLoader = LottieComposition.Factory.fromAssetFileName(getContext(), animationName, new OnCompositionLoadedListener() { @Override public void onCompositionLoaded(LottieComposition composition) { if (cacheStrategy == CacheStrategy.Strong) { strongRefCache.put(animationName, composition); } else if (cacheStrategy == CacheStrategy.Weak) { weakRefCache.put(animationName, new WeakReference<>(composition)); } setComposition(composition); } }); } /** * Sets the animation from a JSONObject. * This will load and deserialize the file asynchronously. * <p> * This is particularly useful for animations loaded from the network. You can fetch the * bodymovin json from the network and pass it directly here. */ public void setAnimation(final JSONObject json) { cancelLoaderTask(); compositionLoader = LottieComposition.Factory.fromJson(getResources(), json, loadedListener); } private void cancelLoaderTask() { if (compositionLoader != null) { compositionLoader.cancel(); compositionLoader = null; } } /** * Sets a composition. * You can set a default cache strategy if this view was inflated with xml by * using {@link R.attr#lottie_cacheStrategy}. */ public void setComposition(@NonNull LottieComposition composition) { if (L.DBG) { Log.v(TAG, "Set Composition \n" + composition); } lottieDrawable.setCallback(this); boolean isNewComposition = lottieDrawable.setComposition(composition); if (!isNewComposition) { // We can avoid re-setting the drawable, and invalidating the view, since the composition // hasn't changed. return; } int screenWidth = Utils.getScreenWidth(getContext()); int screenHeight = Utils.getScreenHeight(getContext()); int compWidth = composition.getBounds().width(); int compHeight = composition.getBounds().height(); if (compWidth > screenWidth || compHeight > screenHeight) { float xScale = screenWidth / (float) compWidth; float yScale = screenHeight / (float) compHeight; setScale(Math.min(xScale, yScale)); Log.w(L.TAG, String.format( "Composition larger than the screen %dx%d vs %dx%d. Scaling down.", compWidth, compHeight, screenWidth, screenHeight)); } // If you set a different composition on the view, the bounds will not update unless // the drawable is different than the original. setImageDrawable(null); setImageDrawable(lottieDrawable); this.composition = composition; requestLayout(); } /** * Returns whether or not any layers in this composition has masks. */ @SuppressWarnings("unused") public boolean hasMasks() { return lottieDrawable.hasMasks(); } /** * Returns whether or not any layers in this composition has a matte layer. */ @SuppressWarnings("unused") public boolean hasMatte() { return lottieDrawable.hasMatte(); } /** * If you use image assets, you must explicitly specify the folder in assets/ in which they are * located because bodymovin uses the name filenames across all compositions (img_#). * Do NOT rename the images themselves. * * If your images are located in src/main/assets/airbnb_loader/ then call * `setImageAssetsFolder("airbnb_loader/");`. */ @SuppressWarnings("WeakerAccess") public void setImageAssetsFolder(String imageAssetsFolder) { lottieDrawable.setImagesAssetsFolder(imageAssetsFolder); } public void addAnimatorUpdateListener(ValueAnimator.AnimatorUpdateListener updateListener) { lottieDrawable.addAnimatorUpdateListener(updateListener); } @SuppressWarnings("unused") public void removeUpdateListener(ValueAnimator.AnimatorUpdateListener updateListener) { lottieDrawable.removeAnimatorUpdateListener(updateListener); } public void addAnimatorListener(Animator.AnimatorListener listener) { lottieDrawable.addAnimatorListener(listener); } @SuppressWarnings("unused") public void removeAnimatorListener(Animator.AnimatorListener listener) { lottieDrawable.removeAnimatorListener(listener); } public void loop(boolean loop) { lottieDrawable.loop(loop); } public boolean isAnimating() { return lottieDrawable.isAnimating(); } public void playAnimation() { lottieDrawable.playAnimation(); } public void resumeAnimation() { lottieDrawable.resumeAnimation(); } @SuppressWarnings("unused") public void reverseAnimation() { lottieDrawable.reverseAnimation(); } @SuppressWarnings("unused") public void resumeReverseAnimation() { lottieDrawable.resumeReverseAnimation(); } @SuppressWarnings("unused") public void setSpeed(float speed) { lottieDrawable.setSpeed(speed); } /** * Use this if you can't bundle images with your app. This may be useful if you download the * animations from the network or have the images saved to an SD Card. In that case, Lottie * will defer the loading of the bitmap to this delegate. */ @SuppressWarnings("unused") public void setImageAssetDelegate(ImageAssetDelegate assetDelegate) { lottieDrawable.setImageAssetDelegate(assetDelegate); } void setScale(float scale) { lottieDrawable.setScale(scale); setImageDrawable(null); setImageDrawable(lottieDrawable); } public void cancelAnimation() { lottieDrawable.cancelAnimation(); } public void pauseAnimation() { float progress = getProgress(); lottieDrawable.cancelAnimation(); setProgress(progress); } public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) { lottieDrawable.setProgress(progress); } @FloatRange(from = 0.0f, to = 1.0f) public float getProgress() { return lottieDrawable.getProgress(); } @SuppressWarnings("unused") public long getDuration() { return composition != null ? composition.getDuration() : 0; } private static class SavedState extends BaseSavedState { String animationName; float progress; boolean isAnimating; boolean isLooping; SavedState(Parcelable superState) { super(superState); } private SavedState(Parcel in) { super(in); animationName = in.readString(); progress = in.readFloat(); isAnimating = in.readInt() == 1; isLooping = in.readInt() == 1; } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); out.writeString(animationName); out.writeFloat(progress); out.writeInt(isAnimating ? 1 : 0); out.writeInt(isLooping ? 1 : 0); } public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() { public SavedState createFromParcel(Parcel in) { return new SavedState(in); } public SavedState[] newArray(int size) { return new SavedState[size]; } }; } }