From 0a00af326614940bfe6ff9efc710b67644b22cf8 Mon Sep 17 00:00:00 2001 From: Rebecca Franks Date: Fri, 3 Jan 2025 16:50:49 +0000 Subject: [PATCH 1/5] Add example of keyframesWithSplines --- .../snippets/animations/AnimationSnippets.kt | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt index 38a1eb44d..0f8f51be2 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt @@ -29,6 +29,7 @@ import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.AnimationVector1D import androidx.compose.animation.core.AnimationVector2D import androidx.compose.animation.core.Easing +import androidx.compose.animation.core.ExperimentalAnimationSpecApi import androidx.compose.animation.core.ExperimentalTransitionApi import androidx.compose.animation.core.FastOutLinearInEasing import androidx.compose.animation.core.FastOutSlowInEasing @@ -43,11 +44,13 @@ import androidx.compose.animation.core.TwoWayConverter import androidx.compose.animation.core.VectorConverter import androidx.compose.animation.core.animateDp import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.animateOffsetAsState import androidx.compose.animation.core.animateRect import androidx.compose.animation.core.animateValueAsState import androidx.compose.animation.core.createChildTransition import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.keyframes +import androidx.compose.animation.core.keyframesWithSpline import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.rememberTransition import androidx.compose.animation.core.repeatable @@ -71,11 +74,13 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.sizeIn @@ -93,25 +98,34 @@ import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.withFrameNanos import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.PointMode import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.boundsInParent +import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.round import com.example.compose.snippets.R import java.text.BreakIterator import java.text.StringCharacterIterator import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive /* * Copyright 2023 The Android Open Source Project @@ -709,6 +723,104 @@ private fun AnimationSpecKeyframe() { // [END android_compose_animations_spec_keyframe] } +@OptIn(ExperimentalAnimationSpecApi::class) +@Composable +private fun AnimationSpecKeyframeWithSpline() { + // [START android_compose_animation_spec_keyframes_with_spline] + val offset by animateOffsetAsState( + targetValue = Offset(300f, 300f), + animationSpec = keyframesWithSpline { + durationMillis = 6000 + Offset(0f, 0f) at 0 + Offset(150f, 200f) atFraction 0.5f + Offset(0f,100f) atFraction 0.7f + } + ) + // [END android_compose_animation_spec_keyframes_with_spline] +} + +@Suppress("PrimitiveInCollection") +@OptIn(ExperimentalAnimationSpecApi::class) +@Preview +@Composable +private fun OffsetKeyframeWithSplineDemo() { + val points = remember { mutableStateListOf() } + val offsetAnim = remember { + androidx.compose.animation.core.Animatable( + Offset.Zero, + Offset.VectorConverter + ) + } + val density = LocalDensity.current + + BoxWithConstraints( + Modifier.fillMaxSize().drawBehind { + drawPoints( + points = points, + pointMode = PointMode.Lines, + color = Color.LightGray, + strokeWidth = 4f, + pathEffect = PathEffect.dashPathEffect(floatArrayOf(30f, 20f)) + ) + } + ) { + val minDimension = minOf(maxWidth, maxHeight) + val size = minDimension / 4 + + val sizePx = with(density) { size.toPx() } + val widthPx = with(density) { maxWidth.toPx() } + val heightPx = with(density) { maxHeight.toPx() } + + val maxXOff = (widthPx - sizePx) / 2f + val maxYOff = heightPx - (sizePx / 2f) + + Box( + Modifier.align(Alignment.TopCenter) + .offset { offsetAnim.value.round() } + .size(size) + .background(Color.Red, RoundedCornerShape(50)) + .onPlaced { points.add(it.boundsInParent().center) } + ) + + LaunchedEffect(Unit) { + delay(1000) + while (isActive) { + offsetAnim.animateTo( + targetValue = Offset.Zero, + animationSpec = + keyframesWithSpline { + durationMillis = 4400 + + // Increasingly approach the halfway point moving from side to side + repeat(4) { + val i = it + 1 + val sign = if (i % 2 == 0) 1 else -1 + Offset( + x = maxXOff * (i.toFloat() / 5f) * sign, + y = (maxYOff) * (i.toFloat() / 5f) + ) atFraction (0.1f * i) + } + + // Halfway point (at bottom of the screen) + Offset(0f, maxYOff) atFraction 0.5f + + // Return with mirrored movement + repeat(4) { + val i = it + 1 + val sign = if (i % 2 == 0) 1 else -1 + Offset( + x = maxXOff * (1f - i.toFloat() / 5f) * sign, + y = (maxYOff) * (1f - i.toFloat() / 5f) + ) atFraction ((0.1f * i) + 0.5f) + } + } + ) + points.clear() + } + } + } +} + @Composable private fun AnimationSpecRepeatable() { // [START android_compose_animations_spec_repeatable] From 96bc06a7afe8de294600803e5eebe78b9bc7ace4 Mon Sep 17 00:00:00 2001 From: riggaroo Date: Fri, 3 Jan 2025 16:54:03 +0000 Subject: [PATCH 2/5] Apply Spotless --- .../snippets/animations/AnimationSnippets.kt | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt index 0f8f51be2..26f3b42ba 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt @@ -729,12 +729,12 @@ private fun AnimationSpecKeyframeWithSpline() { // [START android_compose_animation_spec_keyframes_with_spline] val offset by animateOffsetAsState( targetValue = Offset(300f, 300f), - animationSpec = keyframesWithSpline { - durationMillis = 6000 - Offset(0f, 0f) at 0 - Offset(150f, 200f) atFraction 0.5f - Offset(0f,100f) atFraction 0.7f - } + animationSpec = keyframesWithSpline { + durationMillis = 6000 + Offset(0f, 0f) at 0 + Offset(150f, 200f) atFraction 0.5f + Offset(0f, 100f) atFraction 0.7f + } ) // [END android_compose_animation_spec_keyframes_with_spline] } @@ -788,32 +788,32 @@ private fun OffsetKeyframeWithSplineDemo() { offsetAnim.animateTo( targetValue = Offset.Zero, animationSpec = - keyframesWithSpline { - durationMillis = 4400 - - // Increasingly approach the halfway point moving from side to side - repeat(4) { - val i = it + 1 - val sign = if (i % 2 == 0) 1 else -1 - Offset( - x = maxXOff * (i.toFloat() / 5f) * sign, - y = (maxYOff) * (i.toFloat() / 5f) - ) atFraction (0.1f * i) - } + keyframesWithSpline { + durationMillis = 4400 + + // Increasingly approach the halfway point moving from side to side + repeat(4) { + val i = it + 1 + val sign = if (i % 2 == 0) 1 else -1 + Offset( + x = maxXOff * (i.toFloat() / 5f) * sign, + y = (maxYOff) * (i.toFloat() / 5f) + ) atFraction (0.1f * i) + } - // Halfway point (at bottom of the screen) - Offset(0f, maxYOff) atFraction 0.5f - - // Return with mirrored movement - repeat(4) { - val i = it + 1 - val sign = if (i % 2 == 0) 1 else -1 - Offset( - x = maxXOff * (1f - i.toFloat() / 5f) * sign, - y = (maxYOff) * (1f - i.toFloat() / 5f) - ) atFraction ((0.1f * i) + 0.5f) - } + // Halfway point (at bottom of the screen) + Offset(0f, maxYOff) atFraction 0.5f + + // Return with mirrored movement + repeat(4) { + val i = it + 1 + val sign = if (i % 2 == 0) 1 else -1 + Offset( + x = maxXOff * (1f - i.toFloat() / 5f) * sign, + y = (maxYOff) * (1f - i.toFloat() / 5f) + ) atFraction ((0.1f * i) + 0.5f) } + } ) points.clear() } From f2a014a9bb913207697c2e39a18268525d6c2fdd Mon Sep 17 00:00:00 2001 From: Rebecca Franks Date: Mon, 6 Jan 2025 10:48:22 +0000 Subject: [PATCH 3/5] PR Review comments. --- .../compose/snippets/animations/AnimationSnippets.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt index 26f3b42ba..d2bef5efb 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt @@ -19,6 +19,7 @@ package com.example.compose.snippets.animations import androidx.compose.animation.Animatable +import androidx.compose.animation.core.Animatable import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.Crossfade @@ -739,7 +740,6 @@ private fun AnimationSpecKeyframeWithSpline() { // [END android_compose_animation_spec_keyframes_with_spline] } -@Suppress("PrimitiveInCollection") @OptIn(ExperimentalAnimationSpecApi::class) @Preview @Composable @@ -792,8 +792,7 @@ private fun OffsetKeyframeWithSplineDemo() { durationMillis = 4400 // Increasingly approach the halfway point moving from side to side - repeat(4) { - val i = it + 1 + for (i in 0..4) { val sign = if (i % 2 == 0) 1 else -1 Offset( x = maxXOff * (i.toFloat() / 5f) * sign, @@ -805,8 +804,7 @@ private fun OffsetKeyframeWithSplineDemo() { Offset(0f, maxYOff) atFraction 0.5f // Return with mirrored movement - repeat(4) { - val i = it + 1 + for(i in 0..4) { val sign = if (i % 2 == 0) 1 else -1 Offset( x = maxXOff * (1f - i.toFloat() / 5f) * sign, From d615c02e201ed9175b648185ca80852df2b5670d Mon Sep 17 00:00:00 2001 From: Rebecca Franks Date: Mon, 6 Jan 2025 10:48:59 +0000 Subject: [PATCH 4/5] PR Review comments. --- .../example/compose/snippets/animations/AnimationSnippets.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt index d2bef5efb..1a26a0330 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt @@ -746,7 +746,7 @@ private fun AnimationSpecKeyframeWithSpline() { private fun OffsetKeyframeWithSplineDemo() { val points = remember { mutableStateListOf() } val offsetAnim = remember { - androidx.compose.animation.core.Animatable( + Animatable( Offset.Zero, Offset.VectorConverter ) From b0c9750fe653d460924dff879d9313143debf72a Mon Sep 17 00:00:00 2001 From: riggaroo Date: Mon, 6 Jan 2025 10:51:56 +0000 Subject: [PATCH 5/5] Apply Spotless --- .../example/compose/snippets/animations/AnimationSnippets.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt index 1a26a0330..2fe06cf06 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt @@ -19,7 +19,6 @@ package com.example.compose.snippets.animations import androidx.compose.animation.Animatable -import androidx.compose.animation.core.Animatable import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.Crossfade @@ -27,6 +26,7 @@ import androidx.compose.animation.EnterExitState import androidx.compose.animation.SizeTransform import androidx.compose.animation.animateColor import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationVector1D import androidx.compose.animation.core.AnimationVector2D import androidx.compose.animation.core.Easing @@ -804,7 +804,7 @@ private fun OffsetKeyframeWithSplineDemo() { Offset(0f, maxYOff) atFraction 0.5f // Return with mirrored movement - for(i in 0..4) { + for (i in 0..4) { val sign = if (i % 2 == 0) 1 else -1 Offset( x = maxXOff * (1f - i.toFloat() / 5f) * sign,