Skip to content

Commit

Permalink
Initial support for linear gradients (airbnb#199)
Browse files Browse the repository at this point in the history
  • Loading branch information
gpeal authored Mar 16, 2017
1 parent 413fe8c commit 78c6520
Show file tree
Hide file tree
Showing 7 changed files with 336 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.airbnb.lottie;

import android.graphics.Color;

import org.json.JSONArray;
import org.json.JSONObject;

import java.util.List;

class AnimatableGradientColorValue extends BaseAnimatableValue<GradientColor, GradientColor> {
private AnimatableGradientColorValue(
List<Keyframe<GradientColor>> keyframes, GradientColor initialValue) {
super(keyframes, initialValue);
}

@Override public KeyframeAnimation<GradientColor> createAnimation() {
if (!hasAnimation()) {
return new StaticKeyframeAnimation<>(initialValue);
}
return new GradientColorKeyframeAnimation(keyframes);
}

static final class Factory {
private Factory() {
}

static AnimatableGradientColorValue newInstance(
JSONObject json, LottieComposition composition) {
AnimatableValueParser.Result<GradientColor> result = AnimatableValueParser
.newInstance(json, 1, composition, ValueFactory.INSTANCE)
.parseJson();
GradientColor initialValue = result.initialValue;
return new AnimatableGradientColorValue(result.keyframes, initialValue);
}
}

private static class ValueFactory implements AnimatableValue.Factory<GradientColor> {
private static final ValueFactory INSTANCE = new ValueFactory();

private ValueFactory() {
}

@Override public GradientColor valueFromObject(Object object, float scale) {
JSONArray array = (JSONArray) object;
int size = array.length() / 4;
float[] positions = new float[size];
int[] colors = new int[size];
GradientColor gradientColor = new GradientColor(positions, colors);
int r = 0;
int g = 0;
for (int i = 0; i < array.length(); i++) {
int colorIndex = i / 4;
double value = array.optDouble(i);
switch (i % 4) {
case 0:
// position
positions[colorIndex] = (float) value;
break;
case 1:
r = (int) (value * 255);
break;
case 2:
g = (int) (value * 255);
break;
case 3:
int b = (int) (value * 255);
colors[colorIndex] = Color.argb(255, r, g, b);
break;
}
}
return gradientColor;
}
}
}
2 changes: 2 additions & 0 deletions lottie/src/main/java/com/airbnb/lottie/ContentGroup.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ class ContentGroup implements DrawingContent, PathContent,
Object item = items.get(i);
if (item instanceof ShapeFill) {
contents.add(new FillContent(lottieDrawable, layer, (ShapeFill) item));
} else if (item instanceof GradientFill) {
contents.add(new GradientFillContent(lottieDrawable, layer, (GradientFill) item));
} else if (item instanceof ShapeStroke) {
contents.add(new StrokeContent(lottieDrawable, layer, (ShapeStroke) item));
} else if (item instanceof ShapeGroup) {
Expand Down
35 changes: 35 additions & 0 deletions lottie/src/main/java/com/airbnb/lottie/GradientColor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.airbnb.lottie;

class GradientColor {
private final float[] positions;
private final int[] colors;

GradientColor(float[] positions, int[] colors) {
this.positions = positions;
this.colors = colors;
}

float[] getPositions() {
return positions;
}

int[] getColors() {
return colors;
}

int getSize() {
return colors.length;
}

void lerp(GradientColor gc1, GradientColor gc2, float progress) {
if (gc1.colors.length != gc2.colors.length) {
throw new IllegalArgumentException("Cannot interpolate between gradients. Lengths vary (" +
gc1.colors.length + " vs " + gc2.colors.length + ")");
}

for (int i = 0; i < gc1.colors.length; i++) {
positions[i] = MiscUtils.lerp(gc1.positions[i], gc2.positions[i], progress);
colors[i] = GammaEvaluator.evaluate(progress, gc1.colors[i], gc2.colors[i]);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.airbnb.lottie;

import java.util.List;

class GradientColorKeyframeAnimation extends KeyframeAnimation<GradientColor> {
private final GradientColor gradientColor;

GradientColorKeyframeAnimation(List<? extends Keyframe<GradientColor>> keyframes) {
super(keyframes);
GradientColor startValue = keyframes.get(0).startValue;
int size = startValue == null ? 0 : startValue.getSize();
gradientColor = new GradientColor(new float[size], new int[size]);
}

@Override GradientColor getValue(Keyframe<GradientColor> keyframe, float keyframeProgress) {
gradientColor.lerp(keyframe.startValue, keyframe.endValue, keyframeProgress);
return gradientColor;
}
}
116 changes: 116 additions & 0 deletions lottie/src/main/java/com/airbnb/lottie/GradientFill.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package com.airbnb.lottie;

import android.graphics.Path;
import android.support.annotation.Nullable;

import org.json.JSONObject;

class GradientFill {

enum GradientType {
Linear,
Radial
}

private final GradientType gradientType;
private final Path.FillType fillType;
private final AnimatableGradientColorValue gradientColor;
private final AnimatableIntegerValue opacity;
private final AnimatablePointValue startPoint;
private final AnimatablePointValue endPoint;
@Nullable private final AnimatableFloatValue highlightLength;
@Nullable private final AnimatableFloatValue highlightAngle;

private GradientFill(GradientType gradientType, Path.FillType fillType,
AnimatableGradientColorValue gradientColor,
AnimatableIntegerValue opacity, AnimatablePointValue startPoint,
AnimatablePointValue endPoint, AnimatableFloatValue highlightLength,
AnimatableFloatValue highlightAngle) {
this.gradientType = gradientType;
this.fillType = fillType;
this.gradientColor = gradientColor;
this.opacity = opacity;
this.startPoint = startPoint;
this.endPoint = endPoint;
this.highlightLength = highlightLength;
this.highlightAngle = highlightAngle;
}

GradientType getGradientType() {
return gradientType;
}

Path.FillType getFillType() {
return fillType;
}

AnimatableGradientColorValue getGradientColor() {
return gradientColor;
}

AnimatableIntegerValue getOpacity() {
return opacity;
}

AnimatablePointValue getStartPoint() {
return startPoint;
}

AnimatablePointValue getEndPoint() {
return endPoint;
}

@Nullable AnimatableFloatValue getHighlightLength() {
return highlightLength;
}

@Nullable AnimatableFloatValue getHighlightAngle() {
return highlightAngle;
}

static class Factory {
private Factory() {
}

static GradientFill newInstance(JSONObject json, LottieComposition composition) {
JSONObject jsonColor = json.optJSONObject("g");
if (jsonColor != null && jsonColor.has("k")) {
jsonColor = jsonColor.optJSONObject("k");
}
AnimatableGradientColorValue color = null;
if (jsonColor != null) {
color = AnimatableGradientColorValue.Factory.newInstance(jsonColor, composition);
}

JSONObject jsonOpacity = json.optJSONObject("o");
AnimatableIntegerValue opacity = null;
if (jsonOpacity != null) {
opacity = AnimatableIntegerValue.Factory.newInstance(jsonOpacity, composition);
}

int fillTypeInt = json.optInt("r", 1);
Path.FillType fillType = fillTypeInt == 1 ? Path.FillType.WINDING : Path.FillType.EVEN_ODD;

int gradientTypeInt = json.optInt("r", 1);
GradientType gradientType = gradientTypeInt == 1 ? GradientType.Linear : GradientType.Radial;

JSONObject jsonStartPoint = json.optJSONObject("s");
AnimatablePointValue startPoint = null;
if (jsonStartPoint != null) {
startPoint = AnimatablePointValue.Factory.newInstance(jsonStartPoint, composition);
}

JSONObject jsonEndPoint = json.optJSONObject("e");
AnimatablePointValue endPoint = null;
if (jsonEndPoint != null) {
endPoint = AnimatablePointValue.Factory.newInstance(jsonEndPoint, composition);
}

// TODO: radial gradients.

return new GradientFill(
gradientType, fillType, color, opacity, startPoint, endPoint, null, null);

}
}
}
88 changes: 88 additions & 0 deletions lottie/src/main/java/com/airbnb/lottie/GradientFillContent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.airbnb.lottie;

import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.Shader;

import java.util.ArrayList;
import java.util.List;

class GradientFillContent implements DrawingContent, BaseKeyframeAnimation.AnimationListener {
private final Path path = new Path();
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final RectF boundsRect = new RectF();
private final List<PathContent> paths = new ArrayList<>();
private final KeyframeAnimation<GradientColor> colorAnimation;
private final KeyframeAnimation<Integer> opacityAnimation;
private final KeyframeAnimation<PointF> startPointAnimation;
private final KeyframeAnimation<PointF> endPointAnimation;
private final LottieDrawable lottieDrawable;

GradientFillContent(final LottieDrawable lottieDrawable, BaseLayer layer, GradientFill fill) {
this.lottieDrawable = lottieDrawable;
path.setFillType(fill.getFillType());

colorAnimation = fill.getGradientColor().createAnimation();
colorAnimation.addUpdateListener(this);
layer.addAnimation(colorAnimation);

opacityAnimation = fill.getOpacity().createAnimation();
opacityAnimation.addUpdateListener(this);
layer.addAnimation(opacityAnimation);

startPointAnimation = fill.getStartPoint().createAnimation();
startPointAnimation.addUpdateListener(this);
layer.addAnimation(startPointAnimation);

endPointAnimation = fill.getEndPoint().createAnimation();
endPointAnimation.addUpdateListener(this);
layer.addAnimation(endPointAnimation);
}

@Override public void onValueChanged() {
lottieDrawable.invalidateSelf();
}

@Override public void setContents(List<Content> contentsBefore, List<Content> contentsAfter) {
for (int i = 0; i < contentsAfter.size(); i++) {
Content content = contentsAfter.get(i);
if (content instanceof PathContent) {
paths.add((PathContent) content);
}
}
}

@Override public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
path.reset();
for (int i = 0; i < paths.size(); i++) {
path.addPath(paths.get(i).getPath(), parentMatrix);
}

path.computeBounds(boundsRect, false);

paint.setShader(getShader());
int alpha = (int) ((parentAlpha / 255f * opacityAnimation.getValue() / 100f) * 255);
paint.setAlpha(alpha);

canvas.drawPath(path, paint);
}

private LinearGradient getShader() {
// TODO: cache these
PointF startPoint = startPointAnimation.getValue();
PointF endPoint = endPointAnimation.getValue();
GradientColor gradientColor = colorAnimation.getValue();
int[] colors = gradientColor.getColors();
float[] positions = gradientColor.getPositions();
int x0 = (int) (boundsRect.left + boundsRect.width() / 2 + startPoint.x);
int y0 = (int) (boundsRect.top + boundsRect.height() / 2 + startPoint.y);
int x1 = (int) (boundsRect.left + boundsRect.width() / 2 + endPoint.x);
int y1 = (int) (boundsRect.top + boundsRect.height() / 2 + endPoint.y);
return new LinearGradient(x0 , y0, x1, y1, colors, positions, Shader.TileMode.CLAMP);
}
}
2 changes: 2 additions & 0 deletions lottie/src/main/java/com/airbnb/lottie/ShapeGroup.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ class ShapeGroup {
return ShapeStroke.Factory.newInstance(json, composition);
case "fl":
return ShapeFill.Factory.newInstance(json, composition);
case "gf":
return GradientFill.Factory.newInstance(json, composition);
case "tr":
return AnimatableTransform.Factory.newInstance(json, composition);
case "sh":
Expand Down

0 comments on commit 78c6520

Please sign in to comment.