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

Constrain unbounded dimensions when exactly one dimension is unbounded #2437

Merged
merged 9 commits into from
Dec 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import android.graphics.Typeface
import androidx.annotation.FloatRange
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
Expand All @@ -19,12 +18,10 @@ import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.ScaleFactor
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import com.airbnb.lottie.AsyncUpdates
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 @@ -95,13 +92,13 @@ fun LottieAnimation(

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

val dpScale = Utils.dpScale()
val bounds = composition.bounds
Canvas(
modifier = modifier
.size((composition.bounds.width() / dpScale).dp, (composition.bounds.height() / dpScale).dp)
.lottieSize(bounds.width(), bounds.height())
) {
drawIntoCanvas { canvas ->
val compositionSize = Size(composition.bounds.width().toFloat(), composition.bounds.height().toFloat())
val compositionSize = Size(bounds.width().toFloat(), bounds.height().toFloat())
val intSize = IntSize(size.width.roundToInt(), size.height.roundToInt())

val scale = contentScale.computeScaleFactor(compositionSize, size)
Expand All @@ -125,7 +122,7 @@ fun LottieAnimation(
drawable.maintainOriginalImageBounds = maintainOriginalImageBounds
drawable.clipToCompositionBounds = clipToCompositionBounds
drawable.progress = progress()
drawable.setBounds(0, 0, composition.bounds.width(), composition.bounds.height())
drawable.setBounds(0, 0, bounds.width(), bounds.height())
drawable.draw(canvas.nativeCanvas, matrix)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.airbnb.lottie.compose

import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.ScaleFactor
import androidx.compose.ui.node.LayoutModifierNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.constrain

/**
* Custom layout modifier that Lottie uses instead of the normal size modifier.
*
* This modifier will:
* * Attempt to size the composable to width/height (which is set to the composition bounds)
* * Constrain the size to the incoming constraints
*
* However, if the incoming constraints are unbounded in exactly one dimension, it will constrain that
* dimension to maintain the correct aspect ratio of the composition.
*/
@Stable
internal fun Modifier.lottieSize(
width: Int,
height: Int,
) = this.then(LottieAnimationSizeElement(width, height))

internal data class LottieAnimationSizeElement(
val width: Int,
val height: Int,
) : ModifierNodeElement<LottieAnimationSizeNode>() {
override fun create(): LottieAnimationSizeNode {
return LottieAnimationSizeNode(width, height)
}

override fun update(node: LottieAnimationSizeNode) {
node.width = width
node.height = height
}

override fun InspectorInfo.inspectableProperties() {
name = "Lottie Size"
properties["width"] = width
properties["height"] = height
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is LottieAnimationSizeElement) return false

if (width != other.width) return false
if (height != other.height) return false
return true
}

override fun hashCode(): Int {
var result = width.hashCode()
result = 31 * result + height.hashCode()
return result
}
}

internal class LottieAnimationSizeNode(
var width: Int,
var height: Int,
) : Modifier.Node(), LayoutModifierNode {
override fun MeasureScope.measure(measurable: Measurable, constraints: Constraints): MeasureResult {
val constrainedSize = constraints.constrain(IntSize(width, height))
val wrappedConstraints = when {
// We are constrained in the width dimension but not the height dimension.
constraints.maxHeight == Constraints.Infinity && constraints.maxWidth != Constraints.Infinity -> Constraints(
minWidth = constrainedSize.width,
maxWidth = constrainedSize.width,
minHeight = constrainedSize.width * height / width,
maxHeight = constrainedSize.width * height / width,
)
// We are constrained in the height dimension but not the width dimension.
constraints.maxWidth == Constraints.Infinity && constraints.maxHeight != Constraints.Infinity -> Constraints(
minWidth = constrainedSize.height * width / height,
maxWidth = constrainedSize.height * width / height,
minHeight = constrainedSize.height,
maxHeight = constrainedSize.height,
)
// We are constrained in both or neither dimension. Use the constrained size.
else -> Constraints(
minWidth = constrainedSize.width,
maxWidth = constrainedSize.width,
minHeight = constrainedSize.height,
maxHeight = constrainedSize.height,
)
}

val placeable = measurable.measure(wrappedConstraints)
return layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
}
}

private operator fun Size.times(scale: ScaleFactor): IntSize {
return IntSize((width * scale.scaleX).toInt(), (height * scale.scaleY).toInt())
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
package com.airbnb.lottie.snapshots.tests

import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Text
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.LottieConstants
import com.airbnb.lottie.compose.animateLottieCompositionAsState
import com.airbnb.lottie.compose.rememberLottieComposition
import com.airbnb.lottie.snapshots.R
import com.airbnb.lottie.snapshots.SnapshotTestCase
import com.airbnb.lottie.snapshots.SnapshotTestCaseContext
import com.airbnb.lottie.snapshots.loadCompositionFromAssetsSync
Expand All @@ -28,7 +46,7 @@ class ComposeScaleTypesTestCase : SnapshotTestCase {
{ 1f },
renderMode = renderMode,
modifier = Modifier
.size(720.dp, 1280.dp)
.size(720.dp, 1280.dp),
)
}

Expand All @@ -39,7 +57,7 @@ class ComposeScaleTypesTestCase : SnapshotTestCase {
renderMode = renderMode,
modifier = Modifier
.size(300.dp, 300.dp)
.scale(2f)
.scale(2f),
)
}

Expand All @@ -50,7 +68,7 @@ class ComposeScaleTypesTestCase : SnapshotTestCase {
renderMode = renderMode,
modifier = Modifier
.size(300.dp, 300.dp)
.scale(4f)
.scale(4f),
)
}

Expand All @@ -61,7 +79,7 @@ class ComposeScaleTypesTestCase : SnapshotTestCase {
contentScale = ContentScale.Crop,
renderMode = renderMode,
modifier = Modifier
.size(300.dp, 300.dp)
.size(300.dp, 300.dp),
)
}

Expand All @@ -72,7 +90,7 @@ class ComposeScaleTypesTestCase : SnapshotTestCase {
contentScale = ContentScale.Inside,
renderMode = renderMode,
modifier = Modifier
.size(300.dp, 300.dp)
.size(300.dp, 300.dp),
)
}

Expand All @@ -83,7 +101,7 @@ class ComposeScaleTypesTestCase : SnapshotTestCase {
contentScale = ContentScale.FillBounds,
renderMode = renderMode,
modifier = Modifier
.size(300.dp, 300.dp)
.size(300.dp, 300.dp),
)
}

Expand All @@ -95,7 +113,7 @@ class ComposeScaleTypesTestCase : SnapshotTestCase {
renderMode = renderMode,
modifier = Modifier
.size(300.dp, 300.dp)
.scale(2f)
.scale(2f),
)
}

Expand All @@ -107,7 +125,7 @@ class ComposeScaleTypesTestCase : SnapshotTestCase {
renderMode = renderMode,
modifier = Modifier
.size(300.dp, 300.dp)
.scale(2f)
.scale(2f),
)
}

Expand All @@ -118,7 +136,7 @@ class ComposeScaleTypesTestCase : SnapshotTestCase {
contentScale = ContentScale.Inside,
renderMode = renderMode,
modifier = Modifier
.size(600.dp, 600.dp)
.size(600.dp, 600.dp),
)
}

Expand All @@ -129,7 +147,7 @@ class ComposeScaleTypesTestCase : SnapshotTestCase {
contentScale = ContentScale.FillBounds,
renderMode = renderMode,
modifier = Modifier
.size(600.dp, 600.dp)
.size(600.dp, 600.dp),
)
}

Expand All @@ -140,7 +158,7 @@ class ComposeScaleTypesTestCase : SnapshotTestCase {
contentScale = ContentScale.Fit,
renderMode = renderMode,
modifier = Modifier
.size(600.dp, 600.dp)
.size(600.dp, 600.dp),
)
}

Expand All @@ -151,8 +169,47 @@ class ComposeScaleTypesTestCase : SnapshotTestCase {
contentScale = ContentScale.FillBounds,
renderMode = renderMode,
modifier = Modifier
.size(300.dp, 600.dp)
.size(300.dp, 600.dp),
)
}

val largeSquareComposition = loadCompositionFromAssetsSync("Tests/LargeSquare.json")
snapshotComposable("Compose constrained size", "Column", renderHardwareAndSoftware = true) { renderMode ->
Column(
verticalArrangement = Arrangement.spacedBy(24.dp),
modifier = Modifier
.width(128.dp)
.verticalScroll(rememberScrollState()),
) {
LottieAnimation(
composition = largeSquareComposition,
progress = { 1f },
renderMode = renderMode,
modifier = Modifier.fillMaxWidth(),
)
Text(
modifier = Modifier.fillMaxWidth(),
text = "Other content",
textAlign = TextAlign.Center,
)
}
}

snapshotComposable("Compose constrained size", "Row", renderHardwareAndSoftware = true) { renderMode ->
Row(
horizontalArrangement = Arrangement.spacedBy(24.dp),
modifier = Modifier
.height(128.dp)
.horizontalScroll(rememberScrollState()),
) {
LottieAnimation(
composition = largeSquareComposition,
progress = { 1f },
renderMode = renderMode,
modifier = Modifier.fillMaxHeight(),
)
Text("Other content")
}
}
}
}
}