1 /*
<lambda>null2 * Copyright 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.graphics.shapes.testcompose
18
19 import androidx.compose.animation.core.Animatable
20 import androidx.compose.animation.core.AnimationSpec
21 import androidx.compose.animation.core.AnimationVector1D
22 import androidx.compose.animation.core.DurationBasedAnimationSpec
23 import androidx.compose.animation.core.RepeatMode
24 import androidx.compose.animation.core.infiniteRepeatable
25 import androidx.compose.animation.core.spring
26 import androidx.compose.animation.core.tween
27 import androidx.compose.foundation.layout.Arrangement
28 import androidx.compose.foundation.layout.Box
29 import androidx.compose.foundation.layout.Column
30 import androidx.compose.foundation.layout.Row
31 import androidx.compose.foundation.layout.aspectRatio
32 import androidx.compose.foundation.layout.fillMaxSize
33 import androidx.compose.foundation.layout.fillMaxWidth
34 import androidx.compose.foundation.layout.padding
35 import androidx.compose.foundation.layout.size
36 import androidx.compose.material.icons.Icons
37 import androidx.compose.material.icons.automirrored.filled.Undo
38 import androidx.compose.material.icons.filled.Pause
39 import androidx.compose.material.icons.filled.PlayArrow
40 import androidx.compose.material.icons.filled.Repeat
41 import androidx.compose.material3.ExperimentalMaterial3Api
42 import androidx.compose.material3.Icon
43 import androidx.compose.material3.IconButton
44 import androidx.compose.material3.IconButtonDefaults
45 import androidx.compose.material3.MaterialTheme
46 import androidx.compose.material3.SegmentedButton
47 import androidx.compose.material3.SegmentedButtonDefaults
48 import androidx.compose.material3.SingleChoiceSegmentedButtonRow
49 import androidx.compose.material3.Slider
50 import androidx.compose.material3.Text
51 import androidx.compose.material3.TopAppBar
52 import androidx.compose.runtime.Composable
53 import androidx.compose.runtime.getValue
54 import androidx.compose.runtime.mutableIntStateOf
55 import androidx.compose.runtime.mutableStateOf
56 import androidx.compose.runtime.remember
57 import androidx.compose.runtime.rememberCoroutineScope
58 import androidx.compose.runtime.setValue
59 import androidx.compose.ui.Alignment
60 import androidx.compose.ui.Modifier
61 import androidx.compose.ui.draw.drawWithContent
62 import androidx.compose.ui.graphics.Color
63 import androidx.compose.ui.graphics.drawscope.Fill
64 import androidx.compose.ui.graphics.drawscope.Stroke
65 import androidx.compose.ui.unit.dp
66 import androidx.graphics.shapes.Morph
67 import androidx.graphics.shapes.RoundedPolygon
68 import androidx.graphics.shapes.TransformResult
69 import kotlinx.coroutines.CoroutineScope
70 import kotlinx.coroutines.launch
71
72 @Composable
73 private fun MorphView(
74 morph: Morph,
75 progress: Animatable<Float, AnimationVector1D>,
76 modifier: Modifier = Modifier,
77 fillColor: Color = MaterialTheme.colorScheme.primary,
78 isDebug: Boolean = false,
79 stroked: Boolean = false
80 ) {
81 val scheme = MaterialTheme.colorScheme
82 Box(modifier.fillMaxSize()) {
83 Box(
84 modifier =
85 modifier.aspectRatio(1f).fillMaxSize().align(Alignment.Center).drawWithContent {
86 drawContent()
87 val path = morph.toPath(progress.value)
88 fitToViewport(path, morph.getBounds(), size)
89 if (isDebug) {
90 val scale = size.minDimension
91 drawPath(path, fillColor, style = Stroke(2f))
92 morph.forEachCubic(progress.value) { cubic ->
93 cubic.transform { x, y -> TransformResult(x * scale, y * scale) }
94 debugDrawCubic(cubic, scheme)
95 }
96 } else {
97 val style = if (stroked) Stroke(size.width / 10f) else Fill
98 drawPath(path, fillColor, style = style)
99 }
100 },
101 )
102 }
103 }
104
105 @Composable
AnimatedMorphViewnull106 fun AnimatedMorphView(
107 shapes: List<RoundedPolygon>,
108 selectedStartShape: Int,
109 selectedEndShape: Int,
110 baseAnimation: DurationBasedAnimationSpec<Float> = tween(1400)
111 ) {
112 var selectedDisplayIndex by remember { mutableIntStateOf(0) }
113 val debug = selectedDisplayIndex != 0
114 val morphed =
115 remember(selectedStartShape, selectedEndShape, debug) {
116 Morph(shapes[selectedStartShape], shapes[selectedEndShape])
117 }
118
119 val progress = remember { Animatable(0f) }
120 val scope = rememberCoroutineScope()
121
122 Column {
123 AnimatedMorphViewHeader(selectedDisplayIndex) { index -> selectedDisplayIndex = index }
124
125 AnimationControls(progress, scope, baseAnimation)
126
127 Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
128 MorphView(morphed, progress, isDebug = debug)
129 }
130 }
131 }
132
133 @OptIn(ExperimentalMaterial3Api::class)
134 @Composable
AnimatedMorphViewHeadernull135 fun AnimatedMorphViewHeader(selectedIndex: Int, onSelectedModeIndexChanged: (Int) -> Unit) {
136 val options =
137 listOf(
138 "Output",
139 "Debug",
140 )
141
142 TopAppBar(
143 title = { Text("Morph Preview") },
144 actions = {
145 SingleChoiceSegmentedButtonRow {
146 options.forEachIndexed { index, label ->
147 SegmentedButton(
148 shape =
149 SegmentedButtonDefaults.itemShape(index = index, count = options.size),
150 onClick = { onSelectedModeIndexChanged(index) },
151 selected = index == selectedIndex,
152 icon = {}
153 ) {
154 Text(label)
155 }
156 }
157 }
158 }
159 )
160 }
161
162 @Composable
AnimationControlsnull163 private fun AnimationControls(
164 progress: Animatable<Float, AnimationVector1D>,
165 scope: CoroutineScope,
166 baseAnimation: DurationBasedAnimationSpec<Float>
167 ) {
168 val selectedColors = IconButtonDefaults.filledIconButtonColors()
169 val unselectedColors = IconButtonDefaults.iconButtonColors()
170
171 var playBackwards by remember { mutableStateOf(false) }
172 var isRepeat by remember { mutableStateOf(false) }
173
174 Row(
175 Modifier.padding(horizontal = 5.dp).fillMaxWidth(),
176 horizontalArrangement = Arrangement.SpaceAround
177 ) {
178 IconButton(
179 onClick = {
180 scope.launch { restartOrPause(progress, isRepeat, playBackwards, baseAnimation) }
181 }
182 ) {
183 if (animationCanBeStopped(progress, isRepeat)) {
184 Icon(Icons.Default.Pause, contentDescription = "Pause Animation")
185 } else {
186 Icon(Icons.Default.PlayArrow, contentDescription = "Play Animation")
187 }
188 }
189
190 Slider(
191 value = progress.value.coerceIn(0f, 1f),
192 onValueChange = { scope.launch { progress.snapTo(it) } },
193 Modifier.fillMaxWidth(0.75f)
194 )
195
196 IconButton(
197 onClick = {
198 scope.launch {
199 playBackwards = !playBackwards
200 restartAnimation(progress, isRepeat, playBackwards, baseAnimation)
201 }
202 },
203 colors = if (playBackwards) selectedColors else unselectedColors,
204 modifier = Modifier.size(40.dp)
205 ) {
206 Icon(Icons.AutoMirrored.Default.Undo, contentDescription = "Play Backwards")
207 }
208
209 IconButton(
210 onClick = {
211 scope.launch {
212 isRepeat = !isRepeat
213 if (!isRepeat) {
214 scope.launch { progress.stop() }
215 } else {
216 restartAnimation(progress, true, playBackwards, baseAnimation)
217 }
218 }
219 },
220 colors = if (isRepeat) selectedColors else unselectedColors,
221 modifier = Modifier.size(40.dp)
222 ) {
223 Icon(Icons.Default.Repeat, contentDescription = "Repeat Animation")
224 }
225 }
226 }
227
restartAnimationnull228 private suspend fun restartAnimation(
229 progress: Animatable<Float, AnimationVector1D>,
230 isRepeat: Boolean,
231 isReverse: Boolean,
232 baseAnimation: DurationBasedAnimationSpec<Float>,
233 ) {
234 val startValue = if (isReverse) 1f else 0f
235 val endValue = if (isReverse) 0f else 1f
236
237 val animationSpec: AnimationSpec<Float> =
238 if (isRepeat)
239 infiniteRepeatable(
240 animation = baseAnimation,
241 repeatMode = if (isReverse) RepeatMode.Reverse else RepeatMode.Restart
242 )
243 else spring(dampingRatio = 0.65f, stiffness = 50f)
244
245 progress.snapTo(startValue)
246 progress.animateTo(endValue, animationSpec = animationSpec)
247 }
248
restartOrPausenull249 private suspend fun restartOrPause(
250 progress: Animatable<Float, AnimationVector1D>,
251 isRepeat: Boolean,
252 isReverse: Boolean,
253 baseAnimation: DurationBasedAnimationSpec<Float>
254 ) {
255 if (animationCanBeStopped(progress, isRepeat)) {
256 progress.stop()
257 } else {
258 restartAnimation(progress, isRepeat, isReverse, baseAnimation)
259 }
260 }
261
animationCanBeStoppednull262 private fun animationCanBeStopped(
263 progress: Animatable<Float, AnimationVector1D>,
264 isRepeat: Boolean
265 ): Boolean {
266 return isRepeat && progress.isRunning
267 }
268