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