Coverage Summary for Class: PathMorphingKt (com.vsevolodganin.clicktrack.utils.compose)
Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
PathMorphingKt |
0%
(0/15)
|
2%
(1/50)
|
0%
(0/137)
|
0%
(0/1135)
|
PathMorphingKt$animatePathAsState$2$1 |
0%
(0/1)
|
0%
(0/2)
|
0%
(0/5)
|
0%
(0/50)
|
Total |
0%
(0/16)
|
1.9%
(1/52)
|
0%
(0/142)
|
0%
(0/1185)
|
package com.vsevolodganin.clicktrack.utils.compose
import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
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.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.PathNode
import androidx.compose.ui.graphics.vector.addPathNodes
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp
import com.vsevolodganin.clicktrack.ui.piece.FloatingActionButton
@Composable
fun animatePathAsState(path: String): State<List<PathNode>> {
return animatePathAsState(remember(path) { addPathNodes(path) })
}
@Composable
fun animatePathAsState(path: List<PathNode>): State<List<PathNode>> {
var from by remember { mutableStateOf(path) }
var to by remember { mutableStateOf(path) }
val fraction = remember { Animatable(0f) }
LaunchedEffect(path) {
if (to != path) {
from = to
to = path
fraction.snapTo(0f)
fraction.animateTo(1f)
}
}
return remember {
derivedStateOf {
if (canMorph(from, to)) {
lerp(from, to, fraction.value)
} else {
to
}
}
}
}
// Paths can morph if same size and same node types at same positions.
fun canMorph(from: List<PathNode>, to: List<PathNode>): Boolean {
if (from.size != to.size) {
return false
}
for (i in from.indices) {
if (from[i]::class != to[i]::class) {
return false
}
}
return true
}
// Assume paths can morph (see [canMorph]). If not, will throw.
private fun lerp(fromPath: List<PathNode>, toPath: List<PathNode>, fraction: Float): List<PathNode> {
return fromPath.mapIndexed { i, from ->
val to = toPath[i]
lerp(from, to, fraction)
}
}
private fun lerp(from: PathNode, to: PathNode, fraction: Float): PathNode {
return when (from) {
PathNode.Close -> {
to as PathNode.Close
from
}
is PathNode.RelativeMoveTo -> {
to as PathNode.RelativeMoveTo
PathNode.RelativeMoveTo(
lerp(from.dx, to.dx, fraction),
lerp(from.dy, to.dy, fraction),
)
}
is PathNode.MoveTo -> {
to as PathNode.MoveTo
PathNode.MoveTo(
lerp(from.x, to.x, fraction),
lerp(from.y, to.y, fraction),
)
}
is PathNode.RelativeLineTo -> {
to as PathNode.RelativeLineTo
PathNode.RelativeLineTo(
lerp(from.dx, to.dx, fraction),
lerp(from.dy, to.dy, fraction),
)
}
is PathNode.LineTo -> {
to as PathNode.LineTo
PathNode.LineTo(
lerp(from.x, to.x, fraction),
lerp(from.y, to.y, fraction),
)
}
is PathNode.RelativeHorizontalTo -> {
to as PathNode.RelativeHorizontalTo
PathNode.RelativeHorizontalTo(
lerp(from.dx, to.dx, fraction),
)
}
is PathNode.HorizontalTo -> {
to as PathNode.HorizontalTo
PathNode.HorizontalTo(
lerp(from.x, to.x, fraction),
)
}
is PathNode.RelativeVerticalTo -> {
to as PathNode.RelativeVerticalTo
PathNode.RelativeVerticalTo(
lerp(from.dy, to.dy, fraction),
)
}
is PathNode.VerticalTo -> {
to as PathNode.VerticalTo
PathNode.VerticalTo(
lerp(from.y, to.y, fraction),
)
}
is PathNode.RelativeCurveTo -> {
to as PathNode.RelativeCurveTo
PathNode.RelativeCurveTo(
lerp(from.dx1, to.dx1, fraction),
lerp(from.dy1, to.dy1, fraction),
lerp(from.dx2, to.dx2, fraction),
lerp(from.dy2, to.dy2, fraction),
lerp(from.dx3, to.dx3, fraction),
lerp(from.dy3, to.dy3, fraction),
)
}
is PathNode.CurveTo -> {
to as PathNode.CurveTo
PathNode.CurveTo(
lerp(from.x1, to.x1, fraction),
lerp(from.y1, to.y1, fraction),
lerp(from.x2, to.x2, fraction),
lerp(from.y2, to.y2, fraction),
lerp(from.x3, to.x3, fraction),
lerp(from.y3, to.y3, fraction),
)
}
is PathNode.RelativeReflectiveCurveTo -> {
to as PathNode.RelativeReflectiveCurveTo
PathNode.RelativeReflectiveCurveTo(
lerp(from.dx1, to.dx1, fraction),
lerp(from.dy1, to.dy1, fraction),
lerp(from.dx2, to.dx2, fraction),
lerp(from.dy2, to.dy2, fraction),
)
}
is PathNode.ReflectiveCurveTo -> {
to as PathNode.ReflectiveCurveTo
PathNode.ReflectiveCurveTo(
lerp(from.x1, to.x1, fraction),
lerp(from.y1, to.y1, fraction),
lerp(from.x2, to.x2, fraction),
lerp(from.y2, to.y2, fraction),
)
}
is PathNode.RelativeQuadTo -> {
to as PathNode.RelativeQuadTo
PathNode.RelativeQuadTo(
lerp(from.dx1, to.dx1, fraction),
lerp(from.dy1, to.dy1, fraction),
lerp(from.dx2, to.dx2, fraction),
lerp(from.dy2, to.dy2, fraction),
)
}
is PathNode.QuadTo -> {
to as PathNode.QuadTo
PathNode.QuadTo(
lerp(from.x1, to.x1, fraction),
lerp(from.y1, to.y1, fraction),
lerp(from.x2, to.x2, fraction),
lerp(from.y2, to.y2, fraction),
)
}
is PathNode.RelativeReflectiveQuadTo -> {
to as PathNode.RelativeReflectiveQuadTo
PathNode.RelativeReflectiveQuadTo(
lerp(from.dx, to.dx, fraction),
lerp(from.dy, to.dy, fraction),
)
}
is PathNode.ReflectiveQuadTo -> {
to as PathNode.ReflectiveQuadTo
PathNode.ReflectiveQuadTo(
lerp(from.x, to.x, fraction),
lerp(from.y, to.y, fraction),
)
}
is PathNode.RelativeArcTo -> TODO("Support for RelativeArcTo not implemented yet")
is PathNode.ArcTo -> TODO("Support for ArcTo not implemented yet")
}
}
@Preview
@Composable
private fun PreviewPathMorphing() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
var isPlaying by remember { mutableStateOf(false) }
FloatingActionButton(onClick = { isPlaying = !isPlaying }) {
val pathData by animatePathAsState(
if (isPlaying) {
"M 10 38 L 10 10 L 21.75 10 L 21.75 38 L 10 38 M 26.25 38 L 26.25 10 L 38 10 L 38 38 L 26.25 38"
} else {
"M 16 9.85 L 38 23.85 L 38 23.85 L 16 23.957 L 16 9.85 M 16 23.957 L 38 23.85 L 38 23.85 L 16 37.85 L 16 23.957"
},
)
Icon(
imageVector = ImageVector.Builder(defaultWidth = 24.dp, defaultHeight = 24.dp, viewportWidth = 48f, viewportHeight = 48f)
.addPath(pathData = pathData, fill = SolidColor(Color.White))
.build(),
contentDescription = null,
)
}
}
}