• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 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 @file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
18 
19 package com.android.compose.animation.scene.demo
20 
21 import androidx.compose.animation.core.FiniteAnimationSpec
22 import androidx.compose.animation.core.Spring
23 import androidx.compose.animation.core.spring
24 import androidx.compose.foundation.clickable
25 import androidx.compose.foundation.layout.Column
26 import androidx.compose.foundation.layout.Row
27 import androidx.compose.foundation.layout.fillMaxWidth
28 import androidx.compose.foundation.layout.padding
29 import androidx.compose.foundation.layout.width
30 import androidx.compose.foundation.rememberScrollState
31 import androidx.compose.foundation.verticalScroll
32 import androidx.compose.material.icons.Icons
33 import androidx.compose.material.icons.filled.Add
34 import androidx.compose.material.icons.filled.Remove
35 import androidx.compose.material3.AlertDialog
36 import androidx.compose.material3.Button
37 import androidx.compose.material3.Checkbox
38 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
39 import androidx.compose.material3.Icon
40 import androidx.compose.material3.IconButton
41 import androidx.compose.material3.MaterialTheme
42 import androidx.compose.material3.MotionScheme
43 import androidx.compose.material3.Slider
44 import androidx.compose.material3.Text
45 import androidx.compose.material3.TriStateCheckbox
46 import androidx.compose.runtime.Composable
47 import androidx.compose.runtime.saveable.mapSaver
48 import androidx.compose.ui.Alignment
49 import androidx.compose.ui.Modifier
50 import androidx.compose.ui.draw.clip
51 import androidx.compose.ui.state.ToggleableState
52 import androidx.compose.ui.text.style.TextAlign
53 import androidx.compose.ui.unit.dp
54 import kotlin.math.roundToInt
55 
56 data class DemoConfiguration(
57     val notificationsInLockscreen: Int = 2,
58     val notificationsInShade: Int = 10,
59     val quickSettingsRows: Int = 4,
60     val interactiveNotifications: Boolean = false,
61     val showMediaPlayer: Boolean = true,
62     val isFullscreen: Boolean = false,
63     val canChangeSceneOrOverlays: Boolean = true,
64     val transitionInterceptionThreshold: Float = 0.05f,
65     val motion: MotionConfig = MotionConfig.Default,
66     val lsToShadeRequiresFullSwipe: ToggleableState = ToggleableState.Indeterminate,
67     val enableOverlays: Boolean = false,
68     val transitionBorder: Boolean = true,
69     val deferTransitionProgress: Boolean = false,
70     val firstCompositionDelay: Long = 0L,
71 ) {
72     companion object {
<lambda>null73         val Saver = run {
74             val notificationsInLockscreenKey = "notificationsInLockscreen"
75             val notificationsInShadeKey = "notificationsInShade"
76             val quickSettingsRowsKey = "quickSettingsRows"
77             val interactiveNotificationsKey = "interactiveNotifications"
78             val showMediaPlayerKey = "showMediaPlayer"
79             val isFullscreenKey = "isFullscreen"
80             val canChangeSceneOrOverlaysKey = "canChangeSceneOrOverlays"
81             val transitionInterceptionThresholdKey = "transitionInterceptionThreshold"
82             val motionSchemeKey = "motionScheme"
83             val lsToShadeRequiresFullSwipe = "lsToShadeRequiresFullSwipe"
84             val enableOverlays = "enableOverlays"
85             val transitionBorder = "transitionBorder"
86             val deferTransitionProgress = "deferTransitionProgress"
87             val firstCompositionDelay = "firstCompositionDelay"
88 
89             mapSaver(
90                 save = {
91                     mapOf(
92                         notificationsInLockscreenKey to it.notificationsInLockscreen,
93                         notificationsInShadeKey to it.notificationsInShade,
94                         quickSettingsRowsKey to it.quickSettingsRows,
95                         interactiveNotificationsKey to it.interactiveNotifications,
96                         showMediaPlayerKey to it.showMediaPlayer,
97                         isFullscreenKey to it.isFullscreen,
98                         canChangeSceneOrOverlaysKey to it.canChangeSceneOrOverlays,
99                         transitionInterceptionThresholdKey to it.transitionInterceptionThreshold,
100                         motionSchemeKey to it.motion.name,
101                         lsToShadeRequiresFullSwipe to it.lsToShadeRequiresFullSwipe,
102                         enableOverlays to it.enableOverlays,
103                         transitionBorder to it.transitionBorder,
104                         deferTransitionProgress to it.deferTransitionProgress,
105                         firstCompositionDelay to it.firstCompositionDelay,
106                     )
107                 },
108                 restore = {
109                     DemoConfiguration(
110                         notificationsInLockscreen = it[notificationsInLockscreenKey] as Int,
111                         notificationsInShade = it[notificationsInShadeKey] as Int,
112                         quickSettingsRows = it[quickSettingsRowsKey] as Int,
113                         interactiveNotifications = it[interactiveNotificationsKey] as Boolean,
114                         showMediaPlayer = it[showMediaPlayerKey] as Boolean,
115                         isFullscreen = it[isFullscreenKey] as Boolean,
116                         canChangeSceneOrOverlays = it[canChangeSceneOrOverlaysKey] as Boolean,
117                         transitionInterceptionThreshold =
118                             it[transitionInterceptionThresholdKey] as Float,
119                         motion = MotionConfig.fromName(it[motionSchemeKey] as String),
120                         lsToShadeRequiresFullSwipe =
121                             it[lsToShadeRequiresFullSwipe] as ToggleableState,
122                         enableOverlays = it[enableOverlays] as Boolean,
123                         transitionBorder = it[transitionBorder] as Boolean,
124                         deferTransitionProgress = it[deferTransitionProgress] as Boolean,
125                         firstCompositionDelay = it[firstCompositionDelay] as Long,
126                     )
127                 },
128             )
129         }
130     }
131 }
132 
133 class MotionConfig(val name: String, val scheme: MotionScheme) {
134     companion object {
135         val Options =
136             listOf(
137                 MotionConfig("standard", MotionScheme.standard()),
138                 MotionConfig("expressive", MotionScheme.expressive()),
139                 MotionConfig(
140                     "bouncy",
141                     CustomMotionScheme(
142                         spatial = spring(Spring.DampingRatioHighBouncy, Spring.StiffnessVeryLow),
143                         effects = spring(Spring.DampingRatioNoBouncy, Spring.StiffnessVeryLow),
144                     ),
145                 ),
146                 MotionConfig(
147                     "stiff",
148                     CustomMotionScheme(
149                         spatial = spring(Spring.DampingRatioLowBouncy, Spring.StiffnessHigh),
150                         effects = spring(Spring.DampingRatioNoBouncy, Spring.StiffnessHigh),
151                     ),
152                 ),
153                 MotionConfig(
154                     "high bouncy & stiff",
155                     CustomMotionScheme(
156                         spatial = spring(Spring.DampingRatioHighBouncy, Spring.StiffnessHigh),
157                         effects = spring(Spring.DampingRatioNoBouncy, Spring.StiffnessHigh),
158                     ),
159                 ),
160                 MotionConfig(
161                     "low bouncy & stiff",
162                     CustomMotionScheme(
163                         spatial = spring(Spring.DampingRatioLowBouncy, Spring.StiffnessVeryLow),
164                         effects = spring(Spring.DampingRatioNoBouncy, Spring.StiffnessVeryLow),
165                     ),
166                 ),
167             )
168 
169         val Default: MotionConfig = Options[1]
170 
<lambda>null171         fun fromName(name: String) = Options.first { it.name == name }
172     }
173 
174     // Implementation inspired by MotionScheme.standard()
175     @Suppress("UNCHECKED_CAST")
176     class CustomMotionScheme(
177         private val spatial: FiniteAnimationSpec<Any>,
178         private val effects: FiniteAnimationSpec<Any>,
179         private val fastSpatial: FiniteAnimationSpec<Any> = spatial,
180         private val fastEffects: FiniteAnimationSpec<Any> = effects,
181         private val slowSpatial: FiniteAnimationSpec<Any> = spatial,
182         private val slowEffects: FiniteAnimationSpec<Any> = effects,
183     ) : MotionScheme {
defaultSpatialSpecnull184         override fun <T> defaultSpatialSpec() = spatial as FiniteAnimationSpec<T>
185 
186         override fun <T> fastSpatialSpec() = fastSpatial as FiniteAnimationSpec<T>
187 
188         override fun <T> slowSpatialSpec() = slowSpatial as FiniteAnimationSpec<T>
189 
190         override fun <T> defaultEffectsSpec() = effects as FiniteAnimationSpec<T>
191 
192         override fun <T> fastEffectsSpec() = fastEffects as FiniteAnimationSpec<T>
193 
194         override fun <T> slowEffectsSpec() = slowEffects as FiniteAnimationSpec<T>
195     }
196 }
197 
198 @Composable
199 fun DemoConfigurationDialog(
200     configuration: DemoConfiguration,
201     onConfigurationChange: (DemoConfiguration) -> Unit,
202     onDismissRequest: () -> Unit,
203 ) {
204     AlertDialog(
205         onDismissRequest = onDismissRequest,
206         title = { Text("Demo configuration") },
207         text = {
208             Column(Modifier.fillMaxWidth().verticalScroll(rememberScrollState())) {
209                 Text(text = "Generic app settings", style = MaterialTheme.typography.titleMedium)
210 
211                 // Fullscreen.
212                 Checkbox(
213                     label = "Fullscreen",
214                     checked = configuration.isFullscreen,
215                     onCheckedChange = {
216                         onConfigurationChange(
217                             configuration.copy(isFullscreen = !configuration.isFullscreen)
218                         )
219                     },
220                 )
221 
222                 // Can change scene.
223                 Checkbox(
224                     label = "Can change scene or overlays",
225                     checked = configuration.canChangeSceneOrOverlays,
226                     onCheckedChange = {
227                         onConfigurationChange(
228                             configuration.copy(
229                                 canChangeSceneOrOverlays = !configuration.canChangeSceneOrOverlays
230                             )
231                         )
232                     },
233                 )
234 
235                 // Overlays.
236                 Checkbox(
237                     label = "Overlays",
238                     checked = configuration.enableOverlays,
239                     onCheckedChange = {
240                         onConfigurationChange(
241                             configuration.copy(enableOverlays = !configuration.enableOverlays)
242                         )
243                     },
244                 )
245 
246                 // Transition border.
247                 Checkbox(
248                     label = "Transition border",
249                     checked = configuration.transitionBorder,
250                     onCheckedChange = {
251                         onConfigurationChange(
252                             configuration.copy(transitionBorder = !configuration.transitionBorder)
253                         )
254                     },
255                 )
256 
257                 // Defer transition progress.
258                 Checkbox(
259                     label = "Defer transition progress",
260                     checked = configuration.deferTransitionProgress,
261                     onCheckedChange = {
262                         onConfigurationChange(
263                             configuration.copy(
264                                 deferTransitionProgress = !configuration.deferTransitionProgress
265                             )
266                         )
267                     },
268                 )
269 
270                 // First composition delay.
271                 Text(text = "First composition delay: ${configuration.firstCompositionDelay}ms")
272                 Slider(
273                     value = configuration.firstCompositionDelay,
274                     onValueChange = {
275                         onConfigurationChange(configuration.copy(firstCompositionDelay = it))
276                     },
277                     values = List(21) { it * 10L },
278                     onValueNotFound = { 0 },
279                 )
280 
281                 Text(text = "Theme", style = MaterialTheme.typography.titleMedium)
282 
283                 Text(text = "Motion: ${configuration.motion.name}")
284                 Slider(
285                     value = configuration.motion,
286                     onValueChange = { onConfigurationChange(configuration.copy(motion = it)) },
287                     values = MotionConfig.Options,
288                     onValueNotFound = { 0 },
289                 )
290 
291                 Text(text = "Scrollable", style = MaterialTheme.typography.titleMedium)
292 
293                 // Interception threshold.
294                 val thresholdString =
295                     String.format("%.2f", configuration.transitionInterceptionThreshold)
296                 Text(text = "Interception threshold: $thresholdString")
297                 Slider(
298                     value = configuration.transitionInterceptionThreshold,
299                     onValueChange = {
300                         onConfigurationChange(
301                             configuration.copy(transitionInterceptionThreshold = it)
302                         )
303                     },
304                     valueRange = 0f..0.5f,
305                     stepSize = 0.01f,
306                 )
307 
308                 Text(text = "Media", style = MaterialTheme.typography.titleMedium)
309 
310                 // Whether we should show the media player.
311                 Checkbox(
312                     label = "Show media player",
313                     checked = configuration.showMediaPlayer,
314                     onCheckedChange = {
315                         onConfigurationChange(
316                             configuration.copy(showMediaPlayer = !configuration.showMediaPlayer)
317                         )
318                     },
319                 )
320 
321                 Text(text = "Notifications", style = MaterialTheme.typography.titleMedium)
322 
323                 // Whether notifications are interactive
324                 Checkbox(
325                     label = "Interactive notifications",
326                     checked = configuration.interactiveNotifications,
327                     onCheckedChange = {
328                         onConfigurationChange(
329                             configuration.copy(
330                                 interactiveNotifications = !configuration.interactiveNotifications
331                             )
332                         )
333                     },
334                 )
335 
336                 // Number of notifications in the Shade scene.
337                 Counter(
338                     "# notifications in Shade",
339                     configuration.notificationsInShade,
340                     onValueChange = {
341                         onConfigurationChange(configuration.copy(notificationsInShade = it))
342                     },
343                 )
344 
345                 // Number of notifications in the Lockscreen scene.
346                 Counter(
347                     "# notifications in Lockscreen",
348                     configuration.notificationsInLockscreen,
349                     onValueChange = {
350                         onConfigurationChange(configuration.copy(notificationsInLockscreen = it))
351                     },
352                 )
353 
354                 Text(text = "Quick Settings", style = MaterialTheme.typography.titleMedium)
355 
356                 Counter(
357                     "# quick settings rows",
358                     configuration.quickSettingsRows,
359                     onValueChange = {
360                         onConfigurationChange(configuration.copy(quickSettingsRows = it))
361                     },
362                 )
363 
364                 Text(text = "Lockscreen", style = MaterialTheme.typography.titleMedium)
365 
366                 // Whether the LS => Shade transition requires a full distance swipe to be
367                 // committed.
368                 Checkbox(
369                     label = "Require full LS => Shade swipe",
370                     state = configuration.lsToShadeRequiresFullSwipe,
371                     onStateChange = {
372                         onConfigurationChange(configuration.copy(lsToShadeRequiresFullSwipe = it))
373                     },
374                 )
375             }
376         },
377         confirmButton = { Button(onClick = { onDismissRequest() }) { Text("Done") } },
378         dismissButton = {
379             Button(onClick = { onConfigurationChange(DemoConfiguration()) }) { Text("Reset") }
380         },
381     )
382 }
383 
384 @Composable
Checkboxnull385 private fun Checkbox(
386     label: String,
387     checked: Boolean,
388     onCheckedChange: (Boolean) -> Unit,
389     modifier: Modifier = Modifier,
390 ) {
391     Row(
392         modifier
393             .fillMaxWidth()
394             .clip(MaterialTheme.shapes.small)
395             .clickable(onClick = { onCheckedChange(!checked) }),
396         verticalAlignment = Alignment.CenterVertically,
397     ) {
398         Checkbox(checked, onCheckedChange)
399         Text(label, Modifier.padding(start = 8.dp))
400     }
401 }
402 
403 @Composable
Checkboxnull404 private fun Checkbox(
405     label: String,
406     state: ToggleableState,
407     onStateChange: (ToggleableState) -> Unit,
408     modifier: Modifier = Modifier,
409 ) {
410     fun onClick() {
411         onStateChange(
412             when (state) {
413                 ToggleableState.On -> ToggleableState.Off
414                 ToggleableState.Off -> ToggleableState.Indeterminate
415                 ToggleableState.Indeterminate -> ToggleableState.On
416             }
417         )
418     }
419 
420     Row(
421         modifier.fillMaxWidth().clip(MaterialTheme.shapes.small).clickable(onClick = ::onClick),
422         verticalAlignment = Alignment.CenterVertically,
423     ) {
424         TriStateCheckbox(state, onClick = ::onClick)
425         Text(label, Modifier.padding(start = 8.dp))
426     }
427 }
428 
429 @Composable
Counternull430 private fun Counter(
431     label: String,
432     value: Int,
433     onValueChange: (Int) -> Unit,
434     modifier: Modifier = Modifier,
435 ) {
436     Row(modifier, verticalAlignment = Alignment.CenterVertically) {
437         IconButton(onClick = { onValueChange((value - 1).coerceAtLeast(0)) }) {
438             Icon(Icons.Default.Remove, null)
439         }
440         Text(value.toString(), Modifier.width(18.dp), textAlign = TextAlign.Center)
441         IconButton(onClick = { onValueChange((value + 1)) }) { Icon(Icons.Default.Add, null) }
442         Text(label, Modifier.padding(start = 8.dp))
443     }
444 }
445 
446 @Composable
Slidernull447 private fun <T> Slider(
448     value: T,
449     onValueChange: (T) -> Unit,
450     values: List<T>,
451     onValueNotFound: () -> Int = { 0 },
452 ) {
453     Slider(
<lambda>null454         value = (values.indexOf(value).takeIf { it != -1 } ?: onValueNotFound()).toFloat(),
<lambda>null455         onValueChange = { onValueChange(values[it.roundToInt()]) },
456         valueRange = 0f..values.lastIndex.toFloat(),
457         steps = values.lastIndex - 1,
458     )
459 }
460 
461 @Composable
Slidernull462 private fun Slider(
463     value: Float,
464     onValueChange: (Float) -> Unit,
465     valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
466     stepSize: Float,
467 ) {
468     Slider(
469         value = value,
470         onValueChange = onValueChange,
471         valueRange = valueRange,
472         steps = ((valueRange.endInclusive - valueRange.start) / stepSize).toInt() - 1,
473     )
474 }
475