| 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 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; |
| |
| import static com.airbnb.lottie.Utils.closeQuietly; |
| |
| /** |
| * 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 { |
| |
| private final Map<String, List<Layer>> precomps = new HashMap<>(); |
| private final Map<String, LottieImageAsset> images = new HashMap<>(); |
| private final LongSparseArray<Layer> layerMap = new LongSparseArray<>(); |
| private final List<Layer> layers = new ArrayList<>(); |
| private final Rect bounds; |
| private final long startFrame; |
| private final long endFrame; |
| private final int frameRate; |
| private final float dpScale; |
| |
| private LottieComposition( |
| Rect bounds, long startFrame, long endFrame, int frameRate, float dpScale) { |
| this.bounds = bounds; |
| this.startFrame = startFrame; |
| this.endFrame = endFrame; |
| this.frameRate = frameRate; |
| this.dpScale = dpScale; |
| } |
| |
| 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); |
| } |
| |
| boolean hasImages() { |
| return !images.isEmpty(); |
| } |
| |
| Map<String, LottieImageAsset> getImages() { |
| return images; |
| } |
| |
| float getDurationFrames() { |
| return getDuration() * (float) frameRate / 1000f; |
| } |
| |
| |
| public float getDpScale() { |
| return dpScale; |
| } |
| |
| @Override public String toString() { |
| final StringBuilder sb = new StringBuilder("LottieComposition:\n"); |
| for (Layer layer : layers) { |
| sb.append(layer.toString("\t")); |
| } |
| return sb.toString(); |
| } |
| |
| public static class Factory { |
| private Factory() { |
| } |
| |
| /** |
| * 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.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, stream); |
| return loader; |
| } |
| |
| static LottieComposition fromFileSync(Context context, String fileName) { |
| InputStream stream; |
| try { |
| stream = context.getAssets().open(fileName); |
| } catch (IOException e) { |
| throw new IllegalStateException("Unable to find file " + fileName, e); |
| } |
| return fromInputStream(context.getResources(), stream); |
| } |
| |
| /** |
| * 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.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, json); |
| return loader; |
| } |
| |
| @SuppressWarnings("WeakerAccess") |
| static LottieComposition fromInputStream(Resources res, InputStream stream) { |
| try { |
| // TODO: It's not correct to use available() to allocate the byte array. |
| int size = stream.available(); |
| byte[] buffer = new byte[size]; |
| //noinspection ResultOfMethodCallIgnored |
| stream.read(buffer); |
| String json = new String(buffer, "UTF-8"); |
| JSONObject jsonObject = new JSONObject(json); |
| return 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); |
| } finally { |
| closeQuietly(stream); |
| } |
| } |
| |
| @SuppressWarnings("WeakerAccess") |
| static LottieComposition fromJsonSync(Resources res, JSONObject json) { |
| Rect bounds = null; |
| float scale = res.getDisplayMetrics().density; |
| int width = json.optInt("w", -1); |
| int height = json.optInt("h", -1); |
| |
| if (width != -1 && height != -1) { |
| int scaledWidth = (int) (width * scale); |
| int scaledHeight = (int) (height * scale); |
| bounds = new Rect(0, 0, scaledWidth, scaledHeight); |
| } |
| |
| long startFrame = json.optLong("ip", 0); |
| long endFrame = json.optLong("op", 0); |
| int frameRate = json.optInt("fr", 0); |
| LottieComposition composition = |
| new LottieComposition(bounds, startFrame, endFrame, frameRate, scale); |
| JSONArray assetsJson = json.optJSONArray("assets"); |
| parseImages(assetsJson, composition); |
| parsePrecomps(assetsJson, composition); |
| parseLayers(json, composition); |
| return composition; |
| } |
| |
| private static void parseLayers(JSONObject json, LottieComposition composition) { |
| JSONArray jsonLayers = json.optJSONArray("layers"); |
| int length = jsonLayers.length(); |
| for (int i = 0; i < length; i++) { |
| Layer layer = Layer.Factory.newInstance(jsonLayers.optJSONObject(i), composition); |
| addLayer(composition.layers, composition.layerMap, layer); |
| } |
| } |
| |
| private static void parsePrecomps( |
| @Nullable JSONArray assetsJson, LottieComposition composition) { |
| if (assetsJson == null) { |
| return; |
| } |
| int length = assetsJson.length(); |
| for (int i = 0; i < length; i++) { |
| JSONObject assetJson = assetsJson.optJSONObject(i); |
| JSONArray layersJson = assetJson.optJSONArray("layers"); |
| if (layersJson == null) { |
| continue; |
| } |
| List<Layer> layers = new ArrayList<>(layersJson.length()); |
| LongSparseArray<Layer> layerMap = new LongSparseArray<>(); |
| for (int j = 0; j < layersJson.length(); j++) { |
| Layer layer = Layer.Factory.newInstance(layersJson.optJSONObject(j), composition); |
| layerMap.put(layer.getId(), layer); |
| layers.add(layer); |
| } |
| String id = assetJson.optString("id"); |
| composition.precomps.put(id, layers); |
| } |
| } |
| |
| private static void parseImages( |
| @Nullable JSONArray assetsJson, LottieComposition composition) { |
| if (assetsJson == null) { |
| return; |
| } |
| int length = assetsJson.length(); |
| for (int i = 0; i < length; i++) { |
| JSONObject assetJson = assetsJson.optJSONObject(i); |
| if (!assetJson.has("p")) { |
| continue; |
| } |
| LottieImageAsset image = LottieImageAsset.Factory.newInstance(assetJson); |
| composition.images.put(image.getId(), image); |
| } |
| } |
| |
| private static void addLayer(List<Layer> layers, LongSparseArray<Layer> layerMap, Layer layer) { |
| layers.add(layer); |
| layerMap.put(layer.getId(), layer); |
| } |
| } |
| } |