@@ -14,14 +14,21 @@ import androidx.compose.foundation.layout.fillMaxWidth
1414import androidx.compose.foundation.layout.height
1515import androidx.compose.foundation.layout.padding
1616import androidx.compose.foundation.layout.size
17+ import androidx.compose.foundation.rememberScrollState
18+ import androidx.compose.foundation.shape.CircleShape
19+ import androidx.compose.foundation.verticalScroll
1720import androidx.compose.material.Button
1821import androidx.compose.material.Icon
1922import androidx.compose.material.Text
2023import androidx.compose.material.icons.Icons
2124import androidx.compose.material.icons.filled.ScreenRotation
2225import androidx.compose.runtime.Composable
26+ import androidx.compose.runtime.LaunchedEffect
27+ import androidx.compose.runtime.getValue
28+ import androidx.compose.runtime.mutableIntStateOf
2329import androidx.compose.runtime.remember
2430import androidx.compose.runtime.rememberCoroutineScope
31+ import androidx.compose.runtime.setValue
2532import androidx.compose.ui.Alignment
2633import androidx.compose.ui.Modifier
2734import androidx.compose.ui.draw.rotate
@@ -31,6 +38,7 @@ import androidx.compose.ui.graphics.graphicsLayer
3138import androidx.compose.ui.tooling.preview.Preview
3239import androidx.compose.ui.unit.dp
3340import androidx.compose.ui.unit.sp
41+ import kotlinx.coroutines.CancellationException
3442import kotlinx.coroutines.CoroutineScope
3543import kotlinx.coroutines.isActive
3644import kotlinx.coroutines.launch
@@ -45,7 +53,194 @@ fun Tutorial6_33Screen() {
4553
4654@Composable
4755private fun TutorialContent () {
48- StoppableInfiniteAnimationSample ()
56+ Column (
57+ modifier = Modifier .fillMaxSize().verticalScroll(rememberScrollState())
58+ ) {
59+ InfiniteRotationInterruptionSample ()
60+ Spacer (Modifier .height(16 .dp))
61+ StoppableInfiniteAnimationSample ()
62+ }
63+ }
64+
65+ @Preview
66+ @Composable
67+ fun AnimatableIntteruptionSample () {
68+ Column (
69+ modifier = Modifier .fillMaxSize().padding(16 .dp),
70+ horizontalAlignment = Alignment .CenterHorizontally
71+ ) {
72+
73+ val animatable = remember {
74+ Animatable (0f )
75+ }
76+
77+ val coroutineScope = rememberCoroutineScope()
78+
79+ Canvas (
80+ modifier = Modifier .size(100 .dp).rotate(animatable.value)
81+ .border(2 .dp, Color .Green , CircleShape )
82+ ) {
83+
84+ drawLine(
85+ start = center,
86+ end = Offset (center.x, 0f ),
87+ color = Color .Red ,
88+ strokeWidth = 4 .dp.toPx()
89+ )
90+ }
91+
92+ Text (
93+ " animatable2: ${animatable.value.toInt()} \n "
94+ )
95+
96+ Button (
97+ modifier = Modifier .fillMaxWidth(),
98+ onClick = {
99+ coroutineScope.launch {
100+ try {
101+ val result = animatable.animateTo(
102+ targetValue = 360f ,
103+ animationSpec = tween(durationMillis = 4000 , easing = LinearEasing )
104+ )
105+
106+ println (" Result: $result " )
107+ } catch (e: CancellationException ) {
108+ println (" Exception: ${e.message} " )
109+ }
110+
111+ }
112+ }
113+ ) {
114+ Text (" Animate to 360" )
115+ }
116+
117+ Button (
118+ modifier = Modifier .fillMaxWidth(),
119+ onClick = {
120+ coroutineScope.launch {
121+ animatable.snapTo(350f )
122+ }
123+ }
124+ ) {
125+ Text (" Snap to 350" )
126+ }
127+
128+ Button (
129+ modifier = Modifier .fillMaxWidth(),
130+ onClick = {
131+ coroutineScope.launch {
132+ animatable.snapTo(0f )
133+ }
134+ }
135+ ) {
136+ Text (" Snap to 0" )
137+ }
138+ }
139+ }
140+
141+ @Preview
142+ @Composable
143+ fun InfiniteRotationInterruptionSample () {
144+
145+ var animationDuration by remember { mutableIntStateOf(2000 ) }
146+
147+ val animatable = remember {
148+ Animatable (0f )
149+ }
150+
151+ val animatable2 = remember {
152+ Animatable (0f )
153+ }
154+
155+ LaunchedEffect (animationDuration) {
156+ while (isActive) {
157+ try {
158+ animatable.animateTo(
159+ targetValue = 360f ,
160+ animationSpec = tween(animationDuration, easing = LinearEasing )
161+ )
162+ } catch (e: CancellationException ) {
163+ println (" Animation canceled with: $e " )
164+ }
165+
166+ if (animatable.value >= 360f ) {
167+ animatable.snapTo(targetValue = 0f )
168+ }
169+ }
170+ }
171+
172+ LaunchedEffect (animationDuration) {
173+ while (isActive) {
174+ val currentValue = animatable2.value
175+ try {
176+ animatable2.animateTo(
177+ targetValue = 360f ,
178+ animationSpec = tween((animationDuration * (360f - currentValue) / 360f ).toInt(), easing = LinearEasing )
179+ )
180+
181+ } catch (e: CancellationException ) {
182+ println (" Animation2 canceled with: $e " )
183+ }
184+
185+ if (animatable2.value >= 360f ) {
186+ animatable2.snapTo(targetValue = 0f )
187+ }
188+ }
189+ }
190+
191+ Column (
192+ verticalArrangement = Arrangement .spacedBy(8 .dp),
193+ horizontalAlignment = Alignment .CenterHorizontally
194+ ) {
195+
196+ Text (" Default Animatable Behaviour" , fontSize = 24 .sp)
197+ Canvas (
198+ modifier = Modifier .size(100 .dp).rotate(animatable.value)
199+ .border(2 .dp, Color .Green , CircleShape )
200+ ) {
201+
202+ drawLine(
203+ start = center,
204+ end = Offset (center.x, 0f ),
205+ color = Color .Red ,
206+ strokeWidth = 4 .dp.toPx()
207+ )
208+ }
209+
210+ Text (
211+ " animatable2: ${animatable.value.toInt()} \n " +
212+ " animationDuration: $animationDuration "
213+ )
214+
215+ Text (" Adjust duration after Interruption" , fontSize = 24 .sp)
216+
217+ Canvas (
218+ modifier = Modifier .size(100 .dp).rotate(animatable2.value)
219+ .border(2 .dp, Color .Green , CircleShape )
220+ ) {
221+
222+ drawLine(
223+ start = center,
224+ end = Offset (center.x, 0f ),
225+ color = Color .Red ,
226+ strokeWidth = 4 .dp.toPx()
227+ )
228+ }
229+
230+ Text (
231+ " animatable: ${animatable2.value.toInt()} \n " +
232+ " animationDuration: $animationDuration "
233+ )
234+
235+ Button (
236+ modifier = Modifier .padding(horizontal = 16 .dp).fillMaxWidth(),
237+ onClick = {
238+ animationDuration + = 4000
239+ }
240+ ) {
241+ Text (" Change duration" )
242+ }
243+ }
49244}
50245
51246@Preview
@@ -64,6 +259,9 @@ private fun StoppableInfiniteAnimationSample() {
64259 modifier = Modifier .fillMaxSize()
65260 ) {
66261
262+
263+ Text (" Infinite Stoppable Animatable" , fontSize = 24 .sp)
264+
67265 Row (
68266 modifier = Modifier .fillMaxWidth().padding(16 .dp),
69267 verticalAlignment = Alignment .CenterVertically ,
@@ -72,8 +270,9 @@ private fun StoppableInfiniteAnimationSample() {
72270 Text (" Text1" )
73271 Column {
74272 Canvas (
75- modifier = Modifier .size(100 .dp).rotate(rotateAnimationState.angle)
76- .border(2 .dp, Color .Green )
273+ modifier = Modifier .size(100 .dp)
274+ .rotate(rotateAnimationState.angle)
275+ .border(2 .dp, Color .Green , CircleShape )
77276 ) {
78277
79278 drawLine(
@@ -134,7 +333,6 @@ class RotateAnimationState(
134333 get() = animatable.value
135334
136335 private val animatable = Animatable (0f )
137- private val durationPerAngle = duration / 360f
138336
139337 var rotationStatus: RotationStatus = RotationStatus .Idle
140338
@@ -167,10 +365,10 @@ class RotateAnimationState(
167365 coroutineScope.launch {
168366 rotationStatus = RotationStatus .Stopping
169367 val currentValue = animatable.value
368+ val durationPerAngle = duration / 360f
170369 // Duration depends on how far current angle is to 360f
171370 // total duration is duration per angle multiplied with total angles to rotate
172371 val durationToZero = (durationPerAngle * (360 - currentValue)).toInt()
173- animatable.snapTo(currentValue)
174372 animatable.animateTo(
175373 targetValue = 360f ,
176374 tween(
0 commit comments