Skip to content

Commit

Permalink
[Compose] Add LottiePainter (#2442)
Browse files Browse the repository at this point in the history
This allows Lottie to be used with anything that accepts a Painter (like Image).

Fixes #2193
  • Loading branch information
gpeal committed Dec 30, 2023
1 parent 8383393 commit 4060d30
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 0 deletions.
@@ -0,0 +1,132 @@
package com.airbnb.lottie.compose

import android.graphics.Matrix
import android.graphics.Typeface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ScaleFactor
import androidx.compose.ui.unit.IntSize
import com.airbnb.lottie.AsyncUpdates
import com.airbnb.lottie.LottieComposition
import com.airbnb.lottie.LottieDrawable
import com.airbnb.lottie.RenderMode
import kotlin.math.roundToInt

/**
* A composable that makes it easy to create a [LottiePainter] and update its properties.
*/
@Composable
fun rememberLottiePainter(
composition: LottieComposition? = null,
progress: Float = 0f,
outlineMasksAndMattes: Boolean = false,
applyOpacityToLayers: Boolean = false,
enableMergePaths: Boolean = false,
renderMode: RenderMode = RenderMode.AUTOMATIC,
maintainOriginalImageBounds: Boolean = false,
dynamicProperties: LottieDynamicProperties? = null,
clipToCompositionBounds: Boolean = true,
clipTextToBoundingBox: Boolean = false,
fontMap: Map<String, Typeface>? = null,
asyncUpdates: AsyncUpdates = AsyncUpdates.AUTOMATIC,
): LottiePainter {
val painter = remember { LottiePainter() }
painter.composition = composition
painter.progress = progress
painter.outlineMasksAndMattes = outlineMasksAndMattes
painter.applyOpacityToLayers = applyOpacityToLayers
painter.enableMergePaths = enableMergePaths
painter.renderMode = renderMode
painter.maintainOriginalImageBounds = maintainOriginalImageBounds
painter.dynamicProperties = dynamicProperties
painter.clipToCompositionBounds = clipToCompositionBounds
painter.clipTextToBoundingBox = clipTextToBoundingBox
painter.fontMap = fontMap
painter.asyncUpdates = asyncUpdates
return painter
}

/**
* A [Painter] that renders a [LottieComposition].
*/
class LottiePainter internal constructor(
composition: LottieComposition? = null,
progress: Float = 0f,
outlineMasksAndMattes: Boolean = false,
applyOpacityToLayers: Boolean = false,
enableMergePaths: Boolean = false,
renderMode: RenderMode = RenderMode.AUTOMATIC,
maintainOriginalImageBounds: Boolean = false,
dynamicProperties: LottieDynamicProperties? = null,
clipToCompositionBounds: Boolean = true,
clipTextToBoundingBox: Boolean = false,
fontMap: Map<String, Typeface>? = null,
asyncUpdates: AsyncUpdates = AsyncUpdates.AUTOMATIC,
) : Painter() {
internal var composition by mutableStateOf(composition)
internal var progress by mutableFloatStateOf(progress)
internal var outlineMasksAndMattes by mutableStateOf(outlineMasksAndMattes)
internal var applyOpacityToLayers by mutableStateOf(applyOpacityToLayers)
internal var enableMergePaths by mutableStateOf(enableMergePaths)
internal var renderMode by mutableStateOf(renderMode)
internal var maintainOriginalImageBounds by mutableStateOf(maintainOriginalImageBounds)
internal var dynamicProperties by mutableStateOf(dynamicProperties)
internal var clipToCompositionBounds by mutableStateOf(clipToCompositionBounds)
internal var fontMap by mutableStateOf(fontMap)
internal var asyncUpdates by mutableStateOf(asyncUpdates)
internal var clipTextToBoundingBox by mutableStateOf(clipTextToBoundingBox)

private var setDynamicProperties: LottieDynamicProperties? = null

private val drawable = LottieDrawable()
private val matrix = Matrix()
override val intrinsicSize: Size
get() {
val composition = composition ?: return Size.Unspecified
return Size(composition.bounds.width().toFloat(), composition.bounds.height().toFloat())
}

override fun DrawScope.onDraw() {
val composition = composition ?: return
drawIntoCanvas { canvas ->
val compositionSize = Size(composition.bounds.width().toFloat(), composition.bounds.height().toFloat())
val intSize = IntSize(size.width.roundToInt(), size.height.roundToInt())

matrix.reset()
matrix.preScale(intSize.width / compositionSize.width, intSize.height / compositionSize.height)

drawable.enableMergePathsForKitKatAndAbove(enableMergePaths)
drawable.renderMode = renderMode
drawable.asyncUpdates = asyncUpdates
drawable.composition = composition
drawable.setFontMap(fontMap)
if (dynamicProperties !== setDynamicProperties) {
setDynamicProperties?.removeFrom(drawable)
dynamicProperties?.addTo(drawable)
setDynamicProperties = dynamicProperties
}
drawable.setOutlineMasksAndMattes(outlineMasksAndMattes)
drawable.isApplyingOpacityToLayersEnabled = applyOpacityToLayers
drawable.maintainOriginalImageBounds = maintainOriginalImageBounds
drawable.clipToCompositionBounds = clipToCompositionBounds
drawable.clipTextToBoundingBox = clipTextToBoundingBox
drawable.progress = progress
drawable.setBounds(0, 0, composition.bounds.width(), composition.bounds.height())
drawable.draw(canvas.nativeCanvas, matrix)
}

}
}

private operator fun Size.times(scale: ScaleFactor): IntSize {
return IntSize((width * scale.scaleX).toInt(), (height * scale.scaleY).toInt())
}
Expand Up @@ -31,6 +31,7 @@ import com.airbnb.lottie.snapshots.tests.LargeCompositionSoftwareRendering
import com.airbnb.lottie.snapshots.tests.MarkersTestCase
import com.airbnb.lottie.snapshots.tests.NightModeTestCase
import com.airbnb.lottie.snapshots.tests.OutlineMasksAndMattesTestCase
import com.airbnb.lottie.snapshots.tests.PainterTestCase
import com.airbnb.lottie.snapshots.tests.PartialFrameProgressTestCase
import com.airbnb.lottie.snapshots.tests.PolygonStrokeTestCase
import com.airbnb.lottie.snapshots.tests.ProdAnimationsTestCase
Expand Down Expand Up @@ -147,6 +148,7 @@ class LottieSnapshotTest {
FrameBoundariesTestCase(),
ScaleTypesTestCase(),
ComposeScaleTypesTestCase(),
PainterTestCase(),
DynamicPropertiesTestCase(),
MarkersTestCase(),
AssetsTestCase(),
Expand Down
@@ -0,0 +1,56 @@
package com.airbnb.lottie.snapshots.tests

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.size
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import com.airbnb.lottie.LottieCompositionFactory
import com.airbnb.lottie.compose.rememberLottiePainter
import com.airbnb.lottie.snapshots.SnapshotTestCase
import com.airbnb.lottie.snapshots.SnapshotTestCaseContext
import com.airbnb.lottie.snapshots.snapshotComposable

class PainterTestCase : SnapshotTestCase {
override suspend fun SnapshotTestCaseContext.run() {
snapshotComposable("Compose Painter", "0%") {
val composition = LottieCompositionFactory.fromAssetSync(context, "Tests/Laugh4.json").value!!
val painter = rememberLottiePainter(composition, 0f)
Image(
painter = painter,
contentDescription = "",
contentScale = ContentScale.Fit,
alignment = Alignment.BottomCenter,
modifier = Modifier
.size(500.dp, 700.dp),
)
}

snapshotComposable("Compose Painter", "50%") {
val composition = LottieCompositionFactory.fromAssetSync(context, "Tests/Laugh4.json").value!!
val painter = rememberLottiePainter(composition, 0.5f)
Image(
painter = painter,
contentDescription = "",
contentScale = ContentScale.Fit,
alignment = Alignment.BottomCenter,
modifier = Modifier
.size(500.dp, 700.dp),
)
}

snapshotComposable("Compose Painter", "100%") {
val composition = LottieCompositionFactory.fromAssetSync(context, "Tests/Laugh4.json").value!!
val painter = rememberLottiePainter(composition, 1f)
Image(
painter = painter,
contentDescription = "",
contentScale = ContentScale.Fit,
alignment = Alignment.BottomCenter,
modifier = Modifier
.size(500.dp, 700.dp),
)
}
}
}

0 comments on commit 4060d30

Please sign in to comment.