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);
   }
 
 }