Added support for Skew and Skew Angle (#1118)
Fixes #409
diff --git a/After Effects Samples/Skew.aep b/After Effects Samples/Skew.aep
new file mode 100644
index 0000000..2b02bf5
--- /dev/null
+++ b/After Effects Samples/Skew.aep
Binary files differ
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 77cd7af..ba23304 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
# 3.0.0-beta2
### Features and Improvements
+* Added support for skew and skew angle in transforms.
* Added support for markers. You can now call `setMinFrame`, `setMaxFrame` and `setMinAndMaxFrame` with a marker name.
# 3.0.0-beta1
diff --git a/LottieSample/src/main/assets/Tests/Skew.json b/LottieSample/src/main/assets/Tests/Skew.json
new file mode 100644
index 0000000..323792d
--- /dev/null
+++ b/LottieSample/src/main/assets/Tests/Skew.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":29.9700012207031,"ip":0,"op":61.0000024845809,"w":200,"h":200,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[100,100,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[50,50],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"rc","d":1,"s":{"a":0,"k":[50,50],"ix":2},"p":{"a":0,"k":[98,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 2","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"rc","d":1,"s":{"a":0,"k":[50,50],"ix":2},"p":{"a":0,"k":[98,97],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 3","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-47.551,-73.523],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":45,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":61.0000024845809,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/LottieSample/src/main/assets/Tests/StarSkew.json b/LottieSample/src/main/assets/Tests/StarSkew.json
new file mode 100644
index 0000000..14092c2
--- /dev/null
+++ b/LottieSample/src/main/assets/Tests/StarSkew.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":29.9700012207031,"ip":0,"op":61.0000024845809,"w":200,"h":200,"nm":"Comp 2","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[100,100,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":1,"d":1,"pt":{"a":0,"k":5,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"ir":{"a":0,"k":17,"ix":6},"is":{"a":0,"k":0,"ix":8},"or":{"a":0,"k":58,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"rc","d":1,"s":{"a":0,"k":[4,4],"ix":2},"p":{"a":0,"k":[57,-20],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":54.5,"ix":4},"sa":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0],"e":[360]},{"t":60.0000024438501}],"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":61.0000024845809,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieProperty.java b/lottie/src/main/java/com/airbnb/lottie/LottieProperty.java
index 4d27a74..54d6dcd 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieProperty.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieProperty.java
@@ -18,6 +18,8 @@
* {@link #TRANSFORM_OPACITY}
* {@link #TRANSFORM_SCALE}
* {@link #TRANSFORM_ROTATION}
+ * {@link #TRANSFORM_SKEW}
+ * {@link #TRANSFORM_SKEW_ANGLE}
*
* Fill:
* {@link #COLOR} (non-gradient)
@@ -77,6 +79,10 @@
/** In degrees */
Float TRANSFORM_ROTATION = 1f;
+ /** 0-85 */
+ Float TRANSFORM_SKEW = 0f;
+ /** In degrees */
+ Float TRANSFORM_SKEW_ANGLE = 0f;
/** In Px */
Float STROKE_WIDTH = 2f;
Float TEXT_TRACKING = 3f;
diff --git a/lottie/src/main/java/com/airbnb/lottie/animation/keyframe/TransformKeyframeAnimation.java b/lottie/src/main/java/com/airbnb/lottie/animation/keyframe/TransformKeyframeAnimation.java
index 0932284..fe3b160 100644
--- a/lottie/src/main/java/com/airbnb/lottie/animation/keyframe/TransformKeyframeAnimation.java
+++ b/lottie/src/main/java/com/airbnb/lottie/animation/keyframe/TransformKeyframeAnimation.java
@@ -15,10 +15,16 @@
import static com.airbnb.lottie.LottieProperty.TRANSFORM_POSITION;
import static com.airbnb.lottie.LottieProperty.TRANSFORM_ROTATION;
import static com.airbnb.lottie.LottieProperty.TRANSFORM_SCALE;
+import static com.airbnb.lottie.LottieProperty.TRANSFORM_SKEW;
+import static com.airbnb.lottie.LottieProperty.TRANSFORM_SKEW_ANGLE;
import static com.airbnb.lottie.LottieProperty.TRANSFORM_START_OPACITY;
public class TransformKeyframeAnimation {
private final Matrix matrix = new Matrix();
+ private final Matrix skewMatrix1;
+ private final Matrix skewMatrix2;
+ private final Matrix skewMatrix3;
+ private final float[] skewValues;
private boolean isIdentity;
private final BaseKeyframeAnimation<PointF, PointF> anchorPoint;
@@ -26,6 +32,8 @@
private final BaseKeyframeAnimation<ScaleXY, ScaleXY> scale;
private final BaseKeyframeAnimation<Float, Float> rotation;
private final BaseKeyframeAnimation<Integer, Integer> opacity;
+ @Nullable private final FloatKeyframeAnimation skew;
+ @Nullable private final FloatKeyframeAnimation skewAngle;
// Used for repeaters
@Nullable private final BaseKeyframeAnimation<?, Float> startOpacity;
@@ -38,11 +46,30 @@
position = null;
scale = null;
rotation = null;
+ skew = null;
+ skewAngle = null;
+ skewMatrix1 = null;
+ skewMatrix2 = null;
+ skewMatrix3 = null;
+ skewValues = null;
} else {
anchorPoint = animatableTransform.getAnchorPoint().createAnimation();
position = animatableTransform.getPosition().createAnimation();
scale = animatableTransform.getScale().createAnimation();
rotation = animatableTransform.getRotation().createAnimation();
+ skew = animatableTransform.getSkew() == null ? null : (FloatKeyframeAnimation) animatableTransform.getSkew().createAnimation();
+ if (skew != null) {
+ skewMatrix1 = new Matrix();
+ skewMatrix2 = new Matrix();
+ skewMatrix3 = new Matrix();
+ skewValues = new float[9];
+ } else {
+ skewMatrix1 = null;
+ skewMatrix2 = null;
+ skewMatrix3 = null;
+ skewValues = null;
+ }
+ skewAngle = animatableTransform.getSkewAngle() == null ? null : (FloatKeyframeAnimation) animatableTransform.getSkewAngle().createAnimation();
}
opacity = animatableTransform.getOpacity().createAnimation();
if (animatableTransform.getStartOpacity() != null) {
@@ -73,6 +100,12 @@
layer.addAnimation(position);
layer.addAnimation(scale);
layer.addAnimation(rotation);
+ if (skew != null) {
+ layer.addAnimation(skew);
+ }
+ if (skewAngle != null) {
+ layer.addAnimation(skewAngle);
+ }
}
public void addListener(final BaseKeyframeAnimation.AnimationListener listener) {
@@ -91,6 +124,12 @@
position.addUpdateListener(listener);
scale.addUpdateListener(listener);
rotation.addUpdateListener(listener);
+ if (skew != null) {
+ skew.addUpdateListener(listener);
+ }
+ if (skewAngle != null) {
+ skewAngle.addUpdateListener(listener);
+ }
}
public void setProgress(float progress) {
@@ -109,6 +148,12 @@
position.setProgress(progress);
scale.setProgress(progress);
rotation.setProgress(progress);
+ if (skew != null) {
+ skew.setProgress(progress);
+ }
+ if (skewAngle != null) {
+ skewAngle.setProgress(progress);
+ }
}
public BaseKeyframeAnimation<?, Integer> getOpacity() {
@@ -123,7 +168,6 @@
return endOpacity;
}
-
public Matrix getMatrix() {
if (isIdentity) {
return matrix;
@@ -139,6 +183,36 @@
matrix.preRotate(rotation);
}
+ if (skew != null) {
+ float mCos = skewAngle == null ? 0f : (float) Math.cos(Math.toRadians(-skewAngle.getFloatValue() + 90));
+ float mSin = skewAngle == null ? 0f : (float) Math.sin(Math.toRadians(-skewAngle.getFloatValue() + 90));
+ float aTan = (float) Math.tan(Math.toRadians(skew.getFloatValue()));
+ clearSkewValues();
+ skewValues[0] = mCos;
+ skewValues[1] = mSin;
+ skewValues[3] = -mSin;
+ skewValues[4] = mCos;
+ skewValues[8] = 1f;
+ skewMatrix1.setValues(skewValues);
+ clearSkewValues();
+ skewValues[0] = 1f;
+ skewValues[3] = aTan;
+ skewValues[4] = 1f;
+ skewValues[8] = 1f;
+ skewMatrix2.setValues(skewValues);
+ clearSkewValues();
+ skewValues[0] = mCos;
+ skewValues[1] = -mSin;
+ skewValues[3] = mSin;
+ skewValues[4] = mCos;
+ skewValues[8] = 1;
+ skewMatrix3.setValues(skewValues);
+ skewMatrix2.preConcat(skewMatrix1);
+ skewMatrix3.preConcat(skewMatrix2);
+
+ matrix.preConcat(skewMatrix3);
+ }
+
ScaleXY scaleTransform = this.scale.getValue();
if (scaleTransform.getScaleX() != 1f || scaleTransform.getScaleY() != 1f) {
matrix.preScale(scaleTransform.getScaleX(), scaleTransform.getScaleY());
@@ -148,9 +222,16 @@
if (anchorPoint.x != 0 || anchorPoint.y != 0) {
matrix.preTranslate(-anchorPoint.x, -anchorPoint.y);
}
+
return matrix;
}
+ private void clearSkewValues() {
+ for (int i = 0; i < 9; i++) {
+ skewValues[i] = 0f;
+ }
+ }
+
/**
* TODO: see if we can use this for the main {@link #getMatrix()} method.
*/
@@ -193,6 +274,10 @@
startOpacity.setValueCallback((LottieValueCallback<Float>) callback);
} else if (property == TRANSFORM_END_OPACITY && endOpacity != null) {
endOpacity.setValueCallback((LottieValueCallback<Float>) callback);
+ } else if (property == TRANSFORM_SKEW && skew != null) {
+ skew.setValueCallback((LottieValueCallback<Float>) callback);
+ } else if (property == TRANSFORM_SKEW_ANGLE && skewAngle != null) {
+ skewAngle.setValueCallback((LottieValueCallback<Float>) callback);
} else {
return false;
}
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableTransform.java b/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableTransform.java
index 20c0ccf..6af0679 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableTransform.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableTransform.java
@@ -19,6 +19,8 @@
private final AnimatableFloatValue rotation;
private final AnimatableIntegerValue opacity;
private final boolean isIdentity;
+ @Nullable private final AnimatableFloatValue skew;
+ @Nullable private final AnimatableFloatValue skewAngle;
// Used for repeaters
@Nullable private final AnimatableFloatValue startOpacity;
@@ -32,6 +34,8 @@
new AnimatableFloatValue(),
new AnimatableIntegerValue(),
new AnimatableFloatValue(),
+ new AnimatableFloatValue(),
+ new AnimatableFloatValue(),
new AnimatableFloatValue()
);
}
@@ -39,7 +43,8 @@
public AnimatableTransform(AnimatablePathValue anchorPoint,
AnimatableValue<PointF, PointF> position, AnimatableScaleValue scale,
AnimatableFloatValue rotation, AnimatableIntegerValue opacity,
- @Nullable AnimatableFloatValue startOpacity, @Nullable AnimatableFloatValue endOpacity) {
+ @Nullable AnimatableFloatValue startOpacity, @Nullable AnimatableFloatValue endOpacity,
+ @Nullable AnimatableFloatValue skew, @Nullable AnimatableFloatValue skewAngle) {
this.anchorPoint = anchorPoint;
this.position = position;
this.scale = scale;
@@ -47,12 +52,15 @@
this.opacity = opacity;
this.startOpacity = startOpacity;
this.endOpacity = endOpacity;
+ this.skew = skew;
+ this.skewAngle = skewAngle;
isIdentity = anchorPoint.isStatic() && anchorPoint.getKeyframes().get(0).startValue.equals(0f, 0f) &&
!(position instanceof AnimatableSplitDimensionPathValue) &&
position.isStatic() && position.getKeyframes().get(0).startValue.equals(0f, 0f) &&
scale.isStatic() && scale.getKeyframes().get(0).startValue.equals(1f, 1f) &&
- (rotation.isStatic() && rotation.getKeyframes().get(0).startValue == 0f ||
- rotation.keyframes.isEmpty());
+ (rotation.isStatic() && rotation.getKeyframes().get(0).startValue == 0f || rotation.keyframes.isEmpty()) &&
+ (skew == null || (skew.isStatic() && skew.getKeyframes().get(0).startValue == 0f)) &&
+ (skewAngle == null || (skewAngle.isStatic() && skewAngle.getKeyframes().get(0).startValue == 0f));
}
public AnimatablePathValue getAnchorPoint() {
@@ -83,6 +91,14 @@
return endOpacity;
}
+ @Nullable public AnimatableFloatValue getSkew() {
+ return skew;
+ }
+
+ @Nullable public AnimatableFloatValue getSkewAngle() {
+ return skewAngle;
+ }
+
public boolean isIdentity() {
return isIdentity;
}
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/AnimatableTransformParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/AnimatableTransformParser.java
index 7a6d750..0249dd4 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/AnimatableTransformParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/AnimatableTransformParser.java
@@ -31,6 +31,8 @@
AnimatableIntegerValue opacity = null;
AnimatableFloatValue startOpacity = null;
AnimatableFloatValue endOpacity = null;
+ AnimatableFloatValue skew = null;
+ AnimatableFloatValue skewAngle = null;
boolean isObject = reader.peek() == JsonToken.BEGIN_OBJECT;
if (isObject) {
@@ -83,6 +85,12 @@
case "eo":
endOpacity = AnimatableValueParser.parseFloat(reader, composition, false);
break;
+ case "sk":
+ skew = AnimatableValueParser.parseFloat(reader, composition, false);
+ break;
+ case "sa":
+ skewAngle = AnimatableValueParser.parseFloat(reader, composition, false);
+ break;
default:
reader.skipValue();
}
@@ -109,8 +117,7 @@
opacity = new AnimatableIntegerValue();
}
- return new AnimatableTransform(
- anchorPoint, position, scale, rotation, opacity, startOpacity, endOpacity);
+ return new AnimatableTransform(anchorPoint, position, scale, rotation, opacity, startOpacity, endOpacity, skew, skewAngle);
}
}