blob: 701c69c0cac2b87012757a1135d75038d9a4ed53 [file] [log] [blame]
package com.airbnb.lottie;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Looper;
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.UiThread;
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 also set a default cache strategy with {@link R.attr#lottie_cacheStrategy}.
* <p>
* You may manually set the progress of the animation with {@link #setProgress(float)}
*/
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> 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;
@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));
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 invalidateDrawable(Drawable dr) {
// 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);
}
@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 onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
@Override
protected void onDetachedFromWindow() {
recycleBitmaps();
super.onDetachedFromWindow();
}
@UiThread @VisibleForTesting void recycleBitmaps() {
if (Looper.myLooper() != Looper.getMainLooper()) {
throw new IllegalStateException("This must be called from the main thread.");
}
lottieDrawable.recycleBitmaps();
}
/**
* 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.
*/
@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);
lottieDrawable.setComposition(composition);
// 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();
}
@SuppressWarnings("unused") public void reverseAnimation() {
lottieDrawable.reverseAnimation();
}
@SuppressWarnings("unused") public void setSpeed(float speed) {
lottieDrawable.setSpeed(speed);
}
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];
}
};
}
}