Square moves across the screen
diff --git a/issue-repro/src/main/java/com/airbnb/lottie/issues/IssueReproActivity.kt b/issue-repro/src/main/java/com/airbnb/lottie/issues/IssueReproActivity.kt
index a48d7ff..7fc7b8a 100755
--- a/issue-repro/src/main/java/com/airbnb/lottie/issues/IssueReproActivity.kt
+++ b/issue-repro/src/main/java/com/airbnb/lottie/issues/IssueReproActivity.kt
@@ -15,11 +15,13 @@
super.onCreate(savedInstanceState)
setContent {
val compositionResult = rememberLottieComposition(LottieAnimationSpec.RawRes(R.raw.anim))
+ val state = LottieAnimationState(
+ isPlaying = true,
+ repeatCount = Integer.MAX_VALUE,
+ )
ComposeLottieAnimation(
compositionResult,
- LottieAnimationState(
- isPlaying = true,
- ),
+ state,
)
}
}
diff --git a/issue-repro/src/main/res/raw/anim.json b/issue-repro/src/main/res/raw/anim.json
index febb9af..072ccdb 100644
--- a/issue-repro/src/main/res/raw/anim.json
+++ b/issue-repro/src/main/res/raw/anim.json
@@ -1 +1 @@
-{"v":"5.7.4","fr":60,"ip":0,"op":63,"w":400,"h":400,"nm":"Test","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":[200,200,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":[280.614,280.614],"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":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-6.693,4.307],"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}],"ip":0,"op":63,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
+{"v":"5.7.4","fr":60,"ip":0,"op":61,"w":400,"h":400,"nm":"Test","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":[200,200,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":[150,150],"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":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[-125,-125],"to":[41.667,41.667],"ti":[-41.667,-41.667]},{"t":60,"s":[125,125]}],"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}],"ip":0,"op":61,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/renderer/AnimatableValues.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/renderer/AnimatableValues.kt
deleted file mode 100644
index 8bb6b8b..0000000
--- a/lottie-compose/src/main/java/com/airbnb/lottie/compose/renderer/AnimatableValues.kt
+++ /dev/null
@@ -1,154 +0,0 @@
-package com.airbnb.lottie.compose.renderer
-
-import android.graphics.PointF
-import androidx.compose.runtime.*
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Matrix
-import androidx.compose.ui.graphics.graphicsLayer
-import com.airbnb.lottie.animation.keyframe.PathKeyframe
-import com.airbnb.lottie.model.animatable.AnimatableTransform
-import com.airbnb.lottie.model.layer.Layer
-import com.airbnb.lottie.value.Keyframe
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.setValue
-import com.airbnb.lottie.value.ScaleXY
-import kotlin.math.cos
-import kotlin.math.sin
-import kotlin.math.tan
-
-@Composable
-private fun <T> List<Keyframe<T>>.asState(
- progress: Float,
- defaultValue: T? = null,
- calculator: (keyframe: Keyframe<T>, interpolatedProgress: Float) -> T,
-): T {
- if (isEmpty()) {
- return remember { defaultValue ?: error("You must specify a default value if the keyframes are empty.") }
- }
-
- var lastCalculatedProgress by remember { mutableStateOf(0f) }
- var value by remember { mutableStateOf(defaultValue ?: calculator(first(), 0f)) }
- if (lastCalculatedProgress == progress) return value
- lastCalculatedProgress = progress
-
- var keyframeIndex by remember { mutableStateOf(0) }
- val newKeyframeIndex = findKeyframeIndex(progress, keyframeIndex)
- val keyframe = get(keyframeIndex)
- // The keyframe didn't change and it is static.
- if (keyframeIndex == newKeyframeIndex && keyframe.isStatic) return value
-
- val interpolatedProgress by keyframe.getInterpolatedProgressIntoKeyframe(progress)
- value = calculator(keyframe, interpolatedProgress)
- return value
-}
-
-@Composable
-fun List<Keyframe<Float>>.asFloatState(progress: Float, defaultValue: Float = 0f): Float {
- return asState(progress, defaultValue) { keyframe, interpolatedProgress ->
- lerp(keyframe.startValue ?: 0f, keyframe.endValue ?: 0f, interpolatedProgress)
- }
-}
-
-@Composable
-fun List<Keyframe<Int>>.asIntState(progress: Float, defaultValue: Int = 0): Int {
- return asState(progress, defaultValue) { keyframe, interpolatedProgress ->
- lerp(keyframe.startValue ?: 0, keyframe.endValue ?: 0, interpolatedProgress)
- }
-}
-
-@Composable
-fun List<Keyframe<ScaleXY>>.asScaleXYState(progress: Float, defaultValue: ScaleXY = ScaleXY()): ScaleXY {
- val outPoint = remember { ScaleXY() }
- return asState(progress, defaultValue) { keyframe, interpolatedProgress ->
- outPoint.set(
- lerp(keyframe.startValue?.scaleX ?: 0f, keyframe.endValue?.scaleX ?: 0f, interpolatedProgress),
- lerp(keyframe.startValue?.scaleY ?: 0f, keyframe.endValue?.scaleY ?: 0f, interpolatedProgress),
- )
- outPoint
- }
-}
-
-@Composable
-fun List<Keyframe<PointF>>.asPointFState(progress: Float, defaultValue: PointF = PointF()): PointF {
- val outPoint = remember { PointF() }
- return asState(progress, defaultValue) { keyframe, interpolatedProgress ->
- outPoint.set(
- lerp(keyframe.startValue?.x ?: 0f, keyframe.endValue?.x ?: 0f, interpolatedProgress),
- lerp(keyframe.startValue?.y ?: 0f, keyframe.endValue?.y ?: 0f, interpolatedProgress),
- )
- outPoint
- }
-}
-
-@Composable
-fun List<PathKeyframe>.asPathToPointState(progress: Float, defaultValue: PointF = PointF()): State<PointF> {
- if (isEmpty()) {
- return remember { mutableStateOf(defaultValue) }
- }
-
- var lastCalculatedProgress by remember { mutableStateOf(0f) }
- var value = remember { mutableStateOf(defaultValue ) }
- if (lastCalculatedProgress == progress) return value
- lastCalculatedProgress = progress
-
- var keyframeIndex by remember { mutableStateOf(0) }
- val newKeyframeIndex = findKeyframeIndex(progress, keyframeIndex)
- val keyframe = get(keyframeIndex)
- // The keyframe didn't change and it is static.
- if (keyframeIndex == newKeyframeIndex && keyframe.isStatic) return value
-
- val interpolatedProgress by keyframe.getInterpolatedProgressIntoKeyframe(progress)
- val path = keyframe.path
- if (path == null) {
- value.value = keyframe.startValue ?: value.value
- } else {
- value.value = pointAlongPath(path, interpolatedProgress)
- }
- return value
-}
-
-@Composable
-fun pointAlongPath(path: android.graphics.Path, progress: Float): PointF {
- val locs = remember { FloatArray(2) }
- var value by remember { mutableStateOf(PointF()) }
- val pathMeasure = remember { android.graphics.PathMeasure() }
- LaunchedEffect(path) {
- pathMeasure.setPath(path, false)
- }
- LaunchedEffect(path, progress) {
- pathMeasure.getPosTan(progress * pathMeasure.length, locs, null)
- }
- value.set(locs[0], locs[1])
- return value
-}
-
-private fun List<Keyframe<*>>.findKeyframeIndex(progress: Float, currentKeyframeIndex: Int): Int {
- if (get(currentKeyframeIndex).containsProgress(progress)) return currentKeyframeIndex
- // TODO: if speed is reversed, flip these two.
- for (i in currentKeyframeIndex until size) {
- if (get(i).containsProgress(progress)) return i
- }
- for (i in 0 until currentKeyframeIndex) {
- if (get(0).containsProgress(progress)) return i
- }
- error("Unable to find keyframe for progress $progress.")
-}
-
-@Composable
-private fun Keyframe<*>.getInterpolatedProgressIntoKeyframe(progress: Float): State<Float> {
- var interpolatedProgress = remember { mutableStateOf(0f) }
- val linearProgress = lerp(startProgress, endProgress, progress)
- interpolatedProgress.value = when (val i = interpolator) {
- null -> linearProgress
- else -> i.getInterpolation(linearProgress)
- }
- return interpolatedProgress
-}
-
-class LayerTransform(layer: Layer) {
- private val transform: AnimatableTransform = layer.shapes.firstInstanceOf()
-
- var progress by mutableStateOf(0f)
-
- val position by derivedStateOf { transform.position?.keyframes?.asPointFState(progress) }
-}
\ No newline at end of file
diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/renderer/LayerTransform.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/renderer/LayerTransform.kt
new file mode 100644
index 0000000..4974626
--- /dev/null
+++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/renderer/LayerTransform.kt
@@ -0,0 +1,72 @@
+package com.airbnb.lottie.compose.renderer
+
+import android.graphics.PointF
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.graphics.graphicsLayer
+import com.airbnb.lottie.model.animatable.AnimatableTransform
+import com.airbnb.lottie.model.layer.Layer
+import com.airbnb.lottie.value.Keyframe
+import kotlin.properties.Delegates
+
+fun Modifier.withTransform(transform: LayerTransform): Modifier {
+ if (transform.isIdentity) return this
+
+ // TODO: maybe use composed {}
+ return this.then(
+ Modifier.graphicsLayer(
+ translationX = transform.position.x,
+ translationY = transform.position.y,
+ )
+ )
+}
+
+class LayerTransform(private val transform: AnimatableTransform?) {
+ var progress by Delegates.observable(0f) { _, oldValue, newValue ->
+ if (oldValue == newValue) return@observable
+ updatePosition(newValue)
+ }
+
+ private var positionProgress = 0f
+ private var positionKeyframeIndex = 0
+ private var _position = mutableStateOf(POINT_F)
+ val position: PointF by _position
+
+ val isIdentity get() = transform == null || position.equals(0f, 0f)
+
+ private fun updatePosition(progress: Float) {
+ if (positionProgress == progress) return
+ val keyframes = transform?.position?.keyframes ?: return
+
+ val keyframe = keyframes.findKeyframeIndex(progress, positionKeyframeIndex)
+ val linearProgress = lerp(keyframe.startProgress, keyframe.endProgress, progress)
+ val interpolatedProgress = when (val i = keyframe.interpolator) {
+ null -> linearProgress
+ else -> i.getInterpolation(linearProgress)
+ }
+ _position.value.set(
+ lerp(keyframe.startValue?.x ?: 0f, keyframe.endValue?.x ?: 0f, interpolatedProgress),
+ lerp(keyframe.startValue?.y ?: 0f, keyframe.endValue?.y ?: 0f, interpolatedProgress),
+ )
+ }
+
+ companion object {
+ private val POINT_F = PointF()
+ }
+}
+
+private fun <T> List<Keyframe<T>>.findKeyframeIndex(progress: Float, currentKeyframeIndex: Int): Keyframe<T> {
+ var keyframe = get(currentKeyframeIndex)
+ if (keyframe.containsProgress(progress)) return keyframe
+ // TODO: if speed is reversed, flip these two.
+ for (i in currentKeyframeIndex until size) {
+ keyframe = get(i)
+ if (keyframe.containsProgress(progress)) return keyframe
+ }
+ for (i in 0 until currentKeyframeIndex) {
+ keyframe = get(i)
+ if (keyframe.containsProgress(progress)) return keyframe
+ }
+ return keyframe
+}
\ No newline at end of file
diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/renderer/LottieComposeRenderer.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/renderer/LottieComposeRenderer.kt
index ae26900..20cb2c0 100644
--- a/lottie-compose/src/main/java/com/airbnb/lottie/compose/renderer/LottieComposeRenderer.kt
+++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/renderer/LottieComposeRenderer.kt
@@ -1,12 +1,22 @@
package com.airbnb.lottie.compose.renderer
-import androidx.compose.foundation.Canvas
+import android.graphics.PointF
+import android.util.Log
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.size
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.vector.*
+import androidx.compose.ui.unit.dp
+import com.airbnb.lottie.LottieComposition
import com.airbnb.lottie.compose.LottieAnimationState
import com.airbnb.lottie.compose.LottieCompositionResult
import com.airbnb.lottie.model.animatable.AnimatableTransform
+import com.airbnb.lottie.model.content.RectangleShape
+import com.airbnb.lottie.model.content.ShapeFill
+import com.airbnb.lottie.model.content.ShapeGroup
import com.airbnb.lottie.model.layer.Layer
import java.util.concurrent.TimeUnit
import kotlin.math.floor
@@ -49,7 +59,12 @@
) {
composition.layers.forEach { layer ->
when (layer.layerType) {
- Layer.LayerType.SHAPE -> ShapeLayer(layer)
+ Layer.LayerType.SHAPE -> Image(
+ shapeLayerPainter(composition, layer),
+ contentDescription = null,
+ // TODO: use a different size
+ modifier = Modifier.size(256.dp)
+ )
else -> Unit
}
}
@@ -57,15 +72,98 @@
}
@Composable
-fun ShapeLayer(layer: Layer) {
+fun shapeLayerPainter(composition: LottieComposition, layer: Layer): VectorPainter {
val progress = LocalLottieProgress.current
- val animatableTransform: AnimatableTransform = remember(layer) { layer.shapes.firstInstanceOf() }
- val transform by transformMatrix(animatableTransform)
-
- Canvas(
- modifier = Modifier
- .withTransform(progress, animatableTransform)
- ) {
-
+ val animatableTransform = remember(layer) { layer.shapes.firstOrNull { it is AnimatableTransform } as? AnimatableTransform }
+ val transform = LayerTransform(animatableTransform)
+ LaunchedEffect(progress) {
+ transform.progress = progress
}
+
+ return rememberVectorPainter(
+ defaultWidth = 256.dp,
+ defaultHeight = 256.dp,
+ ) { viewportWidth, viewportHeight ->
+ Group(
+ name = layer.layerName,
+ translationX = transform.position.x,
+ translationY = transform.position.y,
+ scaleX = viewportWidth / composition.bounds.width(),
+ scaleY = viewportHeight / composition.bounds.height(),
+ ) {
+ PathData {
+ layer.shapes.forEach { shapeModel ->
+ when (shapeModel) {
+ is ShapeGroup -> ComposeShapeGroup(shapeModel)
+ else -> Log.d("Gabe", "Don't know how to draw ${shapeModel::class.simpleName}")
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun ComposeShapeGroup(shapeGroup: ShapeGroup) {
+ if (shapeGroup.isHidden || shapeGroup.items.isEmpty()) return
+ val transform = remember(shapeGroup) { LayerTransform(shapeGroup.items.lastOrNull() as? AnimatableTransform) }
+ val pathData = remember { mutableListOf<PathNode>() }
+ val progress = LocalLottieProgress.current
+ LaunchedEffect(progress) {
+ transform.progress = progress
+ }
+ pathData.clear()
+ Group(
+ name = shapeGroup.name,
+ translationX = transform.position.x,
+ translationY = transform.position.y,
+ ) {
+ for (model in shapeGroup.items) {
+ when (model) {
+ is RectangleShape -> {
+ pathData += rectanglePathData(model)
+ }
+ is ShapeFill -> {
+ ComposeShapeFill(model, pathData)
+ }
+ }
+ Log.d("Gabe", "Drawing ${model::class.simpleName} $model")
+ }
+ }
+}
+
+@Composable
+fun rectanglePathData(shape: RectangleShape): List<PathNode> {
+ val progress = LocalLottieProgress.current
+ // TODO: optimize this
+ val size = remember { PointF() }
+ val sizeKeyframe = shape.size.keyframes.firstOrNull { it.containsProgress(progress) } ?: return emptyList()
+ val linearSizeProgress = lerp(sizeKeyframe.startProgress, sizeKeyframe.endProgress, progress)
+ val interpolatedProgress = when (val i = sizeKeyframe.interpolator) {
+ null -> linearSizeProgress
+ else -> i.getInterpolation(linearSizeProgress)
+ }
+ size.set(
+ lerp(sizeKeyframe.startValue?.x ?: 0f, sizeKeyframe.endValue?.x ?: 0f, interpolatedProgress),
+ lerp(sizeKeyframe.startValue?.y ?: 0f, sizeKeyframe.endValue?.y ?: 0f, interpolatedProgress),
+ )
+
+ return PathData {
+ moveTo(0f, 0f)
+ lineTo(size.x, 0f)
+ lineTo(size.x, size.y)
+ lineTo(0f, size.y)
+ lineTo(0f, 0f)
+ close()
+ }
+}
+
+@Composable
+fun ComposeShapeFill(fill: ShapeFill, pathData: List<PathNode>) {
+ Log.d("Gabe", "Drawing fill with ${pathData.size} nodes")
+ Path(
+ pathData,
+ // TODO: use the real color
+ fill = SolidColor(Color.Red),
+ )
}
\ No newline at end of file
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/layer/Layer.java b/lottie/src/main/java/com/airbnb/lottie/model/layer/Layer.java
index 174de65..7bab3ab 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/layer/Layer.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/layer/Layer.java
@@ -133,6 +133,10 @@
return layerType;
}
+ public String getLayerName() {
+ return layerName;
+ }
+
MatteType getMatteType() {
return matteType;
}