@@ -26,9 +26,11 @@ import androidx.compose.animation.EnterExitState
2626import androidx.compose.animation.SizeTransform
2727import androidx.compose.animation.animateColor
2828import androidx.compose.animation.animateContentSize
29+ import androidx.compose.animation.core.Animatable
2930import androidx.compose.animation.core.AnimationVector1D
3031import androidx.compose.animation.core.AnimationVector2D
3132import androidx.compose.animation.core.Easing
33+ import androidx.compose.animation.core.ExperimentalAnimationSpecApi
3234import androidx.compose.animation.core.ExperimentalTransitionApi
3335import androidx.compose.animation.core.FastOutLinearInEasing
3436import androidx.compose.animation.core.FastOutSlowInEasing
@@ -43,11 +45,13 @@ import androidx.compose.animation.core.TwoWayConverter
4345import androidx.compose.animation.core.VectorConverter
4446import androidx.compose.animation.core.animateDp
4547import androidx.compose.animation.core.animateFloatAsState
48+ import androidx.compose.animation.core.animateOffsetAsState
4649import androidx.compose.animation.core.animateRect
4750import androidx.compose.animation.core.animateValueAsState
4851import androidx.compose.animation.core.createChildTransition
4952import androidx.compose.animation.core.infiniteRepeatable
5053import androidx.compose.animation.core.keyframes
54+ import androidx.compose.animation.core.keyframesWithSpline
5155import androidx.compose.animation.core.rememberInfiniteTransition
5256import androidx.compose.animation.core.rememberTransition
5357import androidx.compose.animation.core.repeatable
@@ -71,11 +75,13 @@ import androidx.compose.foundation.Image
7175import androidx.compose.foundation.background
7276import androidx.compose.foundation.clickable
7377import androidx.compose.foundation.layout.Box
78+ import androidx.compose.foundation.layout.BoxWithConstraints
7479import androidx.compose.foundation.layout.Column
7580import androidx.compose.foundation.layout.Row
7681import androidx.compose.foundation.layout.fillMaxSize
7782import androidx.compose.foundation.layout.fillMaxWidth
7883import androidx.compose.foundation.layout.height
84+ import androidx.compose.foundation.layout.offset
7985import androidx.compose.foundation.layout.padding
8086import androidx.compose.foundation.layout.size
8187import androidx.compose.foundation.layout.sizeIn
@@ -93,25 +99,34 @@ import androidx.compose.runtime.State
9399import androidx.compose.runtime.getValue
94100import androidx.compose.runtime.mutableIntStateOf
95101import androidx.compose.runtime.mutableLongStateOf
102+ import androidx.compose.runtime.mutableStateListOf
96103import androidx.compose.runtime.mutableStateOf
97104import androidx.compose.runtime.remember
98105import androidx.compose.runtime.setValue
99106import androidx.compose.runtime.withFrameNanos
100107import androidx.compose.ui.Alignment
101108import androidx.compose.ui.Modifier
109+ import androidx.compose.ui.draw.drawBehind
110+ import androidx.compose.ui.geometry.Offset
102111import androidx.compose.ui.geometry.Rect
103112import androidx.compose.ui.graphics.Color
113+ import androidx.compose.ui.graphics.PathEffect
114+ import androidx.compose.ui.graphics.PointMode
104115import androidx.compose.ui.graphics.graphicsLayer
105116import androidx.compose.ui.layout.ContentScale
117+ import androidx.compose.ui.layout.boundsInParent
118+ import androidx.compose.ui.layout.onPlaced
106119import androidx.compose.ui.platform.LocalDensity
107120import androidx.compose.ui.tooling.preview.Preview
108121import androidx.compose.ui.unit.Dp
109122import androidx.compose.ui.unit.IntSize
110123import androidx.compose.ui.unit.dp
124+ import androidx.compose.ui.unit.round
111125import com.example.compose.snippets.R
112126import java.text.BreakIterator
113127import java.text.StringCharacterIterator
114128import kotlinx.coroutines.delay
129+ import kotlinx.coroutines.isActive
115130
116131/*
117132* Copyright 2023 The Android Open Source Project
@@ -709,6 +724,101 @@ private fun AnimationSpecKeyframe() {
709724 // [END android_compose_animations_spec_keyframe]
710725}
711726
727+ @OptIn(ExperimentalAnimationSpecApi ::class )
728+ @Composable
729+ private fun AnimationSpecKeyframeWithSpline () {
730+ // [START android_compose_animation_spec_keyframes_with_spline]
731+ val offset by animateOffsetAsState(
732+ targetValue = Offset (300f , 300f ),
733+ animationSpec = keyframesWithSpline {
734+ durationMillis = 6000
735+ Offset (0f , 0f ) at 0
736+ Offset (150f , 200f ) atFraction 0.5f
737+ Offset (0f , 100f ) atFraction 0.7f
738+ }
739+ )
740+ // [END android_compose_animation_spec_keyframes_with_spline]
741+ }
742+
743+ @OptIn(ExperimentalAnimationSpecApi ::class )
744+ @Preview
745+ @Composable
746+ private fun OffsetKeyframeWithSplineDemo () {
747+ val points = remember { mutableStateListOf<Offset >() }
748+ val offsetAnim = remember {
749+ 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+ for (i in 0 .. 4 ) {
796+ val sign = if (i % 2 == 0 ) 1 else - 1
797+ Offset (
798+ x = maxXOff * (i.toFloat() / 5f ) * sign,
799+ y = (maxYOff) * (i.toFloat() / 5f )
800+ ) atFraction (0.1f * i)
801+ }
802+
803+ // Halfway point (at bottom of the screen)
804+ Offset (0f , maxYOff) atFraction 0.5f
805+
806+ // Return with mirrored movement
807+ for (i in 0 .. 4 ) {
808+ val sign = if (i % 2 == 0 ) 1 else - 1
809+ Offset (
810+ x = maxXOff * (1f - i.toFloat() / 5f ) * sign,
811+ y = (maxYOff) * (1f - i.toFloat() / 5f )
812+ ) atFraction ((0.1f * i) + 0.5f )
813+ }
814+ }
815+ )
816+ points.clear()
817+ }
818+ }
819+ }
820+ }
821+
712822@Composable
713823private fun AnimationSpecRepeatable () {
714824 // [START android_compose_animations_spec_repeatable]
0 commit comments