@@ -14,14 +14,21 @@ import androidx.compose.foundation.layout.fillMaxWidth
14
14
import androidx.compose.foundation.layout.height
15
15
import androidx.compose.foundation.layout.padding
16
16
import androidx.compose.foundation.layout.size
17
+ import androidx.compose.foundation.rememberScrollState
18
+ import androidx.compose.foundation.shape.CircleShape
19
+ import androidx.compose.foundation.verticalScroll
17
20
import androidx.compose.material.Button
18
21
import androidx.compose.material.Icon
19
22
import androidx.compose.material.Text
20
23
import androidx.compose.material.icons.Icons
21
24
import androidx.compose.material.icons.filled.ScreenRotation
22
25
import androidx.compose.runtime.Composable
26
+ import androidx.compose.runtime.LaunchedEffect
27
+ import androidx.compose.runtime.getValue
28
+ import androidx.compose.runtime.mutableIntStateOf
23
29
import androidx.compose.runtime.remember
24
30
import androidx.compose.runtime.rememberCoroutineScope
31
+ import androidx.compose.runtime.setValue
25
32
import androidx.compose.ui.Alignment
26
33
import androidx.compose.ui.Modifier
27
34
import androidx.compose.ui.draw.rotate
@@ -31,6 +38,7 @@ import androidx.compose.ui.graphics.graphicsLayer
31
38
import androidx.compose.ui.tooling.preview.Preview
32
39
import androidx.compose.ui.unit.dp
33
40
import androidx.compose.ui.unit.sp
41
+ import kotlinx.coroutines.CancellationException
34
42
import kotlinx.coroutines.CoroutineScope
35
43
import kotlinx.coroutines.isActive
36
44
import kotlinx.coroutines.launch
@@ -45,7 +53,194 @@ fun Tutorial6_33Screen() {
45
53
46
54
@Composable
47
55
private 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
+ }
49
244
}
50
245
51
246
@Preview
@@ -64,6 +259,9 @@ private fun StoppableInfiniteAnimationSample() {
64
259
modifier = Modifier .fillMaxSize()
65
260
) {
66
261
262
+
263
+ Text (" Infinite Stoppable Animatable" , fontSize = 24 .sp)
264
+
67
265
Row (
68
266
modifier = Modifier .fillMaxWidth().padding(16 .dp),
69
267
verticalAlignment = Alignment .CenterVertically ,
@@ -72,8 +270,9 @@ private fun StoppableInfiniteAnimationSample() {
72
270
Text (" Text1" )
73
271
Column {
74
272
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 )
77
276
) {
78
277
79
278
drawLine(
@@ -134,7 +333,6 @@ class RotateAnimationState(
134
333
get() = animatable.value
135
334
136
335
private val animatable = Animatable (0f )
137
- private val durationPerAngle = duration / 360f
138
336
139
337
var rotationStatus: RotationStatus = RotationStatus .Idle
140
338
@@ -167,10 +365,10 @@ class RotateAnimationState(
167
365
coroutineScope.launch {
168
366
rotationStatus = RotationStatus .Stopping
169
367
val currentValue = animatable.value
368
+ val durationPerAngle = duration / 360f
170
369
// Duration depends on how far current angle is to 360f
171
370
// total duration is duration per angle multiplied with total angles to rotate
172
371
val durationToZero = (durationPerAngle * (360 - currentValue)).toInt()
173
- animatable.snapTo(currentValue)
174
372
animatable.animateTo(
175
373
targetValue = 360f ,
176
374
tween(
0 commit comments