| package com.airbnb.lottie; |
| |
| import android.animation.Animator; |
| import android.animation.ValueAnimator; |
| import android.content.Context; |
| import android.content.res.TypedArray; |
| import android.graphics.Bitmap; |
| import android.graphics.Color; |
| import android.graphics.ColorFilter; |
| import android.graphics.drawable.Drawable; |
| import android.os.Parcel; |
| import android.os.Parcelable; |
| 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 com.airbnb.lottie.utils.Utils; |
| |
| 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} |
| */ |
| @SuppressWarnings({"unused", "WeakerAccess"}) 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. |
| */ |
| public enum CacheStrategy { |
| None, |
| Weak, |
| Strong |
| } |
| |
| private static final Map<String, LottieComposition> STRONG_REF_CACHE = new HashMap<>(); |
| private static final Map<String, WeakReference<LottieComposition>> WEAK_REF_CACHE = |
| new HashMap<>(); |
| |
| private final OnCompositionLoadedListener loadedListener = |
| new OnCompositionLoadedListener() { |
| @Override public void onCompositionLoaded(@Nullable LottieComposition composition) { |
| if (composition != null) { |
| setComposition(composition); |
| } |
| compositionLoader = null; |
| } |
| }; |
| |
| private final LottieDrawable lottieDrawable = new LottieDrawable(); |
| private CacheStrategy defaultCacheStrategy; |
| private String animationName; |
| private boolean wasAnimatingWhenDetached = false; |
| private boolean autoPlay = false; |
| private boolean useHardwareLayer = 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); |
| int cacheStrategy = ta.getInt( |
| R.styleable.LottieAnimationView_lottie_cacheStrategy, |
| CacheStrategy.Weak.ordinal()); |
| defaultCacheStrategy = CacheStrategy.values()[cacheStrategy]; |
| 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(); |
| autoPlay = true; |
| } |
| 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)); |
| enableMergePathsForKitKatAndAbove(ta.getBoolean( |
| R.styleable.LottieAnimationView_lottie_enableMergePathsForKitKatAndAbove, false)); |
| if (ta.hasValue(R.styleable.LottieAnimationView_lottie_colorFilter)) { |
| addColorFilter(new SimpleColorFilter(ta.getColor( |
| R.styleable.LottieAnimationView_lottie_colorFilter, Color.TRANSPARENT))); |
| } |
| if (ta.hasValue(R.styleable.LottieAnimationView_lottie_scale)) { |
| lottieDrawable.setScale(ta.getFloat(R.styleable.LottieAnimationView_lottie_scale, 1f)); |
| } |
| |
| ta.recycle(); |
| |
| if (Utils.getAnimationScale(getContext()) == 0f) { |
| lottieDrawable.systemAnimationsAreDisabled(); |
| } |
| |
| enableOrDisableHardwareLayer(); |
| } |
| |
| @Override public void setImageResource(int resId) { |
| recycleBitmaps(); |
| cancelLoaderTask(); |
| super.setImageResource(resId); |
| } |
| |
| @Override public void setImageDrawable(Drawable drawable) { |
| if (drawable != lottieDrawable) { |
| recycleBitmaps(); |
| } |
| cancelLoaderTask(); |
| super.setImageDrawable(drawable); |
| } |
| |
| @Override public void setImageBitmap(Bitmap bm) { |
| recycleBitmaps(); |
| cancelLoaderTask(); |
| super.setImageBitmap(bm); |
| } |
| |
| /** |
| * Add a color filter to specific content on a specific layer. |
| * @param layerName name of the layer where the supplied content name lives |
| * @param contentName name of the specific content that the color filter is to be applied |
| * @param colorFilter the color filter, null to clear the color filter |
| */ |
| public void addColorFilterToContent( |
| String layerName, String contentName, @Nullable ColorFilter colorFilter) { |
| lottieDrawable.addColorFilterToContent(layerName, contentName, colorFilter); |
| } |
| |
| /** |
| * Add a color filter to a whole layer |
| * @param layerName name of the layer that the color filter is to be applied |
| * @param colorFilter the color filter, null to clear the color filter |
| */ |
| public void addColorFilterToLayer( |
| String layerName, @Nullable ColorFilter colorFilter) { |
| lottieDrawable.addColorFilterToLayer(layerName, colorFilter); |
| } |
| |
| /** |
| * Add a color filter to all layers |
| * @param colorFilter the color filter, null to clear all color filters |
| */ |
| public void addColorFilter(@Nullable ColorFilter colorFilter) { |
| lottieDrawable.addColorFilter(colorFilter); |
| } |
| |
| /** |
| * Clear all color filters on all layers and all content in the layers |
| */ |
| public void clearColorFilters() { |
| lottieDrawable.clearColorFilters(); |
| } |
| |
| @Override public void invalidateDrawable(@NonNull Drawable dr) { |
| if (getDrawable() == lottieDrawable) { |
| // We always want to invalidate the root drawable so 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(); |
| ss.imageAssetsFolder = lottieDrawable.getImageAssetsFolder(); |
| 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(); |
| } |
| lottieDrawable.setImagesAssetsFolder(ss.imageAssetsFolder); |
| } |
| |
| @Override protected void onAttachedToWindow() { |
| super.onAttachedToWindow(); |
| if (autoPlay && wasAnimatingWhenDetached) { |
| playAnimation(); |
| } |
| } |
| |
| @Override protected void onDetachedFromWindow() { |
| if (isAnimating()) { |
| cancelAnimation(); |
| wasAnimatingWhenDetached = true; |
| } |
| recycleBitmaps(); |
| super.onDetachedFromWindow(); |
| } |
| |
| @VisibleForTesting void recycleBitmaps() { |
| // AppCompatImageView constructor will set the image when set from xml |
| // before LottieDrawable has been initialized |
| if (lottieDrawable != null) { |
| lottieDrawable.recycleBitmaps(); |
| } |
| } |
| |
| /** |
| * Enable this to get merge path support for devices running KitKat (19) and above. |
| * |
| * Merge paths currently don't work if the the operand shape is entirely contained within the |
| * first shape. If you need to cut out one shape from another shape, use an even-odd fill type |
| * instead of using merge paths. |
| */ |
| public void enableMergePathsForKitKatAndAbove(boolean enable) { |
| lottieDrawable.enableMergePathsForKitKatAndAbove(enable); |
| } |
| |
| /** |
| * @see #useHardwareAcceleration(boolean) |
| */ |
| @Deprecated |
| public void useExperimentalHardwareAcceleration() { |
| useHardwareAcceleration(true); |
| } |
| |
| |
| /** |
| * @see #useHardwareAcceleration(boolean) |
| */ |
| @Deprecated |
| public void useExperimentalHardwareAcceleration(boolean use) { |
| useHardwareAcceleration(use); |
| } |
| |
| /** |
| * @see #useHardwareAcceleration(boolean) |
| */ |
| public void useHardwareAcceleration() { |
| useHardwareAcceleration(true); |
| } |
| |
| /** |
| * 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. |
| */ |
| public void useHardwareAcceleration(boolean use) { |
| useHardwareLayer = use; |
| enableOrDisableHardwareLayer(); |
| } |
| |
| /** |
| * 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. |
| */ |
| 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. |
| */ |
| public void setAnimation(final String animationName, final CacheStrategy cacheStrategy) { |
| this.animationName = animationName; |
| if (WEAK_REF_CACHE.containsKey(animationName)) { |
| WeakReference<LottieComposition> compRef = WEAK_REF_CACHE.get(animationName); |
| LottieComposition ref = compRef.get(); |
| if (ref != null) { |
| setComposition(ref); |
| return; |
| } |
| } else if (STRONG_REF_CACHE.containsKey(animationName)) { |
| setComposition(STRONG_REF_CACHE.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) { |
| STRONG_REF_CACHE.put(animationName, composition); |
| } else if (cacheStrategy == CacheStrategy.Weak) { |
| WEAK_REF_CACHE.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); |
| enableOrDisableHardwareLayer(); |
| if (!isNewComposition) { |
| // We can avoid re-setting the drawable, and invalidating the view, since the composition |
| // hasn't changed. |
| return; |
| } |
| |
| // 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. |
| */ |
| public boolean hasMasks() { |
| return lottieDrawable.hasMasks(); |
| } |
| |
| /** |
| * Returns whether or not any layers in this composition has a matte layer. |
| */ |
| public boolean hasMatte() { |
| return lottieDrawable.hasMatte(); |
| } |
| |
| public void addAnimatorUpdateListener(ValueAnimator.AnimatorUpdateListener updateListener) { |
| lottieDrawable.addAnimatorUpdateListener(updateListener); |
| } |
| |
| public void removeUpdateListener(ValueAnimator.AnimatorUpdateListener updateListener) { |
| lottieDrawable.removeAnimatorUpdateListener(updateListener); |
| } |
| |
| public void addAnimatorListener(Animator.AnimatorListener listener) { |
| lottieDrawable.addAnimatorListener(listener); |
| } |
| |
| 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(); |
| enableOrDisableHardwareLayer(); |
| } |
| |
| public void resumeAnimation() { |
| lottieDrawable.resumeAnimation(); |
| enableOrDisableHardwareLayer(); |
| } |
| |
| public void playAnimation(final int startFrame, final int endFrame) { |
| lottieDrawable.playAnimation(startFrame, endFrame); |
| } |
| |
| public void playAnimation(@FloatRange(from = 0f, to = 1f) float startProgress, |
| @FloatRange(from = 0f, to = 1f) float endProgress) { |
| lottieDrawable.playAnimation(startProgress, endProgress); |
| } |
| |
| public void reverseAnimation() { |
| lottieDrawable.reverseAnimation(); |
| enableOrDisableHardwareLayer(); |
| } |
| |
| public void setMinFrame(int startFrame) { |
| lottieDrawable.setMinFrame(startFrame); |
| } |
| |
| public void setMinProgress(float startProgress) { |
| lottieDrawable.setMinProgress(startProgress); |
| } |
| |
| public void setMaxFrame(int endFrame) { |
| lottieDrawable.setMaxFrame(endFrame); |
| } |
| |
| public void setMaxProgress(float endProgress) { |
| lottieDrawable.setMaxProgress(endProgress); |
| } |
| |
| public void setMinAndMaxFrame(int minFrame, int maxFrame) { |
| lottieDrawable.setMinAndMaxFrame(minFrame, maxFrame); |
| } |
| |
| public void setMinAndMaxProgress(float minProgress, float maxProgress) { |
| lottieDrawable.setMinAndMaxProgress(minProgress, maxProgress); |
| } |
| |
| public void resumeReverseAnimation() { |
| lottieDrawable.resumeReverseAnimation(); |
| enableOrDisableHardwareLayer(); |
| } |
| |
| public void setSpeed(float speed) { |
| lottieDrawable.setSpeed(speed); |
| } |
| |
| /** |
| * 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/");`. |
| */ |
| public void setImageAssetsFolder(String imageAssetsFolder) { |
| lottieDrawable.setImagesAssetsFolder(imageAssetsFolder); |
| } |
| |
| @Nullable |
| public String getImageAssetsFolder() { |
| return lottieDrawable.getImageAssetsFolder(); |
| } |
| |
| /** |
| * Allows you to modify or clear a bitmap that was loaded for an image either automatically |
| * through {@link #setImageAssetsFolder(String)} or with an {@link ImageAssetDelegate}. |
| * |
| * @return the previous Bitmap or null. |
| */ |
| @Nullable |
| public Bitmap updateBitmap(String id, @Nullable Bitmap bitmap) { |
| return lottieDrawable.updateBitmap(id, bitmap); |
| } |
| |
| /** |
| * 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. |
| */ |
| public void setImageAssetDelegate(ImageAssetDelegate assetDelegate) { |
| lottieDrawable.setImageAssetDelegate(assetDelegate); |
| } |
| |
| /** |
| * Use this to manually set fonts. |
| */ |
| public void setFontAssetDelegate( |
| @SuppressWarnings("NullableProblems") FontAssetDelegate assetDelegate) { |
| lottieDrawable.setFontAssetDelegate(assetDelegate); |
| } |
| |
| /** |
| * Set this to replace animation text with custom text at runtime |
| */ |
| public void setTextDelegate(TextDelegate textDelegate) { |
| lottieDrawable.setTextDelegate(textDelegate); |
| } |
| |
| /** |
| * Set the scale on the current composition. The only cost of this function is re-rendering the |
| * current frame so you may call it frequent to scale something up or down. |
| * |
| * The smaller the animation is, the better the performance will be. You may find that scaling an |
| * animation down then rendering it in a larger ImageView and letting ImageView scale it back up |
| * with a scaleType such as centerInside will yield better performance with little perceivable |
| * quality loss. |
| */ |
| public void setScale(float scale) { |
| lottieDrawable.setScale(scale); |
| if (getDrawable() == lottieDrawable) { |
| setImageDrawable(null); |
| setImageDrawable(lottieDrawable); |
| } |
| } |
| |
| public float getScale() { |
| return lottieDrawable.getScale(); |
| } |
| |
| public void cancelAnimation() { |
| lottieDrawable.cancelAnimation(); |
| enableOrDisableHardwareLayer(); |
| } |
| |
| public void pauseAnimation() { |
| float progress = getProgress(); |
| lottieDrawable.cancelAnimation(); |
| setProgress(progress); |
| enableOrDisableHardwareLayer(); |
| } |
| |
| 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(); |
| } |
| |
| public long getDuration() { |
| return composition != null ? composition.getDuration() : 0; |
| } |
| |
| public void setPerformanceTrackingEnabled(boolean enabled) { |
| lottieDrawable.setPerformanceTrackingEnabled(enabled); |
| } |
| |
| @Nullable |
| public PerformanceTracker getPerformanceTracker() { |
| return lottieDrawable.getPerformanceTracker(); |
| } |
| |
| private void enableOrDisableHardwareLayer() { |
| boolean useHardwareLayer = this.useHardwareLayer && lottieDrawable.isAnimating(); |
| setLayerType(useHardwareLayer ? LAYER_TYPE_HARDWARE : LAYER_TYPE_SOFTWARE, null); |
| } |
| |
| private static class SavedState extends BaseSavedState { |
| String animationName; |
| float progress; |
| boolean isAnimating; |
| boolean isLooping; |
| String imageAssetsFolder; |
| |
| 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; |
| imageAssetsFolder = in.readString(); |
| } |
| |
| @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); |
| out.writeString(imageAssetsFolder); |
| } |
| |
| public static final Parcelable.Creator<SavedState> CREATOR = |
| new Parcelable.Creator<SavedState>() { |
| @Override |
| public SavedState createFromParcel(Parcel in) { |
| return new SavedState(in); |
| } |
| |
| @Override |
| public SavedState[] newArray(int size) { |
| return new SavedState[size]; |
| } |
| }; |
| } |
| } |