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