Support ScaleType.FIT_XY. (#1418)

Fixes #801 
Fixes #1384 
diff --git a/LottieSample/src/androidTest/java/com/airbnb/lottie/samples/LottieTest.kt b/LottieSample/src/androidTest/java/com/airbnb/lottie/samples/LottieTest.kt
index d01b40c..a4ac57e 100644
--- a/LottieSample/src/androidTest/java/com/airbnb/lottie/samples/LottieTest.kt
+++ b/LottieSample/src/androidTest/java/com/airbnb/lottie/samples/LottieTest.kt
@@ -342,6 +342,26 @@
             animationView.scaleType = ImageView.ScaleType.CENTER_INSIDE
         }
 
+        withAnimationView("LottieLogo1.json", "Scale Types", "300x300 fitXY") { animationView ->
+            animationView.progress = 1f
+            animationView.updateLayoutParams {
+                width = 300.dp.toInt()
+                height = 300.dp.toInt()
+            }
+            animationView.scaleType = ImageView.ScaleType.FIT_XY
+        }
+
+        withAnimationView("LottieLogo1.json", "Scale Types", "300x300 fitXY DisableExtraScale") {
+            animationView ->
+            animationView.progress = 1f
+            animationView.updateLayoutParams {
+                width = 300.dp.toInt()
+                height = 300.dp.toInt()
+            }
+            animationView.disableExtraScaleModeInFitXY()
+            animationView.scaleType = ImageView.ScaleType.FIT_XY
+        }
+
         withAnimationView("LottieLogo1.json", "Scale Types", "300x300 centerInside @2x") { animationView ->
             animationView.progress = 1f
             animationView.updateLayoutParams {
@@ -371,6 +391,25 @@
             animationView.scaleType = ImageView.ScaleType.CENTER_INSIDE
         }
 
+        withAnimationView("LottieLogo1.json", "Scale Types", "600x300 fitXY") { animationView ->
+            animationView.progress = 1f
+            animationView.updateLayoutParams {
+                width = 600.dp.toInt()
+                height = 300.dp.toInt()
+            }
+            animationView.scaleType = ImageView.ScaleType.FIT_XY
+        }
+
+        withAnimationView("LottieLogo1.json", "Scale Types", "600x300 fitXY DisableExtraScale") { animationView ->
+            animationView.progress = 1f
+            animationView.updateLayoutParams {
+                width = 600.dp.toInt()
+                height = 300.dp.toInt()
+            }
+            animationView.disableExtraScaleModeInFitXY()
+            animationView.scaleType = ImageView.ScaleType.FIT_XY
+        }
+
         withAnimationView("LottieLogo1.json", "Scale Types", "300x600 centerInside") { animationView ->
             animationView.progress = 1f
             animationView.updateLayoutParams {
@@ -379,6 +418,25 @@
             }
             animationView.scaleType = ImageView.ScaleType.CENTER_INSIDE
         }
+
+        withAnimationView("LottieLogo1.json", "Scale Types", "300x600 fitXY") { animationView ->
+            animationView.progress = 1f
+            animationView.updateLayoutParams {
+                width = 300.dp.toInt()
+                height = 600.dp.toInt()
+            }
+            animationView.scaleType = ImageView.ScaleType.FIT_XY
+        }
+
+        withAnimationView("LottieLogo1.json", "Scale Types", "300x600 fitXY DisableExtraScale") { animationView ->
+            animationView.progress = 1f
+            animationView.updateLayoutParams {
+                width = 300.dp.toInt()
+                height = 600.dp.toInt()
+            }
+            animationView.disableExtraScaleModeInFitXY()
+            animationView.scaleType = ImageView.ScaleType.FIT_XY
+        }
     }
 
     private suspend fun testDynamicProperties() {
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
index 773a93e..a8113d5 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
@@ -842,6 +842,11 @@
     return lottieDrawable.getScale();
   }
 
+  @Override public void setScaleType(ScaleType scaleType) {
+    super.setScaleType(scaleType);
+    lottieDrawable.setScaleType(scaleType);
+  }
+
   @MainThread
   public void cancelAnimation() {
     wasAnimatingWhenNotShown = false;
@@ -955,6 +960,21 @@
     lottieDrawable.setApplyingOpacityToLayersEnabled(isApplyingOpacityToLayersEnabled);
   }
 
+  /**
+   * Disable the extraScale mode in {@link #draw(Canvas)} function when scaleType is FitXY. It doesn't affect the rendering with other scaleTypes.
+   *
+   * <p>When there are 2 animation layout side by side, the default extra scale mode might leave 1 pixel not drawn between 2 animation, and
+   * disabling the extraScale mode can fix this problem</p>
+   *
+   * <b>Attention:</b> Disable the extra scale mode can downgrade the performance and may lead to larger memory footprint. Please only disable this
+   * mode when using animation with a reasonable dimension (smaller than screen size).
+   *
+   * @see LottieDrawable#drawWithNewAspectRatio(Canvas)
+   */
+  public void disableExtraScaleModeInFitXY() {
+    lottieDrawable.disableExtraScaleModeInFitXY();
+  }
+
   private void enableOrDisableHardwareLayer() {
     int layerType = LAYER_TYPE_SOFTWARE;
     switch (renderMode) {
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java b/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
index 17a21ce..a1ed28d 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
@@ -8,11 +8,13 @@
 import android.graphics.ColorFilter;
 import android.graphics.Matrix;
 import android.graphics.PixelFormat;
+import android.graphics.Rect;
 import android.graphics.Typeface;
 import android.graphics.drawable.Animatable;
 import android.graphics.drawable.Drawable;
 import android.os.Build;
 import android.view.View;
+import android.widget.ImageView;
 
 import androidx.annotation.FloatRange;
 import androidx.annotation.IntDef;
@@ -73,6 +75,8 @@
     }
   };
   @Nullable
+  private ImageView.ScaleType scaleType;
+  @Nullable
   private ImageAssetManager imageAssetManager;
   @Nullable
   private String imageAssetsFolder;
@@ -90,6 +94,7 @@
   private int alpha = 255;
   private boolean performanceTrackingEnabled;
   private boolean isApplyingOpacityToLayersEnabled;
+  private boolean isExtraScaleEnabled = true;
   /**
    * True if the drawable has not been drawn since the last invalidateSelf.
    * We can do this to prevent things like bounds from getting recalculated
@@ -260,6 +265,21 @@
     this.isApplyingOpacityToLayersEnabled = isApplyingOpacityToLayersEnabled;
   }
 
+  /**
+   * Disable the extraScale mode in {@link #draw(Canvas)} function when scaleType is FitXY. It doesn't affect the rendering with other scaleTypes.
+   *
+   * <p>When there are 2 animation layout side by side, the default extra scale mode might leave 1 pixel not drawn between 2 animation, and
+   * disabling the extraScale mode can fix this problem</p>
+   *
+   * <b>Attention:</b> Disable the extra scale mode can downgrade the performance and may lead to larger memory footprint. Please only disable this
+   * mode when using animation with a reasonable dimension (smaller than screen size).
+   *
+   * @see #drawWithNewAspectRatio(Canvas)
+   */
+  public void disableExtraScaleModeInFitXY() {
+    this.isExtraScaleEnabled = false;
+  }
+
   public boolean isApplyingOpacityToLayersEnabled() {
     return isApplyingOpacityToLayersEnabled;
   }
@@ -316,50 +336,16 @@
   @Override
   public void draw(@NonNull Canvas canvas) {
     isDirty = false;
+
     L.beginSection("Drawable#draw");
-    if (compositionLayer == null) {
-      return;
+
+    if (ImageView.ScaleType.FIT_XY == scaleType) {
+      drawWithNewAspectRatio(canvas);
+    } else {
+      drawWithOriginalAspectRatio(canvas);
     }
 
-    float scale = this.scale;
-    float extraScale = 1f;
-    float maxScale = getMaxScale(canvas);
-    if (scale > maxScale) {
-      scale = maxScale;
-      extraScale = this.scale / scale;
-    }
-
-    int saveCount = -1;
-    if (extraScale > 1) {
-      // This is a bit tricky...
-      // We can't draw on a canvas larger than ViewConfiguration.get(context).getScaledMaximumDrawingCacheSize()
-      // which works out to be roughly the size of the screen because Android can't generate a
-      // bitmap large enough to render to.
-      // As a result, we cap the scale such that it will never be wider/taller than the screen
-      // and then only render in the top left corner of the canvas. We then use extraScale
-      // to scale up the rest of the scale. However, since we rendered the animation to the top
-      // left corner, we need to scale up and translate the canvas to zoom in on the top left
-      // corner.
-      saveCount = canvas.save();
-      float halfWidth = composition.getBounds().width() / 2f;
-      float halfHeight = composition.getBounds().height() / 2f;
-      float scaledHalfWidth = halfWidth * scale;
-      float scaledHalfHeight = halfHeight * scale;
-
-      canvas.translate(
-          getScale() * halfWidth - scaledHalfWidth,
-          getScale() * halfHeight - scaledHalfHeight);
-      canvas.scale(extraScale, extraScale, scaledHalfWidth, scaledHalfHeight);
-    }
-
-    matrix.reset();
-    matrix.preScale(scale, scale);
-    compositionLayer.draw(canvas, matrix, alpha);
     L.endSection("Drawable#draw");
-
-    if (saveCount > 0) {
-      canvas.restoreToCount(saveCount);
-    }
   }
 
 // <editor-fold desc="animator">
@@ -1066,6 +1052,10 @@
     callback.unscheduleDrawable(this, what);
   }
 
+  void setScaleType(ImageView.ScaleType scaleType) {
+    this.scaleType = scaleType;
+  }
+
   /**
    * If the composition is larger than the canvas, we have to use a different method to scale it up.
    * See the comments in {@link #draw(Canvas)} for more info.
@@ -1076,6 +1066,94 @@
     return Math.min(maxScaleX, maxScaleY);
   }
 
+  private void drawWithNewAspectRatio(Canvas canvas) {
+    if (compositionLayer == null) {
+      return;
+    }
+
+    int saveCount = -1;
+    Rect bounds = getBounds();
+    // In fitXY mode, the scale doesn't take effect.
+    float scaleX = bounds.width() / (float) composition.getBounds().width();
+    float scaleY = bounds.height() / (float) composition.getBounds().height();
+
+    if (isExtraScaleEnabled) {
+      float maxScale = Math.min(scaleX, scaleY);
+      float extraScale = 1f;
+      if (maxScale < 1f) {
+        extraScale = extraScale / maxScale;
+        scaleX = scaleX / extraScale;
+        scaleY = scaleY / extraScale;
+      }
+
+      if (extraScale > 1) {
+        saveCount = canvas.save();
+        float halfWidth = bounds.width() / 2f;
+        float halfHeight = bounds.height() / 2f;
+        float scaledHalfWidth = halfWidth * maxScale;
+        float scaledHalfHeight = halfHeight * maxScale;
+
+        canvas.translate(
+            halfWidth - scaledHalfWidth,
+            halfHeight - scaledHalfHeight);
+        canvas.scale(extraScale, extraScale, scaledHalfWidth, scaledHalfHeight);
+      }
+    }
+
+    matrix.reset();
+    matrix.preScale(scaleX, scaleY);
+    compositionLayer.draw(canvas, matrix, alpha);
+
+    if (saveCount > 0) {
+      canvas.restoreToCount(saveCount);
+    }
+  }
+
+  private void drawWithOriginalAspectRatio(Canvas canvas) {
+    if (compositionLayer == null) {
+      return;
+    }
+
+    float scale = this.scale;
+    float extraScale = 1f;
+    float maxScale = getMaxScale(canvas);
+    if (scale > maxScale) {
+      scale = maxScale;
+      extraScale = this.scale / scale;
+    }
+
+    int saveCount = -1;
+    if (extraScale > 1) {
+      // This is a bit tricky...
+      // We can't draw on a canvas larger than ViewConfiguration.get(context).getScaledMaximumDrawingCacheSize()
+      // which works out to be roughly the size of the screen because Android can't generate a
+      // bitmap large enough to render to.
+      // As a result, we cap the scale such that it will never be wider/taller than the screen
+      // and then only render in the top left corner of the canvas. We then use extraScale
+      // to scale up the rest of the scale. However, since we rendered the animation to the top
+      // left corner, we need to scale up and translate the canvas to zoom in on the top left
+      // corner.
+      saveCount = canvas.save();
+      float halfWidth = composition.getBounds().width() / 2f;
+      float halfHeight = composition.getBounds().height() / 2f;
+      float scaledHalfWidth = halfWidth * scale;
+      float scaledHalfHeight = halfHeight * scale;
+
+      canvas.translate(
+          getScale() * halfWidth - scaledHalfWidth,
+          getScale() * halfHeight - scaledHalfHeight);
+      canvas.scale(extraScale, extraScale, scaledHalfWidth, scaledHalfHeight);
+    }
+
+    matrix.reset();
+    matrix.preScale(scale, scale);
+    compositionLayer.draw(canvas, matrix, alpha);
+
+    if (saveCount > 0) {
+      canvas.restoreToCount(saveCount);
+    }
+  }
+
   private static class ColorFilterData {
 
     final String layerName;