Skip to content

Instantly share code, notes, and snippets.

@vganin
Last active March 16, 2023 15:34
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save vganin/2f5d636a968e39841421310514fadfec to your computer and use it in GitHub Desktop.
Save vganin/2f5d636a968e39841421310514fadfec to your computer and use it in GitHub Desktop.
Jetpack Compose path morphing animation
@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].javaClass != to[i].javaClass) {
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 PlayPauseButton() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
var isPlaying by remember { mutableStateOf(false) }
FloatingActionButton(onClick = { isPlaying = !isPlaying }) {
// Paths should be able to morph. Otherwise no animation will play. Fix your paths using https://shapeshifter.design/
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"
}
)
val imageVector by derivedStateOf {
ImageVector.Builder(defaultWidth = 24.dp, defaultHeight = 24.dp, viewportWidth = 48f, viewportHeight = 48f)
.addPath(pathData = pathData, fill = SolidColor(Color.White))
.build()
}
Icon(
imageVector = imageVector,
contentDescription = null
)
}
}
}
@Khazbs
Copy link

Khazbs commented Sep 9, 2022

Please note: this requires implementation "androidx.compose.ui:ui-util:$compose_version"

@sahar-android
Copy link

hi,I really need your code work correctly but at first fun {@composable
fun animatePathAsState(path: String): State<List> {
return animatePathAsState(remember(path) { addPathNodes(path) })
} }
error is Required:
String?
Found:
List

can you help me ?I think maybe I couldn't understand your proccess of your code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment