LottieAnimationView

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.RawRes;
import android.support.annotation.VisibleForTesting;
import android.support.v7.widget.AppCompatImageView;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseArray;

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).
 * 

* 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)}. *

* You can set a default cache strategy with {@link R.attr#lottie_cacheStrategy}. *

* 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 SparseArray RAW_RES_STRONG_REF_CACHE = new SparseArray<>(); private static final SparseArray> RAW_RES_WEAK_REF_CACHE = new SparseArray<>(); private static final Map ASSET_STRONG_REF_CACHE = new HashMap<>(); private static final Map> ASSET_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 @RawRes int animationResId; 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]; if (!isInEditMode()) { boolean hasRawRes = ta.hasValue(R.styleable.LottieAnimationView_lottie_rawRes); boolean hasFileName = ta.hasValue(R.styleable.LottieAnimationView_lottie_fileName); if (hasRawRes && hasFileName) { throw new IllegalArgumentException("lottie_rawRes and lottie_fileName cannot be used at " + "the same time. Please use use only one at once."); } else if (hasRawRes) { int rawResId = ta.getResourceId(R.styleable.LottieAnimationView_lottie_rawRes, 0); if (rawResId != 0) { setAnimation(rawResId); } } else if (hasFileName) { String fileName = ta.getString(R.styleable.LottieAnimationView_lottie_fileName); if (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.animationResId = animationResId; 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()); animationName = ss.animationName; if (!TextUtils.isEmpty(animationName)) { setAnimation(animationName); } animationResId = ss.animationResId; if (animationResId != 0) { setAnimation(animationResId); } 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 raw directory. * This will load and deserialize the file asynchronously. *

* Will not cache the composition once loaded. */ public void setAnimation(@RawRes int animationResId) { setAnimation(animationResId, defaultCacheStrategy); } /** * Sets the animation from a file in the raw directory. * This will load and deserialize the file asynchronously. *

* 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(@RawRes final int animationResId, final CacheStrategy cacheStrategy) { this.animationResId = animationResId; animationName = null; if (RAW_RES_WEAK_REF_CACHE.indexOfKey(animationResId) > 0) { WeakReference compRef = RAW_RES_WEAK_REF_CACHE.get(animationResId); LottieComposition ref = compRef.get(); if (ref != null) { setComposition(ref); return; } } else if (RAW_RES_STRONG_REF_CACHE.indexOfKey(animationResId) > 0) { setComposition(RAW_RES_STRONG_REF_CACHE.get(animationResId)); return; } lottieDrawable.cancelAnimation(); cancelLoaderTask(); compositionLoader = LottieComposition.Factory.fromRawFile(getContext(), animationResId, new OnCompositionLoadedListener() { @Override public void onCompositionLoaded(LottieComposition composition) { if (cacheStrategy == CacheStrategy.Strong) { RAW_RES_STRONG_REF_CACHE.put(animationResId, composition); } else if (cacheStrategy == CacheStrategy.Weak) { RAW_RES_WEAK_REF_CACHE.put(animationResId, new WeakReference<>(composition)); } setComposition(composition); } }); } /** * Sets the animation from a file in the assets directory. * This will load and deserialize the file asynchronously. *

* 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. *

* 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; animationResId = 0; if (ASSET_WEAK_REF_CACHE.containsKey(animationName)) { WeakReference compRef = ASSET_WEAK_REF_CACHE.get(animationName); LottieComposition ref = compRef.get(); if (ref != null) { setComposition(ref); return; } } else if (ASSET_STRONG_REF_CACHE.containsKey(animationName)) { setComposition(ASSET_STRONG_REF_CACHE.get(animationName)); return; } lottieDrawable.cancelAnimation(); cancelLoaderTask(); compositionLoader = LottieComposition.Factory.fromAssetFileName(getContext(), animationName, new OnCompositionLoadedListener() { @Override public void onCompositionLoaded(LottieComposition composition) { if (cacheStrategy == CacheStrategy.Strong) { ASSET_STRONG_REF_CACHE.put(animationName, composition); } else if (cacheStrategy == CacheStrategy.Weak) { ASSET_WEAK_REF_CACHE.put(animationName, new WeakReference<>(composition)); } setComposition(composition); } }); } /** * Sets the animation from a JSONObject. * This will load and deserialize the file asynchronously. *

* 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(); } /** * Plays the animation from the beginning. If speed is < 0, it will start at the end * and play towards the beginning */ public void playAnimation() { lottieDrawable.playAnimation(); enableOrDisableHardwareLayer(); } /** * Continues playing the animation from its current position. If speed < 0, it will play backwards * from the current position. */ public void resumeAnimation() { lottieDrawable.resumeAnimation(); enableOrDisableHardwareLayer(); } /** * Sets the minimum frame that the animation will start from when playing or looping. */ public void setMinFrame(int startFrame) { lottieDrawable.setMinFrame(startFrame); } /** * Sets the minimum progress that the animation will start from when playing or looping. */ public void setMinProgress(float startProgress) { lottieDrawable.setMinProgress(startProgress); } /** * Sets the maximum frame that the animation will end at when playing or looping. */ public void setMaxFrame(int endFrame) { lottieDrawable.setMaxFrame(endFrame); } /** * Sets the maximum progress that the animation will end at when playing or looping. */ public void setMaxProgress(@FloatRange(from = 0f, to = 1f) float endProgress) { lottieDrawable.setMaxProgress(endProgress); } /** * @see #setMinFrame(int) * @see #setMaxFrame(int) */ public void setMinAndMaxFrame(int minFrame, int maxFrame) { lottieDrawable.setMinAndMaxFrame(minFrame, maxFrame); } /** * @see #setMinProgress(float) * @see #setMaxProgress(float) */ public void setMinAndMaxProgress( @FloatRange(from = 0f, to = 1f) float minProgress, @FloatRange(from = 0f, to = 1f) float maxProgress) { lottieDrawable.setMinAndMaxProgress(minProgress, maxProgress); } /** * Reverses the current animation speed. This does NOT play the animation. * @see #setSpeed(float) * @see #playAnimation() * @see #resumeAnimation() */ public void reverseAnimationSpeed() { lottieDrawable.reverseAnimationSpeed(); } /** * Sets the playback speed. If speed < 0, the animation will play backwards. */ public void setSpeed(float speed) { lottieDrawable.setSpeed(speed); } /** * Returns the current playback speed. This will be < 0 if the animation is playing backwards. */ public float getSpeed() { return lottieDrawable.getSpeed(); } 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(); } /** * 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() { lottieDrawable.pauseAnimation(); enableOrDisableHardwareLayer(); } /** * Sets the progress to the specified frame. * If the composition isn't set yet, the progress will be set to the frame when * it is. */ public void setFrame(int frame) { lottieDrawable.setFrame(frame); } /** * Get the currently rendered frame. */ public int getFrame() { return lottieDrawable.getFrame(); } 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; int animationResId; 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 CREATOR = new Parcelable.Creator() { @Override public SavedState createFromParcel(Parcel in) { return new SavedState(in); } @Override public SavedState[] newArray(int size) { return new SavedState[size]; } }; } }


你可能感兴趣的:(LottieAnimationView)