blob: aaf42fe787a78f6297d595c066b25df3d4c31a2a [file] [log] [blame]
package com.airbnb.lottie;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Rect;
import android.os.AsyncTask;
import android.support.annotation.Nullable;
import android.support.v4.util.LongSparseArray;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* After Effects/Bodymovin composition model. This is the serialized model from which the
* animation will be created.
* It can be used with a {@link com.airbnb.lottie.LottieAnimationView} or
* {@link com.airbnb.lottie.LottieDrawable}.
*/
public class LottieComposition {
public interface OnCompositionLoadedListener {
void onCompositionLoaded(LottieComposition composition);
}
interface Cancellable {
void cancel();
}
/**
* The largest bitmap drawing cache can be is 8,294,400 bytes. There are 4 bytes per pixel
* leaving ~2.3M pixels available.
* Reduce the number a little bit for safety.
* <p>
* Hopefully this can be hardware accelerated someday.
*/
private static final int MAX_PIXELS = 1000;
/**
* Loads a composition from a file stored in /assets.
*/
public static Cancellable fromAssetFileName(Context context, String fileName,
OnCompositionLoadedListener loadedListener) {
InputStream stream;
try {
stream = context.getAssets().open(fileName);
} catch (IOException e) {
throw new IllegalStateException("Unable to find file " + fileName, e);
}
return fromInputStream(context, stream, loadedListener);
}
/**
* Loads a composition from an arbitrary input stream.
* <p>
* ex: fromInputStream(context, new FileInputStream(filePath), (composition) -> {});
*/
public static Cancellable fromInputStream(Context context, InputStream stream,
OnCompositionLoadedListener loadedListener) {
FileCompositionLoader loader =
new FileCompositionLoader(context.getResources(), loadedListener);
loader.execute(stream);
return loader;
}
static LottieComposition fromFileSync(Context context, String fileName) {
InputStream file;
try {
file = context.getAssets().open(fileName);
} catch (IOException e) {
throw new IllegalStateException("Unable to find file " + fileName, e);
}
return fromInputStream(context.getResources(), file);
}
/**
* Loads a composition from a raw json object. This is useful for animations loaded from the
* network.
*/
public static Cancellable fromJson(Resources res, JSONObject json,
OnCompositionLoadedListener loadedListener) {
JsonCompositionLoader loader = new JsonCompositionLoader(res, loadedListener);
loader.execute(json);
return loader;
}
@SuppressWarnings("WeakerAccess")
static LottieComposition fromInputStream(Resources res, 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");
JSONObject jsonObject = new JSONObject(json);
return LottieComposition.fromJsonSync(res, jsonObject);
} catch (IOException e) {
throw new IllegalStateException("Unable to find file.", e);
} catch (JSONException e) {
throw new IllegalStateException("Unable to load JSON.", e);
}
}
@SuppressWarnings("WeakerAccess")
static LottieComposition fromJsonSync(Resources res, JSONObject json) {
LottieComposition composition = new LottieComposition(res);
int width = json.optInt("w", -1);
int height = json.optInt("h", -1);
if (width != -1 && height != -1) {
int scaledWidth = (int) (width * composition.scale);
int scaledHeight = (int) (height * composition.scale);
if (Math.max(scaledWidth, scaledHeight) > MAX_PIXELS) {
float factor = (float) MAX_PIXELS / (float) Math.max(scaledWidth, scaledHeight);
scaledWidth *= factor;
scaledHeight *= factor;
composition.scale *= factor;
}
composition.bounds = new Rect(0, 0, scaledWidth, scaledHeight);
}
composition.startFrame = json.optLong("ip", 0);
composition.endFrame = json.optLong("op", 0);
composition.frameRate = json.optInt("fr", 0);
JSONArray jsonLayers = json.optJSONArray("layers");
for (int i = 0; i < jsonLayers.length(); i++) {
Layer layer = new Layer(jsonLayers.optJSONObject(i), composition);
addLayer(composition, layer);
}
JSONArray precompsJson = json.optJSONArray("assets");
for (int i = 0; i < precompsJson.length(); i++) {
JSONObject precomp = precompsJson.optJSONObject(i);
JSONArray layersJson = precomp.optJSONArray("layers");
if (layersJson == null) {
Log.w(L.TAG, "Lottie doesn't yet support images.");
// TODO: image support
continue;
}
List<Layer> layers = new ArrayList<>(layersJson.length());
LongSparseArray<Layer> layerMap = new LongSparseArray<>();
for (int j = 0; j < layersJson.length(); j++) {
Layer layer = new Layer(layersJson.optJSONObject(j), composition);
layerMap.put(layer.getId(), layer);
layers.add(layer);
}
String id = precomp.optString("id");
composition.precomps.put(id, layers);
}
return composition;
}
private static void addLayer(LottieComposition composition, Layer layer) {
composition.layers.add(layer);
composition.layerMap.put(layer.getId(), layer);
}
private final Map<String, List<Layer>> precomps = new HashMap<>();
private final LongSparseArray<Layer> layerMap = new LongSparseArray<>();
private final List<Layer> layers = new ArrayList<>();
private Rect bounds;
private long startFrame;
private long endFrame;
private int frameRate;
private float scale;
private LottieComposition(Resources res) {
scale = res.getDisplayMetrics().density;
}
Layer layerModelForId(long id) {
return layerMap.get(id);
}
@SuppressWarnings("WeakerAccess") public Rect getBounds() {
return bounds;
}
@SuppressWarnings("WeakerAccess") public long getDuration() {
long frameDuration = endFrame - startFrame;
return (long) (frameDuration / (float) frameRate * 1000);
}
long getEndFrame() {
return endFrame;
}
List<Layer> getLayers() {
return layers;
}
@Nullable
List<Layer> getPrecomps(String id) {
return precomps.get(id);
}
float getDurationFrames() {
return getDuration() * (float) frameRate / 1000f;
}
public float getScale() {
return scale;
}
@Override public String toString() {
final StringBuilder sb = new StringBuilder("LottieComposition:\n");
for (Layer layer : layers) {
sb.append(layer.toString("\t"));
}
return sb.toString();
}
private static final class FileCompositionLoader extends CompositionLoader<InputStream> {
private final Resources res;
private final OnCompositionLoadedListener loadedListener;
FileCompositionLoader(Resources res, OnCompositionLoadedListener loadedListener) {
this.res = res;
this.loadedListener = loadedListener;
}
@Override
protected LottieComposition doInBackground(InputStream... params) {
return fromInputStream(res, params[0]);
}
@Override
protected void onPostExecute(LottieComposition composition) {
loadedListener.onCompositionLoaded(composition);
}
}
private static final class JsonCompositionLoader extends CompositionLoader<JSONObject> {
private final Resources res;
private final OnCompositionLoadedListener loadedListener;
JsonCompositionLoader(Resources res, OnCompositionLoadedListener loadedListener) {
this.res = res;
this.loadedListener = loadedListener;
}
@Override
protected LottieComposition doInBackground(JSONObject... params) {
return fromJsonSync(res, params[0]);
}
@Override
protected void onPostExecute(LottieComposition composition) {
loadedListener.onCompositionLoaded(composition);
}
}
private abstract static class CompositionLoader<Params>
extends AsyncTask<Params, Void, LottieComposition>
implements Cancellable {
@Override public void cancel() {
cancel(true);
}
}
}