Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an API to always render images at the original size #1706

Merged
merged 11 commits into from Jan 16, 2022
Expand Up @@ -54,6 +54,12 @@ import kotlin.math.roundToInt
* features so it defaults to off. The only way to know if your animation will work
* well with merge paths or not is to try it. If your animation has merge paths and
* doesn't render correctly, please file an issue.
* @param renderMode Allows you to specify whether you want Lottie to use hardware or software rendering.
* Defaults to AUTOMATIC. Refer to [LottieAnimationView.setRenderMode] for more info.
* @param maintainOriginalImageBounds When true, dynamically set bitmaps will be drawn with the exact bounds of the original animation,
* regardless of the bitmap size.
* When false, dynamically set bitmaps will be drawn at the top left of the original image but with its own bounds.
* Defaults to false.
* @param dynamicProperties Allows you to change the properties of an animation dynamically. To use them, use
* [rememberLottieDynamicProperties]. Refer to its docs for more info.
* @param alignment Define where the animation should be placed within this composable if it has a different
Expand All @@ -69,6 +75,7 @@ fun LottieAnimation(
applyOpacityToLayers: Boolean = false,
enableMergePaths: Boolean = false,
renderMode: RenderMode = RenderMode.AUTOMATIC,
maintainOriginalImageBounds: Boolean = false,
dynamicProperties: LottieDynamicProperties? = null,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
Expand Down Expand Up @@ -106,6 +113,7 @@ fun LottieAnimation(
drawable.isApplyingOpacityToLayersEnabled = applyOpacityToLayers
drawable.enableMergePathsForKitKatAndAbove(enableMergePaths)
drawable.useSoftwareRendering(useSoftwareRendering)
drawable.maintainOriginalImageBounds = maintainOriginalImageBounds
drawable.progress = progress
drawable.setBounds(0, 0, composition.bounds.width(), composition.bounds.height())
drawable.draw(canvas.nativeCanvas, matrix)
Expand Down Expand Up @@ -133,6 +141,7 @@ fun LottieAnimation(
applyOpacityToLayers: Boolean = false,
enableMergePaths: Boolean = false,
renderMode: RenderMode = RenderMode.AUTOMATIC,
maintainOriginalImageBounds: Boolean = false,
dynamicProperties: LottieDynamicProperties? = null,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
Expand All @@ -153,6 +162,7 @@ fun LottieAnimation(
applyOpacityToLayers,
enableMergePaths,
renderMode,
maintainOriginalImageBounds,
dynamicProperties,
alignment,
contentScale,
Expand Down
Expand Up @@ -19,6 +19,7 @@ import com.airbnb.lottie.value.ScaleXY
* This takes a vararg of individual dynamic properties which should be created with [rememberLottieDynamicProperty].
*
* @see rememberLottieDynamicProperty
* @see LottieDrawable.setMaintainOriginalImageBounds
*/
@Composable
fun rememberLottieDynamicProperties(
Expand Down
20 changes: 20 additions & 0 deletions lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
Expand Up @@ -893,6 +893,26 @@ public String getImageAssetsFolder() {
return lottieDrawable.getImageAssetsFolder();
}

/**
* When true, dynamically set bitmaps will be drawn with the exact bounds of the original animation, regardless of the bitmap size.
* When false, dynamically set bitmaps will be drawn at the top left of the original image but with its own bounds.
*
* Defaults to false.
*/
public void setMaintainOriginalImageBounds(boolean maintainOriginalImageBounds) {
lottieDrawable.setMaintainOriginalImageBounds(maintainOriginalImageBounds);
}

/**
* When true, dynamically set bitmaps will be drawn with the exact bounds of the original animation, regardless of the bitmap size.
* When false, dynamically set bitmaps will be drawn at the top left of the original image but with its own bounds.
*
* Defaults to false.
*/
public boolean getMaintainOriginalImageBounds() {
return lottieDrawable.getMaintainOriginalImageBounds();
}

/**
* Allows you to modify or clear a bitmap that was loaded for an image either automatically
* through {@link #setImageAssetsFolder(String)} or with an {@link ImageAssetDelegate}.
Expand Down
65 changes: 65 additions & 0 deletions lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
Expand Up @@ -99,6 +99,7 @@ public void onAnimationUpdate(ValueAnimator animation) {
@Nullable
TextDelegate textDelegate;
private boolean enableMergePaths;
private boolean maintainOriginalImageBounds = false;
@Nullable
private CompositionLayer compositionLayer;
private int alpha = 255;
Expand Down Expand Up @@ -219,6 +220,26 @@ public String getImageAssetsFolder() {
return imageAssetsFolder;
}

/**
* When true, dynamically set bitmaps will be drawn with the exact bounds of the original animation, regardless of the bitmap size.
* When false, dynamically set bitmaps will be drawn at the top left of the original image but with its own bounds.
*
* Defaults to false.
*/
public void setMaintainOriginalImageBounds(boolean maintainOriginalImageBounds) {
this.maintainOriginalImageBounds = maintainOriginalImageBounds;
}

/**
* When true, dynamically set bitmaps will be drawn with the exact bounds of the original animation, regardless of the bitmap size.
* When false, dynamically set bitmaps will be drawn at the top left of the original image but with its own bounds.
*
* Defaults to false.
*/
public boolean getMaintainOriginalImageBounds() {
return maintainOriginalImageBounds;
}

/**
* Create a composition with {@link LottieCompositionFactory}
*
Expand Down Expand Up @@ -1045,7 +1066,11 @@ public Bitmap updateBitmap(String id, @Nullable Bitmap bitmap) {
return ret;
}

/**
* @deprecated use {@link #getBitmapForId(String)}.
*/
@Nullable
@Deprecated
public Bitmap getImageAsset(String id) {
ImageAssetManager bm = getImageAssetManager();
if (bm != null) {
Expand All @@ -1058,6 +1083,46 @@ public Bitmap getImageAsset(String id) {
return null;
}

/**
* Returns the bitmap that will be rendered for the given id in the Lottie animation file.
* The id is the asset reference id stored in the "id" property of each object in the "assets" array.
*
* The returned bitmap could be from:
* * Embedded in the animation file as a base64 string.
* * In the same directory as the animation file.
* * In the same zip file as the animation file.
* * Returned from an {@link ImageAssetDelegate}.
* or null if the image doesn't exist from any of those places.
*/
@Nullable
public Bitmap getBitmapForId(String id) {
ImageAssetManager assetManager = getImageAssetManager();
if (assetManager != null) {
return assetManager.bitmapForId(id);
}
return null;
}

/**
* Returns the {@link LottieImageAsset} that will be rendered for the given id in the Lottie animation file.
* The id is the asset reference id stored in the "id" property of each object in the "assets" array.
*
* The returned bitmap could be from:
* * Embedded in the animation file as a base64 string.
* * In the same directory as the animation file.
* * In the same zip file as the animation file.
* * Returned from an {@link ImageAssetDelegate}.
* or null if the image doesn't exist from any of those places.
*/
@Nullable
public LottieImageAsset getLottieImageAssetForId(String id) {
LottieComposition composition = this.composition;
if (composition == null) {
return null;
}
return composition.getImages().get(id);
}

private ImageAssetManager getImageAssetManager() {
if (getCallback() == null) {
// We can't get a bitmap since we can't get a Context from the callback.
Expand Down
5 changes: 4 additions & 1 deletion lottie/src/main/java/com/airbnb/lottie/LottieImageAsset.java
Expand Up @@ -6,7 +6,7 @@
import androidx.annotation.RestrictTo;

/**
* Data class describing an image asset exported by bodymovin.
* Data class describing an image asset embedded in a Lottie json file.
*/
public class LottieImageAsset {
private final int width;
Expand Down Expand Up @@ -36,6 +36,9 @@ public int getHeight() {
return height;
}

/**
* The reference id in the json file.
*/
public String getId() {
return id;
}
Expand Down
Expand Up @@ -62,6 +62,10 @@ public void setDelegate(@Nullable ImageAssetDelegate assetDelegate) {
return prevBitmap;
}

@Nullable public LottieImageAsset getImageAssetById(String id) {
return imageAssets.get(id);
}

@Nullable public Bitmap bitmapForId(String id) {
LottieImageAsset asset = imageAssets.get(id);
if (asset == null) {
Expand Down
31 changes: 24 additions & 7 deletions lottie/src/main/java/com/airbnb/lottie/model/layer/ImageLayer.java
Expand Up @@ -12,6 +12,7 @@
import androidx.annotation.Nullable;

import com.airbnb.lottie.LottieDrawable;
import com.airbnb.lottie.LottieImageAsset;
import com.airbnb.lottie.LottieProperty;
import com.airbnb.lottie.animation.LPaint;
import com.airbnb.lottie.animation.keyframe.BaseKeyframeAnimation;
Expand All @@ -24,16 +25,18 @@ public class ImageLayer extends BaseLayer {
private final Paint paint = new LPaint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
private final Rect src = new Rect();
private final Rect dst = new Rect();
@Nullable private final LottieImageAsset lottieImageAsset;
@Nullable private BaseKeyframeAnimation<ColorFilter, ColorFilter> colorFilterAnimation;
@Nullable private BaseKeyframeAnimation<Bitmap, Bitmap> imageAnimation;

ImageLayer(LottieDrawable lottieDrawable, Layer layerModel) {
super(lottieDrawable, layerModel);
lottieImageAsset = lottieDrawable.getLottieImageAssetForId(layerModel.getRefId());
}

@Override public void drawLayer(@NonNull Canvas canvas, Matrix parentMatrix, int parentAlpha) {
Bitmap bitmap = getBitmap();
if (bitmap == null || bitmap.isRecycled()) {
if (bitmap == null || bitmap.isRecycled() || lottieImageAsset == null) {
return;
}
float density = Utils.dpScale();
Expand All @@ -45,16 +48,21 @@ public class ImageLayer extends BaseLayer {
canvas.save();
canvas.concat(parentMatrix);
src.set(0, 0, bitmap.getWidth(), bitmap.getHeight());
dst.set(0, 0, (int) (bitmap.getWidth() * density), (int) (bitmap.getHeight() * density));
if (lottieDrawable.getMaintainOriginalImageBounds()) {
dst.set(0, 0, (int) (lottieImageAsset.getWidth() * density), (int) (lottieImageAsset.getHeight() * density));
} else {
dst.set(0, 0, (int) (bitmap.getWidth() * density), (int) (bitmap.getHeight() * density));
}

canvas.drawBitmap(bitmap, src, dst, paint);
canvas.restore();
}

@Override public void getBounds(RectF outBounds, Matrix parentMatrix, boolean applyParents) {
super.getBounds(outBounds, parentMatrix, applyParents);
Bitmap bitmap = getBitmap();
if (bitmap != null) {
outBounds.set(0, 0, bitmap.getWidth() * Utils.dpScale(), bitmap.getHeight() * Utils.dpScale());
if (lottieImageAsset != null) {
float scale = Utils.dpScale();
outBounds.set(0, 0, lottieImageAsset.getWidth() * scale, lottieImageAsset.getHeight() * scale);
boundsMatrix.mapRect(outBounds);
}
}
Expand All @@ -63,11 +71,20 @@ public class ImageLayer extends BaseLayer {
private Bitmap getBitmap() {
if (imageAnimation != null) {
Bitmap callbackBitmap = imageAnimation.getValue();
if (callbackBitmap != null)
if (callbackBitmap != null) {
return callbackBitmap;
}
}
String refId = layerModel.getRefId();
return lottieDrawable.getImageAsset(refId);
Bitmap bitmapFromDrawable = lottieDrawable.getBitmapForId(refId);
if (bitmapFromDrawable != null) {
return bitmapFromDrawable;
}
LottieImageAsset asset = this.lottieImageAsset;
if (asset != null) {
return asset.getBitmap();
}
return null;
}

@SuppressWarnings("SingleStatementInBlock")
Expand Down
Expand Up @@ -9,14 +9,14 @@ import android.graphics.PorterDuff
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.platform.ComposeView
import androidx.core.view.doOnAttach
import androidx.core.view.doOnLayout
import androidx.core.view.doOnPreDraw
import com.airbnb.lottie.FontAssetDelegate
import com.airbnb.lottie.LottieAnimationView
import com.airbnb.lottie.LottieComposition
Expand All @@ -27,14 +27,11 @@ import com.airbnb.lottie.snapshots.utils.BitmapPool
import com.airbnb.lottie.snapshots.utils.HappoSnapshotter
import com.airbnb.lottie.snapshots.utils.ObjectPool
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.android.awaitFrame
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.withContext
import kotlin.coroutines.resume

private val ActivityContentLock = Mutex()

/**
* Set of properties that are available to all [SnapshotTestCase] runs.
*/
Expand Down Expand Up @@ -153,6 +150,12 @@ suspend fun SnapshotTestCaseContext.snapshotComposition(
bitmapPool.release(bitmap)
}

/**
* Use this to signal that the composition is not ready to be snapshot yet.
* This use useful if you are using things like `rememberLottieComposition` which parses a composition asynchronously.
*/
val LocalSnapshotReady = compositionLocalOf { MutableStateFlow<Boolean?>(true) }

suspend fun SnapshotTestCaseContext.snapshotComposable(
name: String,
variant: String = "default",
Expand All @@ -162,18 +165,33 @@ suspend fun SnapshotTestCaseContext.snapshotComposable(
val composeView = ComposeView(context)
composeView.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)
val bitmap = withContext(Dispatchers.Main) {
composeView.setContent(content)
val readyFlow = MutableStateFlow<Boolean?>(null)
composeView.setContent {
CompositionLocalProvider(LocalSnapshotReady provides readyFlow) {
content()
}
if (readyFlow.value == null) readyFlow.value = true
}
lateinit var onDrawListener: ViewTreeObserver.OnDrawListener
suspendCancellableCoroutine<Bitmap> { cont ->
composeView.doOnLayout {
log("Drawing $name")
val bitmap = bitmapPool.acquire(composeView.width, composeView.height)
val canvas = Canvas(bitmap)
composeView.draw(canvas)
cont.resume(bitmap)
onDrawListener = ViewTreeObserver.OnDrawListener {
composeView.post {
if (readyFlow.value != true) {
return@post
}
log("Drawing $name")
val bitmap = bitmapPool.acquire(composeView.width, composeView.height)
val canvas = Canvas(bitmap)
composeView.draw(canvas)
cont.resume(bitmap)
}
}
composeView.viewTreeObserver.addOnDrawListener(onDrawListener)
onActivity { activity ->
activity.binding.content.addView(composeView)
}
}.also {
composeView.viewTreeObserver.removeOnDrawListener(onDrawListener)
}
}
onActivity { activity ->
Expand Down