@@ -29,6 +29,7 @@ import androidx.compose.animation.animateContentSize
2929import androidx.compose.animation.core.AnimationVector1D
3030import androidx.compose.animation.core.AnimationVector2D
3131import androidx.compose.animation.core.Easing
32+ import androidx.compose.animation.core.ExperimentalAnimationSpecApi
3233import androidx.compose.animation.core.ExperimentalTransitionApi
3334import androidx.compose.animation.core.FastOutLinearInEasing
3435import androidx.compose.animation.core.FastOutSlowInEasing
@@ -43,11 +44,13 @@ import androidx.compose.animation.core.TwoWayConverter
4344import androidx.compose.animation.core.VectorConverter
4445import androidx.compose.animation.core.animateDp
4546import androidx.compose.animation.core.animateFloatAsState
47+ import androidx.compose.animation.core.animateOffsetAsState
4648import androidx.compose.animation.core.animateRect
4749import androidx.compose.animation.core.animateValueAsState
4850import androidx.compose.animation.core.createChildTransition
4951import androidx.compose.animation.core.infiniteRepeatable
5052import androidx.compose.animation.core.keyframes
53+ import androidx.compose.animation.core.keyframesWithSpline
5154import androidx.compose.animation.core.rememberInfiniteTransition
5255import androidx.compose.animation.core.rememberTransition
5356import androidx.compose.animation.core.repeatable
@@ -71,11 +74,13 @@ import androidx.compose.foundation.Image
7174import androidx.compose.foundation.background
7275import androidx.compose.foundation.clickable
7376import androidx.compose.foundation.layout.Box
77+ import androidx.compose.foundation.layout.BoxWithConstraints
7478import androidx.compose.foundation.layout.Column
7579import androidx.compose.foundation.layout.Row
7680import androidx.compose.foundation.layout.fillMaxSize
7781import androidx.compose.foundation.layout.fillMaxWidth
7882import androidx.compose.foundation.layout.height
83+ import androidx.compose.foundation.layout.offset
7984import androidx.compose.foundation.layout.padding
8085import androidx.compose.foundation.layout.size
8186import androidx.compose.foundation.layout.sizeIn
@@ -93,25 +98,34 @@ import androidx.compose.runtime.State
9398import androidx.compose.runtime.getValue
9499import androidx.compose.runtime.mutableIntStateOf
95100import androidx.compose.runtime.mutableLongStateOf
101+ import androidx.compose.runtime.mutableStateListOf
96102import androidx.compose.runtime.mutableStateOf
97103import androidx.compose.runtime.remember
98104import androidx.compose.runtime.setValue
99105import androidx.compose.runtime.withFrameNanos
100106import androidx.compose.ui.Alignment
101107import androidx.compose.ui.Modifier
108+ import androidx.compose.ui.draw.drawBehind
109+ import androidx.compose.ui.geometry.Offset
102110import androidx.compose.ui.geometry.Rect
103111import androidx.compose.ui.graphics.Color
112+ import androidx.compose.ui.graphics.PathEffect
113+ import androidx.compose.ui.graphics.PointMode
104114import androidx.compose.ui.graphics.graphicsLayer
105115import androidx.compose.ui.layout.ContentScale
116+ import androidx.compose.ui.layout.boundsInParent
117+ import androidx.compose.ui.layout.onPlaced
106118import androidx.compose.ui.platform.LocalDensity
107119import androidx.compose.ui.tooling.preview.Preview
108120import androidx.compose.ui.unit.Dp
109121import androidx.compose.ui.unit.IntSize
110122import androidx.compose.ui.unit.dp
123+ import androidx.compose.ui.unit.round
111124import com.example.compose.snippets.R
112125import java.text.BreakIterator
113126import java.text.StringCharacterIterator
114127import kotlinx.coroutines.delay
128+ import kotlinx.coroutines.isActive
115129
116130/*
117131* Copyright 2023 The Android Open Source Project
@@ -709,6 +723,104 @@ private fun AnimationSpecKeyframe() {
709723 // [END android_compose_animations_spec_keyframe]
710724}
711725
726+ @OptIn(ExperimentalAnimationSpecApi ::class )
727+ @Composable
728+ private fun AnimationSpecKeyframeWithSpline () {
729+ // [START android_compose_animation_spec_keyframes_with_spline]
730+ val offset by animateOffsetAsState(
731+ targetValue = Offset (300f , 300f ),
732+ animationSpec = keyframesWithSpline {
733+ durationMillis = 6000
734+ Offset (0f , 0f ) at 0
735+ Offset (150f , 200f ) atFraction 0.5f
736+ Offset (0f ,100f ) atFraction 0.7f
737+ }
738+ )
739+ // [END android_compose_animation_spec_keyframes_with_spline]
740+ }
741+
742+ @Suppress(" PrimitiveInCollection" )
743+ @OptIn(ExperimentalAnimationSpecApi ::class )
744+ @Preview
745+ @Composable
746+ private fun OffsetKeyframeWithSplineDemo () {
747+ val points = remember { mutableStateListOf<Offset >() }
748+ val offsetAnim = remember {
749+ androidx.compose.animation.core.Animatable (
750+ Offset .Zero ,
751+ Offset .VectorConverter
752+ )
753+ }
754+ val density = LocalDensity .current
755+
756+ BoxWithConstraints (
757+ Modifier .fillMaxSize().drawBehind {
758+ drawPoints(
759+ points = points,
760+ pointMode = PointMode .Lines ,
761+ color = Color .LightGray ,
762+ strokeWidth = 4f ,
763+ pathEffect = PathEffect .dashPathEffect(floatArrayOf(30f , 20f ))
764+ )
765+ }
766+ ) {
767+ val minDimension = minOf(maxWidth, maxHeight)
768+ val size = minDimension / 4
769+
770+ val sizePx = with (density) { size.toPx() }
771+ val widthPx = with (density) { maxWidth.toPx() }
772+ val heightPx = with (density) { maxHeight.toPx() }
773+
774+ val maxXOff = (widthPx - sizePx) / 2f
775+ val maxYOff = heightPx - (sizePx / 2f )
776+
777+ Box (
778+ Modifier .align(Alignment .TopCenter )
779+ .offset { offsetAnim.value.round() }
780+ .size(size)
781+ .background(Color .Red , RoundedCornerShape (50 ))
782+ .onPlaced { points.add(it.boundsInParent().center) }
783+ )
784+
785+ LaunchedEffect (Unit ) {
786+ delay(1000 )
787+ while (isActive) {
788+ offsetAnim.animateTo(
789+ targetValue = Offset .Zero ,
790+ animationSpec =
791+ keyframesWithSpline {
792+ durationMillis = 4400
793+
794+ // Increasingly approach the halfway point moving from side to side
795+ repeat(4 ) {
796+ val i = it + 1
797+ val sign = if (i % 2 == 0 ) 1 else - 1
798+ Offset (
799+ x = maxXOff * (i.toFloat() / 5f ) * sign,
800+ y = (maxYOff) * (i.toFloat() / 5f )
801+ ) atFraction (0.1f * i)
802+ }
803+
804+ // Halfway point (at bottom of the screen)
805+ Offset (0f , maxYOff) atFraction 0.5f
806+
807+ // Return with mirrored movement
808+ repeat(4 ) {
809+ val i = it + 1
810+ val sign = if (i % 2 == 0 ) 1 else - 1
811+ Offset (
812+ x = maxXOff * (1f - i.toFloat() / 5f ) * sign,
813+ y = (maxYOff) * (1f - i.toFloat() / 5f )
814+ ) atFraction ((0.1f * i) + 0.5f )
815+ }
816+ }
817+ )
818+ points.clear()
819+ }
820+ }
821+ }
822+ }
823+
712824@Composable
713825private fun AnimationSpecRepeatable () {
714826 // [START android_compose_animations_spec_repeatable]
0 commit comments