Skip to content

Commit

Permalink
[Compose] Add support for ContentScale and Alignment (#1844)
Browse files Browse the repository at this point in the history
LottieAnimation's behavior now perfectly matches the behavior of ContentScale and Alignment in Image.
  • Loading branch information
gpeal authored Jul 15, 2021
1 parent f6a7569 commit 954f7d5
Show file tree
Hide file tree
Showing 7 changed files with 244 additions and 23 deletions.
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
package com.airbnb.lottie.compose

import android.graphics.Matrix
import androidx.annotation.FloatRange
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.drawscope.withTransform
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 com.airbnb.lottie.LottieComposition
import com.airbnb.lottie.LottieDrawable
import kotlin.math.roundToInt

/**
* This is the base LottieAnimation composable. It takes a composition and renders it at a specific progress.
Expand Down Expand Up @@ -47,6 +52,9 @@ import com.airbnb.lottie.LottieDrawable
* doesn't render correctly, please file an issue.
* @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
* size than this composable.
* @param contentScale Define how the animation should be scaled if it has a different size than this Composable.
*/
@Composable
fun LottieAnimation(
Expand All @@ -57,32 +65,41 @@ fun LottieAnimation(
applyOpacityToLayers: Boolean = false,
enableMergePaths: Boolean = false,
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) }

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

Canvas(
modifier = modifier
.maintainAspectRatio(composition)
.fillMaxSize()
) {
drawIntoCanvas { canvas ->
withTransform({
scale(size.width / composition.bounds.width().toFloat(), size.height / composition.bounds.height().toFloat(), Offset.Zero)
}) {
drawable.composition = composition
if (dynamicProperties !== setDynamicProperties) {
setDynamicProperties?.removeFrom(drawable)
dynamicProperties?.addTo(drawable)
setDynamicProperties = dynamicProperties
}
drawable.setOutlineMasksAndMattes(outlineMasksAndMattes)
drawable.isApplyingOpacityToLayersEnabled = applyOpacityToLayers
drawable.enableMergePathsForKitKatAndAbove(enableMergePaths)
drawable.progress = progress
drawable.draw(canvas.nativeCanvas)
val compositionSize = Size(composition.bounds.width().toFloat(), composition.bounds.height().toFloat())
val intSize = IntSize(size.width.roundToInt(), size.height.roundToInt())

val scale = contentScale.computeScaleFactor(compositionSize, size)
val translation = alignment.align(compositionSize * scale, intSize, layoutDirection)
matrix.reset()
matrix.preTranslate(translation.x.toFloat(), translation.y.toFloat())
matrix.preScale(scale.scaleX, scale.scaleY)


drawable.composition = composition
if (dynamicProperties !== setDynamicProperties) {
setDynamicProperties?.removeFrom(drawable)
dynamicProperties?.addTo(drawable)
setDynamicProperties = dynamicProperties
}
drawable.setOutlineMasksAndMattes(outlineMasksAndMattes)
drawable.isApplyingOpacityToLayersEnabled = applyOpacityToLayers
drawable.enableMergePathsForKitKatAndAbove(enableMergePaths)
drawable.progress = progress
drawable.draw(canvas.nativeCanvas, matrix)
}
}
}
Expand All @@ -107,6 +124,8 @@ fun LottieAnimation(
applyOpacityToLayers: Boolean = false,
enableMergePaths: Boolean = false,
dynamicProperties: LottieDynamicProperties? = null,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
) {
val progress by animateLottieCompositionAsState(
composition,
Expand All @@ -124,11 +143,11 @@ fun LottieAnimation(
applyOpacityToLayers,
enableMergePaths,
dynamicProperties,
alignment,
contentScale,
)
}

private fun Modifier.maintainAspectRatio(composition: LottieComposition?): Modifier {
composition ?: return this
// TODO: use ContentScale and a transform here
return this.then(aspectRatio(composition.bounds.width() / composition.bounds.height().toFloat()))
}
private operator fun Size.times(scale: ScaleFactor): IntSize {
return IntSize((width * scale.scaleX).toInt(), (height * scale.scaleY).toInt())
}
8 changes: 8 additions & 0 deletions lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
Original file line number Diff line number Diff line change
Expand Up @@ -1200,6 +1200,14 @@ private float getMaxScale(@NonNull Canvas canvas) {
return Math.min(maxScaleX, maxScaleY);
}

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public void draw(Canvas canvas, Matrix matrix) {
if (compositionLayer == null) {
return;
}
compositionLayer.draw(canvas, matrix, alpha);
}

private void drawWithNewAspectRatio(Canvas canvas) {
if (compositionLayer == null) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import androidx.navigation.compose.rememberNavController
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.sample.compose.examples.AnimatableExamplesPage
import com.airbnb.lottie.sample.compose.examples.BasicUsageExamplesPage
import com.airbnb.lottie.sample.compose.examples.ContentScaleExamplesPage
import com.airbnb.lottie.sample.compose.examples.DynamicPropertiesExamplesPage
import com.airbnb.lottie.sample.compose.examples.ExamplesPage
import com.airbnb.lottie.sample.compose.examples.ImagesExamplesPage
Expand Down Expand Up @@ -102,6 +103,7 @@ class ComposeActivity : AppCompatActivity() {
composable(Route.DynamicPropertiesExamples.route) { DynamicPropertiesExamplesPage() }
composable(Route.ImagesExamples.route) { ImagesExamplesPage() }
composable(Route.TextExamples.route) { TextExamplesPage() }
composable(Route.ContentScaleExamples.route) { ContentScaleExamplesPage() }
composable(
Route.Player.fullRoute,
arguments = Route.Player.args
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ sealed class Route(val route: String, val args: List<NamedNavArgument> = emptyLi

object DynamicPropertiesExamples : Route("dynamic properties examples")

object ContentScaleExamples : Route("ContentScale examples")

object Player : Route(
"player",
listOf(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package com.airbnb.lottie.sample.compose.examples

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDownward
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.ArrowDropUp
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.PopupProperties
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.rememberLottieComposition
import com.airbnb.lottie.sample.compose.R

@Composable
fun ContentScaleExamplesPage() {
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.gradient))
var alignment by remember { mutableStateOf(Alignment.Center) }
var contentScale by remember { mutableStateOf(ContentScale.Fit) }

UsageExamplePageScaffold {
Column(
modifier = Modifier
.fillMaxWidth()
) {
OptionsRow(
alignment,
contentScale,
onAlignmentChanged = { alignment = it },
onContentScaleChanged = { contentScale = it },
)
Box(
modifier = Modifier
.fillMaxSize()
.padding(8.dp)
.border(2.dp, Color.Green)
.padding(2.dp)
) {
LottieAnimation(
composition,
progress = 0f,
alignment = alignment,
contentScale = contentScale,
)
}
}
}
}
@Composable
private fun OptionsRow(
alignment: Alignment,
contentScale: ContentScale,
onContentScaleChanged: (ContentScale) -> Unit,
onAlignmentChanged: (Alignment) -> Unit,
) {
var scaleExpanded by remember { mutableStateOf(false) }
var alignmentExpanded by remember { mutableStateOf(false) }

val onContentScaleChangedState by rememberUpdatedState(onContentScaleChanged)
val onAlignmentChangedState by rememberUpdatedState(onAlignmentChanged)

Row(
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.height(64.dp)
) {
Row(
modifier = Modifier
.clickable {
scaleExpanded = true
alignmentExpanded = false
}
.padding(horizontal = 4.dp, vertical = 16.dp)
){
Text(
ContentScales[contentScale] ?: "Unknown Content Scale",
textAlign = TextAlign.Center,
modifier = Modifier
.defaultMinSize(minWidth = 128.dp)
)
Icon(
imageVector = if (scaleExpanded) Icons.Filled.ArrowDropUp else Icons.Filled.ArrowDropDown,
contentDescription = null,
)
DropdownMenu(
scaleExpanded,
onDismissRequest = { scaleExpanded = false },
) {
ContentScales.forEach { (cs, label) ->
DropdownMenuItem(
onClick = { onContentScaleChangedState(cs) },
) {
Text(label)
}
}
}
}
Row(
modifier = Modifier
.clickable {
alignmentExpanded = true
scaleExpanded = false
}
.padding(horizontal = 4.dp, vertical = 16.dp)
) {
Text(
Alignments[alignment] ?: "Unknown Alignment",
textAlign = TextAlign.Center,
modifier = Modifier
.defaultMinSize(minWidth = 128.dp)
)
Icon(
imageVector = if (alignmentExpanded) Icons.Filled.ArrowDropUp else Icons.Filled.ArrowDropDown,
contentDescription = null,
)
DropdownMenu(
alignmentExpanded,
onDismissRequest = { alignmentExpanded = false }
) {
Alignments.forEach { (a, label) ->
DropdownMenuItem(
onClick = { onAlignmentChangedState(a) },
) {
Text(label)
}
}
}
}
}
}

private val Alignments = mapOf(
Alignment.TopStart to "TopStart",
Alignment.TopCenter to "TopCenter",
Alignment.TopEnd to "TopEnd",
Alignment.CenterStart to "CenterStart",
Alignment.Center to "Center",
Alignment.CenterEnd to "CenterEnd",
Alignment.BottomStart to "BottomStart",
Alignment.BottomCenter to "BottomCenter",
Alignment.BottomEnd to "BottomEnd",
)

private val ContentScales = mapOf(
ContentScale.Fit to "Fit",
ContentScale.Crop to "Crop",
ContentScale.Inside to "Inside",
ContentScale.None to "None",
ContentScale.FillBounds to "FillBounds",
ContentScale.FillHeight to "FillHeight",
ContentScale.FillWidth to "FillWidth",
)
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,11 @@ fun ExamplesPage(navController: NavController) {
modifier = Modifier
.clickable { navController.navigate(Route.TextExamples) }
)
ListItem(
text = { Text("ContentScale and Alignment") },
secondaryText = { Text("Changing an animation's ContentScale and Alignment") },
modifier = Modifier
.clickable { navController.navigate(Route.ContentScaleExamples) }
)
}
}
1 change: 1 addition & 0 deletions sample-compose/src/main/res/raw/gradient.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"v":"5.7.7","fr":29.9700012207031,"ip":0,"op":900.000036657751,"w":500,"h":200,"nm":"Gradient","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[500,200],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.526341557503,0.526341557503,0.526341557503,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[250,100],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,1,0,0,0.5,0.5,0,0.5,1,0,0,1],"ix":9}},"s":{"a":0,"k":[0,0],"ix":5},"e":{"a":0,"k":[500,0],"ix":6},"t":1,"nm":"Gradient Fill 1","mn":"ADBE Vector Graphic - G-Fill","hd":false}],"ip":0,"op":900.000036657751,"st":0,"bm":0}],"markers":[]}

0 comments on commit 954f7d5

Please sign in to comment.