WIP mattes
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 80f04ca..ce93001 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
@@ -1,6 +1,7 @@
package com.airbnb.lottie.issues
import android.os.Bundle
+import android.util.Log
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.background
diff --git a/issue-repro/src/main/res/raw/anim.json b/issue-repro/src/main/res/raw/anim.json
index 5cecbc8..b7691a7 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":300,"w":400,"h":400,"nm":"Matte","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":[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":0,"k":[75,75],"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":181,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
+{"v":"5.7.4","fr":60,"ip":0,"op":300,"w":400,"h":400,"nm":"Matte","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 2","td":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":[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":0,"k":[75,75],"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":181,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 1","tt":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":[150,150],"ix":2},"p":{"a":0,"k":[50,50],"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":[75,75],"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":181,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/renderer/ComposeLottieTransform.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/renderer/ComposeLottieTransform.kt
index 1dcafdb..4ab4337 100644
--- a/lottie-compose/src/main/java/com/airbnb/lottie/compose/renderer/ComposeLottieTransform.kt
+++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/renderer/ComposeLottieTransform.kt
@@ -11,21 +11,22 @@
import kotlin.properties.Delegates
@Composable
-fun rememberTransform(layer: Layer): ComposeLottieTransform {
+fun rememberTransform(layer: Layer, progress: Float): ComposeLottieTransform {
val animatableTransform = remember(layer) { layer.transform }
- return rememberTransform(animatableTransform)
+ return rememberTransform(animatableTransform, progress)
}
@Composable
-fun rememberTransform(shapeGroup: ShapeGroup): ComposeLottieTransform {
+fun rememberTransform(shapeGroup: ShapeGroup, progress: Float): ComposeLottieTransform {
val animatableTransform = remember(shapeGroup) { shapeGroup.items.lastOrNull() as? AnimatableTransform }
- return rememberTransform(animatableTransform)
+ return rememberTransform(animatableTransform, progress)
}
@Composable
-fun rememberTransform(transform: AnimatableTransform?): ComposeLottieTransform {
+fun rememberTransform(transform: AnimatableTransform?, progress: Float): ComposeLottieTransform {
+ // TODO: look into optimizations here.
val composeTransform = remember(transform) { ComposeLottieTransform(transform) }
- composeTransform.progress = LocalLottieProgress.current
+ composeTransform.progress = progress
return composeTransform
}
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 58b0222..6820d14 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
@@ -2,16 +2,15 @@
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.animation.core.*
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.Box
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.geometry.Rect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.*
+import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.vector.*
import androidx.compose.ui.unit.dp
import com.airbnb.lottie.LottieComposition
@@ -21,10 +20,9 @@
import com.airbnb.lottie.model.layer.Layer
import com.airbnb.lottie.value.Keyframe
import java.util.concurrent.TimeUnit
+import kotlin.math.ceil
import kotlin.math.floor
-val LocalLottieProgress = compositionLocalOf { 0f }
-
@Composable
fun ComposeLottieAnimation(
compositionResult: LottieCompositionResult,
@@ -60,33 +58,65 @@
}
}
- Providers(
- LocalLottieProgress provides state.progress
- ) {
+ Box(modifier = Modifier.then(modifier)) {
+ var matteLayer: Layer? = null
composition.layers.forEach { layer ->
when (layer.layerType) {
- Layer.LayerType.SHAPE -> Image(
- shapeLayerPainter(composition, layer),
- contentDescription = null,
- modifier = modifier,
- )
+ Layer.LayerType.SHAPE -> {
+ if (layer.matteType != Layer.MatteType.NONE) {
+ matteLayer = layer
+ } else {
+ ShapeLayer(composition, layer, matteLayer, state.progress)
+ }
+ }
else -> Unit
}
+ matteLayer = null
+ }
+ }
+}
+
+@Composable
+fun ShapeLayer(composition: LottieComposition, layer: Layer, matteLayer: Layer?, progress: Float) {
+ val layerPainter = shapeLayerPainter(composition, layer, progress)
+ val matteLayerPainter = if (matteLayer == null) null else shapeLayerPainter(composition, layer, progress)
+ Canvas(
+ modifier = Modifier
+ ) {
+ with(layerPainter) {
+ // TODO: cache this Size class.
+ draw(Size(composition.bounds.width().toFloat(), composition.bounds.height().toFloat()))
+ }
+ if (matteLayerPainter != null) {
+ drawIntoCanvas { canvas ->
+ // TODO: only save the right bounds.
+ val mattePaint = Paint()
+ mattePaint.blendMode = BlendMode.DstOut
+ canvas.withSaveLayer(
+ Rect(0f, 0f, composition.bounds.width().toFloat(), composition.bounds.height().toFloat()),
+ mattePaint,
+ ) {
+ with(matteLayerPainter) {
+ // TODO: cache this Size class.
+ draw(Size(composition.bounds.width().toFloat(), composition.bounds.height().toFloat()))
+ }
+ }
+ }
}
}
}
@Composable
-fun shapeLayerPainter(composition: LottieComposition, layer: Layer): VectorPainter {
- val transform = rememberTransform(layer)
+fun shapeLayerPainter(composition: LottieComposition, layer: Layer, progress: Float): VectorPainter {
+ val transform = rememberTransform(layer, progress)
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)}")
+ val clipPathData = getMaskPathData(layer.masks, progress)
+
Group(
name = layer.layerName,
translationX = transform.position.x,
@@ -97,8 +127,7 @@
) {
layer.shapes.forEach { shapeModel ->
when (shapeModel) {
- is ShapeGroup -> ComposeShapeGroup(shapeModel)
- else -> Log.d("Gabe", "Don't know how to draw ${shapeModel::class.simpleName}")
+ is ShapeGroup -> ComposeShapeGroup(shapeModel, progress)
}
}
}
@@ -106,10 +135,10 @@
}
@Composable
-fun ComposeShapeGroup(shapeGroup: ShapeGroup) {
+fun ComposeShapeGroup(shapeGroup: ShapeGroup, progress: Float) {
if (shapeGroup.isHidden || shapeGroup.items.isEmpty()) return
- val transform = rememberTransform(shapeGroup)
- val pathData = remember { mutableListOf<PathNode>() }
+ val transform = rememberTransform(shapeGroup, progress)
+ val pathData = remember { mutableStateListOf<PathNode>() }
Group(
name = shapeGroup.name,
translationX = transform.position.x,
@@ -117,18 +146,10 @@
) {
// 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)
+ pathData += rectanglePathData(model, progress)
}
is ShapeFill -> {
ComposeShapeFill(model, pathData)
@@ -139,9 +160,9 @@
}
@Composable
-fun rectanglePathData(shape: RectangleShape): List<PathNode> {
+fun rectanglePathData(shape: RectangleShape, progress: Float): List<PathNode> {
val size = remember { PointF() }
- val (sizeKeyframe, interpolatedProgress) = shape.size.keyframes.rememberKeyframeProgress()
+ val (sizeKeyframe, interpolatedProgress) = shape.size.keyframes.rememberKeyframeProgress(progress)
size.set(
lerp(sizeKeyframe?.startValue?.x ?: 0f, sizeKeyframe?.endValue?.x ?: 0f, interpolatedProgress),
lerp(sizeKeyframe?.startValue?.y ?: 0f, sizeKeyframe?.endValue?.y ?: 0f, interpolatedProgress),
@@ -172,7 +193,7 @@
}
@Composable
-fun getMaskPathData(masks: List<Mask>): List<PathNode> {
+fun getMaskPathData(masks: List<Mask>, progress: Float): List<PathNode> {
// TODO: figure out how to reuse the mutable list.
// https://issuetracker.google.com/issues/180774141
val pathNodes = mutableListOf<PathNode>()
@@ -180,12 +201,12 @@
pathNodes.clear()
for (i in masks.indices) {
val mask = masks[i]
- val (keyframe, progress) = mask.maskPath.keyframes.rememberKeyframeProgress()
+ val (keyframe, keyframeProgress) = mask.maskPath.keyframes.rememberKeyframeProgress(progress)
val shapeData = allShapeData[i]
val startData = keyframe?.startValue
val endData = keyframe?.endValue
if (startData != null && endData != null) {
- shapeData.interpolateBetween(startData, endData, progress)
+ shapeData.interpolateBetween(startData, endData, keyframeProgress)
pathNodes += PathData {
moveTo(shapeData.initialPoint.x, shapeData.initialPoint.y)
for (curveData in shapeData.curves) {
@@ -207,8 +228,7 @@
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
+fun <T> List<Keyframe<T>>.rememberKeyframeProgress(progress: Float): KeyframeProgress<T> {
val value = remember { KeyframeProgress<T>() }
val keyframe = firstOrNull { it.containsProgress(progress) } ?: return value
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/layer/BaseLayer.java b/lottie/src/main/java/com/airbnb/lottie/model/layer/BaseLayer.java
index f287299..d009f8f 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/layer/BaseLayer.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/layer/BaseLayer.java
@@ -146,6 +146,11 @@
invalidateSelf();
}
+ @Nullable
+ public BaseLayer getMatteLayer() {
+ return matteLayer;
+ }
+
Layer getLayerModel() {
return layerModel;
}
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 fe4e940..32abfe0 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
@@ -137,7 +137,7 @@
return layerName;
}
- MatteType getMatteType() {
+ public MatteType getMatteType() {
return matteType;
}
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/lottiefiles/LottieFilesSearchPage.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/lottiefiles/LottieFilesSearchPage.kt
index 37e48e2..17b70c4 100644
--- a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/lottiefiles/LottieFilesSearchPage.kt
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/lottiefiles/LottieFilesSearchPage.kt
@@ -84,7 +84,6 @@
if (state.currentPage >= state.lastPage) return@withState
fetchJob = viewModelScope.launch {
val response = try {
- Log.d("Gabe", "Fetching page ${state.currentPage + 1}")
api.search(state.query, state.currentPage + 1)
} catch (e: Exception) {
setState { copy(fetchException = true) }