blob: 58b022220a1c0403efb90f418699e96da9ed7e49 [file] [log] [blame]
package com.airbnb.lottie.compose.renderer
import android.graphics.PointF
import android.util.Log
import androidx.compose.animation.animate
import androidx.compose.animation.core.DurationBasedAnimationSpec
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.TweenSpec
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.foundation.Image
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
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.content.*
import com.airbnb.lottie.model.layer.Layer
import com.airbnb.lottie.value.Keyframe
import java.util.concurrent.TimeUnit
import kotlin.math.floor
val LocalLottieProgress = compositionLocalOf { 0f }
@Composable
fun ComposeLottieAnimation(
compositionResult: LottieCompositionResult,
state: LottieAnimationState,
modifier: Modifier = Modifier,
) {
if (compositionResult !is LottieCompositionResult.Success) return
val composition = compositionResult.composition
LaunchedEffect(composition, state.isPlaying) {
if (!state.isPlaying) return@LaunchedEffect
var repeatCount = 0
if (state.isPlaying && state.progress == 1f) state.progress = 0f
var lastFrameTime = withFrameNanos { it }
while (true) {
withFrameNanos { frameTime ->
val dTime = (frameTime - lastFrameTime) / TimeUnit.MILLISECONDS.toNanos(1).toFloat()
lastFrameTime = frameTime
val dProgress = (dTime * state.speed) / composition.duration
val previousProgress = state.progress
state.progress = (state.progress + dProgress) % 1f
if (previousProgress > state.progress) {
repeatCount++
if (repeatCount != 0 && repeatCount > state.repeatCount) {
state.progress = 1f
state.isPlaying = false
}
}
// TODO: handle min/max frames.
val frame = floor(lerp(composition.startFrame, composition.endFrame, state.progress)).toInt()
state.updateFrame(frame)
}
}
}
Providers(
LocalLottieProgress provides state.progress
) {
composition.layers.forEach { layer ->
when (layer.layerType) {
Layer.LayerType.SHAPE -> Image(
shapeLayerPainter(composition, layer),
contentDescription = null,
modifier = modifier,
)
else -> Unit
}
}
}
}
@Composable
fun shapeLayerPainter(composition: LottieComposition, layer: Layer): VectorPainter {
val transform = rememberTransform(layer)
return rememberVectorPainter(
defaultWidth = composition.bounds.width().dp,
defaultHeight = composition.bounds.height().dp,
viewportWidth = composition.bounds.width().toFloat(),
viewportHeight = composition.bounds.height().toFloat(),
) { viewportWidth, viewportHeight ->
val clipPathData = getMaskPathData(layer.masks)
Log.d("Gabe", "clipPathData hash ${clipPathData.hashCode()} identity ${System.identityHashCode(clipPathData)}")
Group(
name = layer.layerName,
translationX = transform.position.x,
translationY = transform.position.y,
scaleX = composition.bounds.width() / viewportWidth,
scaleY = composition.bounds.height() / viewportHeight,
clipPathData = clipPathData,
) {
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 = rememberTransform(shapeGroup)
val pathData = remember { mutableListOf<PathNode>() }
Group(
name = shapeGroup.name,
translationX = transform.position.x,
translationY = transform.position.y,
) {
// Reuse the list and clear it so that the backing array doesn't need to be recreated.
pathData.clear()
pathData += PathData {
moveTo(380f, 0f)
lineTo(400f, 0f)
lineTo(400f, 20f)
lineTo(380f, 20f)
lineTo(380f, 0f)
close()
}
for (model in shapeGroup.items) {
when (model) {
is RectangleShape -> {
pathData += rectanglePathData(model)
}
is ShapeFill -> {
ComposeShapeFill(model, pathData)
}
}
}
}
}
@Composable
fun rectanglePathData(shape: RectangleShape): List<PathNode> {
val size = remember { PointF() }
val (sizeKeyframe, interpolatedProgress) = shape.size.keyframes.rememberKeyframeProgress()
size.set(
lerp(sizeKeyframe?.startValue?.x ?: 0f, sizeKeyframe?.endValue?.x ?: 0f, interpolatedProgress),
lerp(sizeKeyframe?.startValue?.y ?: 0f, sizeKeyframe?.endValue?.y ?: 0f, interpolatedProgress),
)
val halfWidth = size.x / 2f
val halfHeight = size.y / 2f
return PathData {
moveTo(-halfWidth, -halfHeight)
lineTo(halfWidth, -halfHeight)
lineTo(halfWidth, halfHeight)
lineTo(-halfWidth, halfHeight)
lineTo(-halfWidth, -halfHeight)
close()
}
}
@Composable
fun ComposeShapeFill(fill: ShapeFill, pathData: List<PathNode>) {
// TODO
val colorAnimation = fill.color?.keyframes ?: return
Path(
pathData,
// TODO: use the real color
fill = SolidColor(Color.Red),
)
}
@Composable
fun getMaskPathData(masks: List<Mask>): List<PathNode> {
// TODO: figure out how to reuse the mutable list.
// https://issuetracker.google.com/issues/180774141
val pathNodes = mutableListOf<PathNode>()
val allShapeData = remember(masks.size) { List(masks.size) { ShapeData() } }
pathNodes.clear()
for (i in masks.indices) {
val mask = masks[i]
val (keyframe, progress) = mask.maskPath.keyframes.rememberKeyframeProgress()
val shapeData = allShapeData[i]
val startData = keyframe?.startValue
val endData = keyframe?.endValue
if (startData != null && endData != null) {
shapeData.interpolateBetween(startData, endData, progress)
pathNodes += PathData {
moveTo(shapeData.initialPoint.x, shapeData.initialPoint.y)
for (curveData in shapeData.curves) {
curveTo(
curveData.controlPoint1.x,
curveData.controlPoint1.y,
curveData.controlPoint2.x,
curveData.controlPoint2.y,
curveData.vertex.x,
curveData.vertex.y,
)
}
}
}
}
return pathNodes
}
data class KeyframeProgress<T>(var keyframe: Keyframe<T>? = null, var progress: Float = 0f)
@Composable
fun <T> List<Keyframe<T>>.rememberKeyframeProgress(): KeyframeProgress<T> {
val progress = LocalLottieProgress.current
val value = remember { KeyframeProgress<T>() }
val keyframe = firstOrNull { it.containsProgress(progress) } ?: return value
value.keyframe = keyframe
val linearSizeProgress = lerp(keyframe.startProgress, keyframe.endProgress, progress)
value.progress = when (val i = keyframe.interpolator) {
null -> linearSizeProgress
else -> i.getInterpolation(linearSizeProgress)
}
return value
}