blob: 7056a3980511c7a78b1e61bab0e541f902eb6d89 [file] [log] [blame]
package com.airbnb.lottie;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
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.text.TextUtils;
import android.util.AttributeSet;
import android.util.LongSparseArray;
import android.widget.ImageView;
import com.airbnb.lottie.model.Layer;
import com.airbnb.lottie.layers.LayerView;
import com.airbnb.lottie.layers.RootAnimatableLayer;
import com.airbnb.lottie.model.Composition;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.List;
/**
* 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)} or {@link #setAnimation(JSONObject)}.
*
* You may manually set the progress of the animation with {@link #setProgress(float)}
*/
public class LottieAnimationView extends ImageView {
/**
* Returns a {@link LottieAnimationView} that will allow it to be used without being attached to a window.
* Normally this isn't possible.
*/
@VisibleForTesting
public static LottieAnimationView forScreenshotTest(Context context) {
LottieAnimationView view = new LottieAnimationView(context);
view.isScreenshotTest = true;
return view;
}
private final LongSparseArray<LayerView> layerMap = new LongSparseArray<>();
private final RootAnimatableLayer rootAnimatableLayer = new RootAnimatableLayer(this);
@FloatRange(from=0f, to=1f) private float progress;
private String animationName;
private boolean isScreenshotTest;
private boolean isAnimationLoading;
private boolean setProgressWhenCompositionSet;
private boolean playAnimationWhenCompositionSet;
@Nullable private AsyncTask fileToJsonTask;
@Nullable private AsyncTask jsonToCompositionTask;
/** Can be null because it is created async */
@Nullable private Composition composition;
private boolean hasInvalidatedThisFrame;
@Nullable private Bitmap mainBitmap = null;
@Nullable private Bitmap maskBitmap = null;
@Nullable private Bitmap matteBitmap = null;
@Nullable private Bitmap mainBitmapForMatte = null;
@Nullable private Bitmap maskBitmapForMatte = null;
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) {
L.SCALE = getResources().getDisplayMetrics().density;
TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.LottieAnimationView);
String fileName = ta.getString(R.styleable.LottieAnimationView_lottie_fileName);
if (fileName != null) {
setAnimation(fileName);
}
if (ta.getBoolean(R.styleable.LottieAnimationView_lottie_autoPlay, false)) {
rootAnimatableLayer.playAnimation();
}
rootAnimatableLayer.loop(ta.getBoolean(R.styleable.LottieAnimationView_lottie_loop, false));
ta.recycle();
setLayerType(LAYER_TYPE_SOFTWARE, null);
}
@Override
protected Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.animationName = animationName;
ss.progress = rootAnimatableLayer.getProgress();
ss.isAnimating = rootAnimatableLayer.isAnimating();
ss.isLooping = rootAnimatableLayer.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();
}
}
@SuppressLint("MissingSuperCall")
@Override
protected boolean verifyDrawable(@NonNull Drawable drawable) {
return true;
}
@Override
public void invalidateDrawable(@NonNull Drawable dr) {
if (!hasInvalidatedThisFrame) {
super.invalidateDrawable(rootAnimatableLayer);
hasInvalidatedThisFrame = true;
}
}
@Override
protected void onDraw(Canvas canvas) {
hasInvalidatedThisFrame = false;
super.onDraw(canvas);
}
@Override
protected void onDetachedFromWindow() {
recycleBitmaps();
super.onDetachedFromWindow();
}
@VisibleForTesting
public void recycleBitmaps() {
if (mainBitmap != null) {
mainBitmap.recycle();
mainBitmap = null;
}
if (maskBitmap != null) {
maskBitmap.recycle();
maskBitmap = null;
}
if (matteBitmap != null) {
matteBitmap.recycle();
matteBitmap = null;
}
if (mainBitmapForMatte != null) {
mainBitmapForMatte.recycle();
mainBitmapForMatte = null;
}
if (maskBitmapForMatte != null) {
maskBitmapForMatte.recycle();
maskBitmapForMatte = null;
}
}
/**
* Sets the animation from a file in the assets directory.
* This will load and deserialize the file asynchronously.
*/
public void setAnimation(final String animationName) {
isAnimationLoading = true;
setProgressWhenCompositionSet = false;
playAnimationWhenCompositionSet = false;
this.animationName = animationName;
if (fileToJsonTask != null) {
fileToJsonTask.cancel(true);
fileToJsonTask = null;
}
if (jsonToCompositionTask != null) {
jsonToCompositionTask.cancel(true);
jsonToCompositionTask = null;
}
InputStream file;
try {
file = getContext().getAssets().open(animationName);
} catch (IOException e) {
onAnimationLoadingFail();
throw new IllegalStateException("Unable to find file " + animationName, e);
}
fileToJsonTask = new AsyncTask<InputStream, Void, JSONObject>() {
@Override
protected JSONObject doInBackground(InputStream... params) {
//noinspection WrongThread
return setAnimationSync(params[0]);
}
@Override
protected void onPostExecute(JSONObject jsonObject) {
setAnimation(jsonObject);
}
}.execute(file);
}
/**
* Sets the animation using the raw JSON Object.
*/
public void setAnimation(JSONObject json) {
this.animationName = null;
if (jsonToCompositionTask != null) {
jsonToCompositionTask.cancel(true);
}
// TODO: cancel these if the View gets detached.
jsonToCompositionTask = new AsyncTask<JSONObject, Void, Composition>() {
@Override
protected Composition doInBackground(JSONObject... params) {
return Composition.fromJson(params[0]);
}
@Override
protected void onPostExecute(Composition model) {
setComposition(model);
}
}.execute(json);
}
/**
* Like {@link #setAnimation(String)} except it loads and deserializes the file
* synchronously. This should only be used for tests.
*/
@VisibleForTesting
public void setAnimationSync(String animationName) {
InputStream file;
try {
file = getContext().getAssets().open(animationName);
} catch (IOException e) {
onAnimationLoadingFail();
throw new IllegalStateException("Unable to find file " + animationName, e);
}
setJsonSync(setAnimationSync(file));
}
private JSONObject setAnimationSync(InputStream file) {
try {
int size = file.available();
byte[] buffer = new byte[size];
//noinspection ResultOfMethodCallIgnored
file.read(buffer);
file.close();
String json = new String(buffer, "UTF-8");
return new JSONObject(json);
} catch (IOException e) {
onAnimationLoadingFail();
throw new IllegalStateException("Unable to find file.", e);
} catch (JSONException e) {
onAnimationLoadingFail();
throw new IllegalStateException("Unable to load JSON.", e);
}
}
private void setJsonSync(JSONObject json) {
Composition composition = Composition.fromJson(json);
setComposition(composition);
}
private void onAnimationLoadingFail() {
isAnimationLoading = false;
setProgressWhenCompositionSet = false;
playAnimationWhenCompositionSet = false;
}
private void setComposition(@NonNull Composition composition) {
if (getWindowToken() == null && !isScreenshotTest) {
return;
}
isAnimationLoading = false;
clearComposition();
if (setProgressWhenCompositionSet) {
setProgressWhenCompositionSet = false;
setProgress(progress);
} else {
setProgress(0f);
}
this.composition = composition;
rootAnimatableLayer.setCompDuration(composition.getDuration());
rootAnimatableLayer.setBounds(0, 0, composition.getBounds().width(), composition.getBounds().height());
buildSubviewsForComposition();
requestLayout();
setImageDrawable(rootAnimatableLayer);
if (playAnimationWhenCompositionSet) {
playAnimationWhenCompositionSet = false;
playAnimation();
}
}
private void buildSubviewsForComposition() {
//noinspection ConstantConditions
List<Layer> reversedLayers = composition.getLayers();
Collections.reverse(reversedLayers);
Rect bounds = composition.getBounds();
if (composition.hasMasks() || composition.hasMattes()) {
mainBitmap = Bitmap.createBitmap(bounds.width(), bounds.height(), Bitmap.Config.ARGB_8888);
}
if (composition.hasMasks()) {
maskBitmap = Bitmap.createBitmap(bounds.width(), bounds.height(), Bitmap.Config.ALPHA_8);
}
if (composition.hasMattes()) {
matteBitmap = Bitmap.createBitmap(bounds.width(), bounds.height(), Bitmap.Config.ARGB_8888);
}
LayerView maskedLayer = null;
for (int i = 0; i < reversedLayers.size(); i++) {
Layer layer = reversedLayers.get(i);
LayerView layerView;
if (maskedLayer == null) {
layerView = new LayerView(layer, composition, this, mainBitmap, maskBitmap, matteBitmap);
} else {
if (mainBitmapForMatte == null) {
mainBitmapForMatte = Bitmap.createBitmap(bounds.width(), bounds.height(), Bitmap.Config.ALPHA_8);
}
if (maskBitmapForMatte == null && !layer.getMasks().isEmpty()) {
maskBitmapForMatte = Bitmap.createBitmap(bounds.width(), bounds.height(), Bitmap.Config.ALPHA_8);
}
layerView = new LayerView(layer, composition, this, mainBitmapForMatte, maskBitmapForMatte, null);
}
layerMap.put(layerView.getId(), layerView);
if (maskedLayer != null) {
maskedLayer.setMatte(layerView);
maskedLayer = null;
} else {
if (layer.getMatteType() == Layer.MatteType.Add) {
maskedLayer = layerView;
}
rootAnimatableLayer.addLayer(layerView);
}
}
}
private void clearComposition() {
composition = null;
recycleBitmaps();
rootAnimatableLayer.clearLayers();
layerMap.clear();
}
public void addAnimatorUpdateListener(ValueAnimator.AnimatorUpdateListener updateListener) {
rootAnimatableLayer.addAnimatorUpdateListener(updateListener);
}
public void removeUpdateListener(ValueAnimator.AnimatorUpdateListener updateListener) {
rootAnimatableLayer.removeAnimatorUpdateListener(updateListener);
}
public void addAnimatorListener(Animator.AnimatorListener listener) {
rootAnimatableLayer.addAnimatorListener(listener);
}
public void removeAnimatorListener(Animator.AnimatorListener listener) {
rootAnimatableLayer.removeAnimatorListener(listener);
}
public void loop(boolean loop) {
rootAnimatableLayer.loop(loop);
}
public boolean isAnimating() {
return rootAnimatableLayer.isAnimating();
}
public void playAnimation() {
if (isAnimationLoading) {
playAnimationWhenCompositionSet = true;
return;
}
rootAnimatableLayer.playAnimation();
}
public void cancelAnimation() {
setProgressWhenCompositionSet = false;
playAnimationWhenCompositionSet = false;
rootAnimatableLayer.cancelAnimation();
}
public void setProgress(@FloatRange(from=0f, to=1f) float progress) {
this.progress = progress;
if (isAnimationLoading) {
setProgressWhenCompositionSet = true;
return;
}
rootAnimatableLayer.setProgress(progress);
}
public long getDuration() {
return composition != null ? composition.getDuration() : 0;
}
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];
}
};
}
}