Skip to content

Commit

Permalink
Constrain unbounded dimensions when exactly one dimension is unbounded (
Browse files Browse the repository at this point in the history
#2437)

Fixes #2264
  • Loading branch information
gpeal committed Dec 29, 2023
1 parent a855905 commit a6e416d
Show file tree
Hide file tree
Showing 4 changed files with 403 additions and 20 deletions.
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
@@ -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())
}
@@ -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")
}
}
}
}
}

0 comments on commit a6e416d

Please sign in to comment.