Skip to content

Commit f1cbeb7

Browse files
add Animatable interruption samples
1 parent 4d19912 commit f1cbeb7

File tree

1 file changed

+203
-5
lines changed

1 file changed

+203
-5
lines changed

Tutorial1-1Basics/src/main/java/com/smarttoolfactory/tutorial1_1basics/chapter6_graphics/Tutorial6_33StoppableInfiniteAnimation.kt

+203-5
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,21 @@ import androidx.compose.foundation.layout.fillMaxWidth
1414
import androidx.compose.foundation.layout.height
1515
import androidx.compose.foundation.layout.padding
1616
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
1720
import androidx.compose.material.Button
1821
import androidx.compose.material.Icon
1922
import androidx.compose.material.Text
2023
import androidx.compose.material.icons.Icons
2124
import androidx.compose.material.icons.filled.ScreenRotation
2225
import androidx.compose.runtime.Composable
26+
import androidx.compose.runtime.LaunchedEffect
27+
import androidx.compose.runtime.getValue
28+
import androidx.compose.runtime.mutableIntStateOf
2329
import androidx.compose.runtime.remember
2430
import androidx.compose.runtime.rememberCoroutineScope
31+
import androidx.compose.runtime.setValue
2532
import androidx.compose.ui.Alignment
2633
import androidx.compose.ui.Modifier
2734
import androidx.compose.ui.draw.rotate
@@ -31,6 +38,7 @@ import androidx.compose.ui.graphics.graphicsLayer
3138
import androidx.compose.ui.tooling.preview.Preview
3239
import androidx.compose.ui.unit.dp
3340
import androidx.compose.ui.unit.sp
41+
import kotlinx.coroutines.CancellationException
3442
import kotlinx.coroutines.CoroutineScope
3543
import kotlinx.coroutines.isActive
3644
import kotlinx.coroutines.launch
@@ -45,7 +53,194 @@ fun Tutorial6_33Screen() {
4553

4654
@Composable
4755
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+
}
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

Comments
 (0)