blob: af4003203a9d4daae51a54f28c83c4e63014a68f [file] [log] [blame]
package com.airbnb.lottie;
import android.graphics.PointF;
import android.support.annotation.FloatRange;
import android.support.annotation.Nullable;
import android.support.v4.view.animation.PathInterpolatorCompat;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
class Keyframe<T> {
private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
/**
* The json doesn't include end frames. The data can be taken from the start frame of the next
* keyframe though.
*/
static void setEndFrames(List<? extends Keyframe<?>> keyframes) {
int size = keyframes.size();
for (int i = 0; i < size - 1; i++) {
// In the json, the keyframes only contain their starting frame.
keyframes.get(i).endFrame = keyframes.get(i + 1).startFrame;
}
Keyframe<?> lastKeyframe = keyframes.get(size - 1);
if (lastKeyframe.startValue == null) {
// The only purpose the last keyframe has is to provide the end frame of the previous
// keyframe.
//noinspection SuspiciousMethodCalls
keyframes.remove(lastKeyframe);
}
}
private final LottieComposition composition;
@Nullable final T startValue;
@Nullable final T endValue;
@Nullable final Interpolator interpolator;
@SuppressWarnings("WeakerAccess") final float startFrame;
@SuppressWarnings("WeakerAccess") @Nullable Float endFrame;
public Keyframe(LottieComposition composition, @Nullable T startValue, @Nullable T endValue,
@Nullable Interpolator interpolator, float startFrame, @Nullable Float endFrame) {
this.composition = composition;
this.startValue = startValue;
this.endValue = endValue;
this.interpolator = interpolator;
this.startFrame = startFrame;
this.endFrame = endFrame;
}
@FloatRange(from = 0f, to = 1f)
float getStartProgress() {
return startFrame / composition.getDurationFrames();
}
@FloatRange(from = 0f, to = 1f)
float getEndProgress() {
//noinspection Range
return endFrame == null ? 1f : endFrame / composition.getDurationFrames();
}
boolean isStatic() {
return interpolator == null;
}
boolean containsProgress(@FloatRange(from = 0f, to = 1f) float progress) {
return progress >= getStartProgress() && progress <= getEndProgress();
}
@Override public String toString() {
return "Keyframe{" + "startValue=" + startValue +
", endValue=" + endValue +
", startFrame=" + startFrame +
", endFrame=" + endFrame +
", interpolator=" + interpolator +
'}';
}
static class Factory {
private Factory() {
}
static <T> Keyframe<T> newInstance(JSONObject json, LottieComposition composition, float scale,
AnimatableValue.Factory<T> valueFactory) {
PointF cp1 = null;
PointF cp2 = null;
float startFrame = 0;
T startValue = null;
T endValue = null;
Interpolator interpolator = null;
if (json.has("t")) {
startFrame = (float) json.optDouble("t", 0);
Object startValueJson = json.opt("s");
if (startValueJson != null) {
startValue = valueFactory.valueFromObject(startValueJson, scale);
}
Object endValueJson = json.opt("e");
if (endValueJson != null) {
endValue = valueFactory.valueFromObject(endValueJson, scale);
}
JSONObject cp1Json = json.optJSONObject("o");
JSONObject cp2Json = json.optJSONObject("i");
if (cp1Json != null && cp2Json != null) {
cp1 = JsonUtils.pointFromJsonObject(cp1Json, scale);
cp2 = JsonUtils.pointFromJsonObject(cp2Json, scale);
}
boolean hold = json.optInt("h", 0) == 1;
if (hold) {
endValue = startValue;
// TODO: create a HoldInterpolator so progress changes don't invalidate.
interpolator = LINEAR_INTERPOLATOR;
} else if (cp1 != null) {
interpolator = PathInterpolatorCompat.create(
cp1.x / scale, cp1.y / scale, cp2.x / scale, cp2.y / scale);
} else {
interpolator = LINEAR_INTERPOLATOR;
}
} else {
startValue = valueFactory.valueFromObject(json, scale);
endValue = startValue;
}
return new Keyframe<>(composition, startValue, endValue, interpolator, startFrame, null);
}
static <T> List<Keyframe<T>> parseKeyframes(JSONArray json, LottieComposition composition,
float scale, AnimatableValue.Factory<T> valueFactory) {
int length = json.length();
if (length == 0) {
return Collections.emptyList();
}
List<Keyframe<T>> keyframes = new ArrayList<>();
for (int i = 0; i < length; i++) {
keyframes.add(Keyframe.Factory.newInstance(json.optJSONObject(i), composition, scale,
valueFactory));
}
setEndFrames(keyframes);
return keyframes;
}
}
}