| package com.airbnb.lottie; |
| |
| import android.graphics.PointF; |
| 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.JSONException; |
| import org.json.JSONObject; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| |
| abstract class BaseAnimatableValue<V, O> implements AnimatableValue<O> { |
| final List<V> keyValues = new ArrayList<>(); |
| final List<Float> keyTimes = new ArrayList<>(); |
| final List<Interpolator> interpolators = new ArrayList<>(); |
| long delay; |
| long duration; |
| final LottieComposition composition; |
| private final boolean isDp; |
| |
| private long startFrame; |
| private long durationFrames; |
| private final int frameRate; |
| |
| V initialValue; |
| |
| /** Create a default static animatable path. */ |
| BaseAnimatableValue(LottieComposition composition) { |
| this.composition = composition; |
| isDp = false; |
| frameRate = 0; |
| } |
| |
| BaseAnimatableValue(@Nullable JSONObject json, int frameRate, LottieComposition composition, |
| boolean isDp) { |
| this.frameRate = frameRate; |
| this.composition = composition; |
| this.isDp = isDp; |
| if (json != null) { |
| init(json); |
| } |
| } |
| |
| void init(JSONObject json) { |
| try { |
| Object value = json.get("k"); |
| |
| if (value instanceof JSONArray && |
| ((JSONArray) value).get(0) instanceof JSONObject && |
| ((JSONArray) value).getJSONObject(0).has("t")) { |
| buildAnimationForKeyframes((JSONArray) value); |
| } else { |
| initialValue = valueFromObject(value, getScale()); |
| } |
| } catch (JSONException e) { |
| throw new IllegalStateException("Unable to parse json " + json, e); |
| } |
| } |
| |
| @SuppressWarnings("Duplicates") |
| private void buildAnimationForKeyframes(JSONArray keyframes) { |
| try { |
| for (int i = 0; i < keyframes.length(); i++) { |
| JSONObject kf = keyframes.getJSONObject(i); |
| if (kf.has("t")) { |
| startFrame = kf.getLong("t"); |
| break; |
| } |
| } |
| |
| for (int i = keyframes.length() - 1; i >= 0; i--) { |
| JSONObject keyframe = keyframes.getJSONObject(i); |
| if (keyframe.has("t")) { |
| long endFrame = keyframe.getLong("t"); |
| if (endFrame <= startFrame) { |
| throw new IllegalStateException( |
| "Invalid frame compDuration " + startFrame + "->" + endFrame); |
| } |
| durationFrames = endFrame - startFrame; |
| duration = (long) (durationFrames / (float) frameRate * 1000); |
| delay = (long) (startFrame / (float) frameRate * 1000); |
| break; |
| } |
| } |
| |
| boolean addStartValue = true; |
| boolean addTimePadding = false; |
| V outValue = null; |
| |
| for (int i = 0; i < keyframes.length(); i++) { |
| JSONObject keyframe = keyframes.getJSONObject(i); |
| long frame = keyframe.getLong("t"); |
| float timePercentage = (float) (frame - startFrame) / (float) durationFrames; |
| |
| if (outValue != null) { |
| keyValues.add(outValue); |
| interpolators.add(new LinearInterpolator()); |
| outValue = null; |
| } |
| |
| V startValue = |
| keyframe.has("s") ? valueFromObject(keyframe.getJSONArray("s"), getScale()) : null; |
| if (addStartValue) { |
| if (startValue != null) { |
| if (i == 0) { |
| //noinspection ResourceAsColor |
| initialValue = startValue; |
| } |
| keyValues.add(startValue); |
| if (!interpolators.isEmpty()) { |
| interpolators.add(new LinearInterpolator()); |
| } |
| } |
| addStartValue = false; |
| } |
| |
| if (addTimePadding) { |
| float holdPercentage = timePercentage - 0.00001f; |
| keyTimes.add(holdPercentage); |
| addTimePadding = false; |
| } |
| |
| if (keyframe.has("e")) { |
| V endValue = valueFromObject(keyframe.getJSONArray("e"), getScale()); |
| keyValues.add(endValue); |
| /* |
| Timing function for time interpolation between keyframes. |
| Should be n - 1 where n is the number of keyframes. |
| */ |
| Interpolator timingFunction; |
| if (keyframe.has("o") && keyframe.has("i")) { |
| JSONObject timingControlPoint1 = keyframe.getJSONObject("o"); |
| JSONObject timingControlPoint2 = keyframe.getJSONObject("i"); |
| PointF cp1 = JsonUtils.pointValueFromJsonObject(timingControlPoint1, 1); |
| PointF cp2 = JsonUtils.pointValueFromJsonObject(timingControlPoint2, 1); |
| |
| timingFunction = PathInterpolatorCompat.create(cp1.x, cp1.y, cp2.x, cp2.y); |
| } else { |
| timingFunction = new LinearInterpolator(); |
| } |
| interpolators.add(timingFunction); |
| } |
| |
| keyTimes.add(timePercentage); |
| |
| if (keyframe.has("h") && keyframe.getInt("h") == 1) { |
| outValue = startValue; |
| addStartValue = true; |
| addTimePadding = true; |
| } |
| } |
| } catch (JSONException e) { |
| throw new IllegalArgumentException("Unable to parse values.", e); |
| } |
| } |
| |
| private float getScale() { |
| return isDp ? composition.getScale() : 1f; |
| } |
| |
| O convertType(V value) { |
| //noinspection unchecked |
| return (O) value; |
| } |
| |
| public boolean hasAnimation() { |
| return !keyValues.isEmpty(); |
| } |
| |
| public O getInitialValue() { |
| return convertType(initialValue); |
| } |
| |
| protected abstract V valueFromObject(Object object, float scale) throws JSONException; |
| |
| public abstract KeyframeAnimation<O> createAnimation(); |
| |
| @Override public String toString() { |
| final StringBuilder sb = new StringBuilder(); |
| sb.append("initialValue=").append(initialValue); |
| if (!keyValues.isEmpty()) { |
| sb.append(", values=").append(Arrays.toString(keyTimes.toArray())); |
| } |
| return sb.toString(); |
| } |
| } |