New RenderMode API (#1072)

While testing, I discovered that animations with lots of/large masks and mattes perform significantly worse with hardware acceleration than software acceleration on pre-Pie devices because of RenderNode#textureUpload. It is really hard to detect this dynamically so I'm leaving the default on automatic (with an additional heuristic for >4 mattes and masks) but having an option to set it to hardware/software manually.

#381
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PlayerFragment.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PlayerFragment.kt
index df218ee..068561b 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PlayerFragment.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PlayerFragment.kt
@@ -19,6 +19,7 @@
 import com.airbnb.lottie.L
 import com.airbnb.lottie.LottieAnimationView
 import com.airbnb.lottie.LottieComposition
+import com.airbnb.lottie.RenderMode
 import com.airbnb.lottie.model.KeyPath
 import com.airbnb.lottie.samples.model.CompositionArgs
 import com.airbnb.lottie.samples.views.BackgroundColorView
@@ -148,6 +149,16 @@
             animationView.setBackgroundResource(if (it) R.drawable.outline else 0)
         }
 
+        hardwareAccelerationToggle.setOnClickListener {
+            val renderMode = if (animationView.layerType == View.LAYER_TYPE_HARDWARE) {
+                RenderMode.Software
+            } else {
+                RenderMode.Hardware
+            }
+            animationView.setRenderMode(renderMode)
+            hardwareAccelerationToggle.isActivated = animationView.layerType == View.LAYER_TYPE_HARDWARE
+        }
+
         viewModel.selectSubscribe(PlayerState::controlsVisible) { controlsContainer.animateVisible(it) }
 
         viewModel.selectSubscribe(PlayerState::controlBarVisible) { controlBar.animateVisible(it) }
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
index 8035373..be8c037 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
@@ -73,6 +73,7 @@
   private @RawRes int animationResId;
   private boolean wasAnimatingWhenDetached = false;
   private boolean autoPlay = false;
+  private RenderMode renderMode = RenderMode.Automatic;
   private Set<LottieOnCompositionLoadedListener> lottieOnCompositionLoadedListeners = new HashSet<>();
 
   @Nullable private LottieTask<LottieComposition> compositionTask;
@@ -744,12 +745,44 @@
     lottieDrawable.clearComposition();
   }
 
+  /**
+   * Call this to set whether or not to render with hardware or software acceleration.
+   * Lottie defaults to Automatic which will use hardware acceleration unless:
+   * 1) There are dash paths and the device is pre-Pie.
+   * 2) There are more than 4 masks and mattes and the device is pre-Pie.
+   *    Hardware acceleration is generally faster for those devices unless
+   *    there are many large mattes and masks in which case there is a ton
+   *    of GPU uploadTexture thrashing which makes it much slower.
+   *
+   * In most cases, hardware rendering will be faster, even if you have mattes and masks.
+   * However, if you have multiple mattes and masks (especially large ones) then you
+   * should test both render modes. You should also test on pre-Pie and Pie+ devices
+   * because the underlying rendering enginge changed significantly.
+   */
+  public void setRenderMode(RenderMode renderMode) {
+    this.renderMode = renderMode;
+    enableOrDisableHardwareLayer();
+  }
+
   private void enableOrDisableHardwareLayer() {
-    boolean useHardwareLayer = true;
-    if (composition != null && composition.hasDashPattern() && Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
-      useHardwareLayer = false;
+    switch (renderMode) {
+      case Hardware:
+        setLayerType(LAYER_TYPE_HARDWARE, null);
+        break;
+      case Software:
+        setLayerType(LAYER_TYPE_SOFTWARE, null);
+        break;
+      case Automatic:
+        boolean useHardwareLayer = true;
+        if (composition != null && composition.hasDashPattern() && Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
+          useHardwareLayer = false;
+        } else if (composition != null && composition.getMaskAndMatteCount() > 4) {
+          useHardwareLayer = false;
+        }
+        setLayerType(useHardwareLayer ? LAYER_TYPE_HARDWARE : LAYER_TYPE_SOFTWARE, null);
+        break;
     }
-    setLayerType(useHardwareLayer ? LAYER_TYPE_HARDWARE : LAYER_TYPE_SOFTWARE, null);
+
   }
 
   public boolean addLottieOnCompositionLoadedListener(@NonNull LottieOnCompositionLoadedListener lottieOnCompositionLoadedListener) {
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieComposition.java b/lottie/src/main/java/com/airbnb/lottie/LottieComposition.java
index 1314c01..2497a58 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieComposition.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieComposition.java
@@ -55,6 +55,12 @@
    * Used to determine if an animation can be drawn with hardware acceleration.
    */
   private boolean hasDashPattern;
+  /**
+   * Counts the number of mattes and masks. Before Android switched to SKIA
+   * for drawing in Oreo (API 28), using hardware acceleration with mattes and masks
+   * was only faster until you had ~4 masks after which it would actually become slower.
+   */
+  private int maskAndMatteCount = 0;
 
   @RestrictTo(RestrictTo.Scope.LIBRARY)
   public void init(Rect bounds, float startFrame, float endFrame, float frameRate,
@@ -84,13 +90,27 @@
     this.hasDashPattern = hasDashPattern;
   }
 
+  @RestrictTo(RestrictTo.Scope.LIBRARY)
+  public void incrementMatteOrMaskCount(int amount) {
+    maskAndMatteCount += amount;
+  }
+
   /**
    * Used to determine if an animation can be drawn with hardware acceleration.
    */
+  @RestrictTo(RestrictTo.Scope.LIBRARY)
   public boolean hasDashPattern() {
     return hasDashPattern;
   }
 
+  /**
+   * Used to determine if an animation can be drawn with hardware acceleration.
+   */
+  @RestrictTo(RestrictTo.Scope.LIBRARY)
+  public int getMaskAndMatteCount() {
+    return maskAndMatteCount;
+  }
+
   public ArrayList<String> getWarnings() {
     return new ArrayList<>(Arrays.asList(warnings.toArray(new String[warnings.size()])));
   }
diff --git a/lottie/src/main/java/com/airbnb/lottie/RenderMode.java b/lottie/src/main/java/com/airbnb/lottie/RenderMode.java
new file mode 100644
index 0000000..98859a0
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/RenderMode.java
@@ -0,0 +1,13 @@
+package com.airbnb.lottie;
+
+/**
+ * Controls how Lottie should render.
+ * Defaults to {@link RenderMode#Automatic}.
+ *
+ * @see LottieAnimationView#setRenderMode(RenderMode) for more information.
+ */
+public enum RenderMode {
+    Automatic,
+    Hardware,
+    Software
+}
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/LayerParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/LayerParser.java
index f5be26e..89082bf 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/LayerParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/LayerParser.java
@@ -100,12 +100,14 @@
           break;
         case "tt":
           matteType = Layer.MatteType.values()[reader.nextInt()];
+          composition.incrementMatteOrMaskCount(1);
           break;
         case "masksProperties":
           reader.beginArray();
           while (reader.hasNext()) {
             masks.add(MaskParser.parse(reader, composition));
           }
+          composition.incrementMatteOrMaskCount(masks.size());
           reader.endArray();
           break;
         case "shapes":
diff --git a/lottie/src/main/res/values/attrs.xml b/lottie/src/main/res/values/attrs.xml
index 70fc991..3f54388 100644
--- a/lottie/src/main/res/values/attrs.xml
+++ b/lottie/src/main/res/values/attrs.xml
@@ -17,5 +17,11 @@
         <attr name="lottie_colorFilter" format="color" />
         <attr name="lottie_scale" format="float" />
         <attr name="lottie_speed" format="float" />
+        <!-- These values must be kept in sync with the RenderMode enum -->
+        <attr name="lottie_renderMode" format="enum">
+            <enum name="automatic" value="0" />
+            <enum name="hardware" value="1" />
+            <enum name="software" value="2" />
+        </attr>
     </declare-styleable>
 </resources>
\ No newline at end of file