Skip to content

Commit

Permalink
Replace LottieAnimationView layer types with internal bitmap rendering (
Browse files Browse the repository at this point in the history
#1952)

This change marks a significant change in the underlying rendering pipeline for Lottie.

Previously, Lottie would always set a layerType on LottieAnimationView. For hardware accelerated animations, this meant that a separate graphics layer was allocated for the animation. The disadvantage with this method is that the texture has to be uploaded separately to the GPU on each frame.

For software rendering, Lottie would depend on Android's view caching mechanism in which View allocates a drawing cache bitmap which it then draws to a canvas. This has the disadvantage of the Bitmap always being the size of the containing View even if it's larger than the drawable.
It also abstracts away the Bitmap so that further optimizations. When software rendering is enabled and it has to be redrawn without getting invalidated then it can skip re-rendering and simply redraw the cached bitmap.
It also upstream the bitmap rendering form LottieAnimationView to LottieDrawable which allows it to be exposed to lottie-compose which is important for the same reasons it is important for the base library and wasn't possible before.

While working on this PR, I tried rendering and then drawing only the bounds of the animation. However, I found that calculating the bounds for the entire animation was slower than drawing the entire bitmap (which is very fast).

Fixes #1387
  • Loading branch information
gpeal authored Dec 10, 2021
1 parent 6d8fd33 commit 3b5387a
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 232 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.airbnb.lottie.compose

import android.graphics.Matrix
import android.os.Build
import androidx.annotation.FloatRange
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box
Expand All @@ -21,6 +22,7 @@ import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import com.airbnb.lottie.LottieComposition
import com.airbnb.lottie.LottieDrawable
import com.airbnb.lottie.RenderMode
import com.airbnb.lottie.utils.Utils
import kotlin.math.roundToInt

Expand Down Expand Up @@ -66,17 +68,20 @@ fun LottieAnimation(
outlineMasksAndMattes: Boolean = false,
applyOpacityToLayers: Boolean = false,
enableMergePaths: Boolean = false,
renderMode: RenderMode = RenderMode.AUTOMATIC,
dynamicProperties: LottieDynamicProperties? = null,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
) {
val drawable = remember { LottieDrawable() }
val matrix = remember { Matrix() }
var setDynamicProperties: LottieDynamicProperties? by remember { mutableStateOf(null) }
val useSoftwareRendering: Boolean = remember(renderMode, composition) {
renderMode.useSoftwareRendering(Build.VERSION.SDK_INT, composition?.hasDashPattern() ?: false, composition?.maskAndMatteCount ?: 0)
}

if (composition == null || composition.duration == 0f) return Box(modifier)


Canvas(
modifier = modifier
.size((composition.bounds.width() / Utils.dpScale()).dp, (composition.bounds.height() / Utils.dpScale()).dp)
Expand All @@ -101,6 +106,7 @@ fun LottieAnimation(
drawable.setOutlineMasksAndMattes(outlineMasksAndMattes)
drawable.isApplyingOpacityToLayersEnabled = applyOpacityToLayers
drawable.enableMergePathsForKitKatAndAbove(enableMergePaths)
drawable.useSoftwareRendering(useSoftwareRendering)
drawable.progress = progress
drawable.draw(canvas.nativeCanvas, matrix)
}
Expand All @@ -126,6 +132,7 @@ fun LottieAnimation(
outlineMasksAndMattes: Boolean = false,
applyOpacityToLayers: Boolean = false,
enableMergePaths: Boolean = false,
renderMode: RenderMode = RenderMode.AUTOMATIC,
dynamicProperties: LottieDynamicProperties? = null,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
Expand All @@ -145,6 +152,7 @@ fun LottieAnimation(
outlineMasksAndMattes,
applyOpacityToLayers,
enableMergePaths,
renderMode,
dynamicProperties,
alignment,
contentScale,
Expand Down
85 changes: 36 additions & 49 deletions lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
package com.airbnb.lottie;

import static com.airbnb.lottie.RenderMode.HARDWARE;

import android.animation.Animator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.drawable.Drawable;
import android.os.Build;
Expand Down Expand Up @@ -116,6 +113,7 @@ public void onResult(Throwable result) {
private boolean autoPlay = false;
private boolean cacheComposition = true;
private RenderMode renderMode = RenderMode.AUTOMATIC;
private boolean useSoftwareRendering = false;
private final Set<LottieOnCompositionLoadedListener> lottieOnCompositionLoadedListeners = new HashSet<>();
/**
* Prevents a StackOverflowException on 4.4 in which getDrawingCache() calls buildDrawingCache().
Expand Down Expand Up @@ -232,7 +230,7 @@ private void init(@Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {

lottieDrawable.setSystemAnimationsAreEnabled(Utils.getAnimationScale(getContext()) != 0f);

enableOrDisableHardwareLayer();
computeRenderMode();
isInitialized = true;
}

Expand Down Expand Up @@ -585,7 +583,7 @@ public void setComposition(@NonNull LottieComposition composition) {
ignoreUnschedule = true;
boolean isNewComposition = lottieDrawable.setComposition(composition);
ignoreUnschedule = false;
enableOrDisableHardwareLayer();
computeRenderMode();
if (getDrawable() == lottieDrawable && !isNewComposition) {
// We can avoid re-setting the drawable, and invalidating the view, since the composition
// hasn't changed.
Expand Down Expand Up @@ -634,7 +632,7 @@ public boolean hasMatte() {
public void playAnimation() {
if (isShown()) {
lottieDrawable.playAnimation();
enableOrDisableHardwareLayer();
computeRenderMode();
} else {
playAnimationWhenShown = true;
}
Expand All @@ -648,7 +646,7 @@ public void playAnimation() {
public void resumeAnimation() {
if (isShown()) {
lottieDrawable.resumeAnimation();
enableOrDisableHardwareLayer();
computeRenderMode();
} else {
playAnimationWhenShown = false;
wasAnimatingWhenNotShown = true;
Expand Down Expand Up @@ -1001,7 +999,7 @@ public void cancelAnimation() {
wasAnimatingWhenNotShown = false;
playAnimationWhenShown = false;
lottieDrawable.cancelAnimation();
enableOrDisableHardwareLayer();
computeRenderMode();
}

@MainThread
Expand All @@ -1011,7 +1009,7 @@ public void pauseAnimation() {
wasAnimatingWhenNotShown = false;
playAnimationWhenShown = false;
lottieDrawable.pauseAnimation();
enableOrDisableHardwareLayer();
computeRenderMode();
}

/**
Expand Down Expand Up @@ -1085,7 +1083,7 @@ public void buildDrawingCache(boolean autoScale) {
super.buildDrawingCache(autoScale);
if (buildDrawingCacheDepth == 1 && getWidth() > 0 && getHeight() > 0 &&
getLayerType() == LAYER_TYPE_SOFTWARE && getDrawingCache(autoScale) == null) {
setRenderMode(HARDWARE);
setRenderMode(RenderMode.HARDWARE);
}
buildDrawingCacheDepth--;
L.endSection("buildDrawingCache");
Expand All @@ -1097,17 +1095,38 @@ public void buildDrawingCache(boolean autoScale) {
* 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
* there are many large mattes and masks in which case there is a lot
* of GPU uploadTexture thrashing which makes it much slower.
* <p>
* 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
* However, if you have multiple mattes and masks (especially large ones), you
* should test both render modes. You should also test on pre-Pie and Pie+ devices
* because the underlying rendering enginge changed significantly.
* because the underlying rendering engine changed significantly.
*
* @see LottieDrawable#useSoftwareRendering(boolean)
* @see <a href="https://developer.android.com/guide/topics/graphics/hardware-accel#unsupported">Android Hardware Acceleration</a>
*/
public void setRenderMode(RenderMode renderMode) {
this.renderMode = renderMode;
enableOrDisableHardwareLayer();
computeRenderMode();
}

/**
* Returns the actual render mode being used. It will always be {@link RenderMode#HARDWARE} or {@link RenderMode#SOFTWARE}.
* When the render mode is set to AUTOMATIC, the value will be derived from {@link RenderMode#useSoftwareRendering(int, boolean, int)}.
*/
public RenderMode getRenderMode() {
return useSoftwareRendering ? RenderMode.SOFTWARE : RenderMode.HARDWARE;
}

private void computeRenderMode() {
LottieComposition composition = this.composition;
if (composition == null) {
return;
}
useSoftwareRendering = renderMode.useSoftwareRendering(
Build.VERSION.SDK_INT, composition.hasDashPattern(), composition.getMaskAndMatteCount());
lottieDrawable.useSoftwareRendering(useSoftwareRendering);
}

/**
Expand All @@ -1127,46 +1146,13 @@ public void setApplyingOpacityToLayersEnabled(boolean isApplyingOpacityToLayersE
}

/**
* 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).
* This API no longer has any effect.
*/
@Deprecated
public void disableExtraScaleModeInFitXY() {
lottieDrawable.disableExtraScaleModeInFitXY();
}

private void enableOrDisableHardwareLayer() {
int layerType = LAYER_TYPE_SOFTWARE;
switch (renderMode) {
case HARDWARE:
layerType = LAYER_TYPE_HARDWARE;
break;
case SOFTWARE:
layerType = LAYER_TYPE_SOFTWARE;
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;
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
useHardwareLayer = false;
} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.N || Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1) {
useHardwareLayer = false;
}
layerType = useHardwareLayer ? LAYER_TYPE_HARDWARE : LAYER_TYPE_SOFTWARE;
break;
}
if (layerType != getLayerType()) {
setLayerType(layerType, null);
}
}

public boolean addLottieOnCompositionLoadedListener(@NonNull LottieOnCompositionLoadedListener lottieOnCompositionLoadedListener) {
LottieComposition composition = this.composition;
if (composition != null) {
Expand All @@ -1189,6 +1175,7 @@ private void setLottieDrawable() {
// if the composition changes.
setImageDrawable(null);
setImageDrawable(lottieDrawable);
computeRenderMode();
if (wasAnimating) {
// This is necessary because lottieDrawable will get unscheduled and canceled when the drawable is set to null.
lottieDrawable.resumeAnimation();
Expand Down
Loading

0 comments on commit 3b5387a

Please sign in to comment.