1 /*
<lambda>null2  * 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 package com.android.compose.animation.scene.demo
18 
19 import androidx.compose.animation.AnimatedVisibility
20 import androidx.compose.animation.animateContentSize
21 import androidx.compose.animation.core.Spring
22 import androidx.compose.animation.expandVertically
23 import androidx.compose.foundation.border
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.shape.RoundedCornerShape
32 import androidx.compose.foundation.verticalScroll
33 import androidx.compose.material.icons.Icons
34 import androidx.compose.material.icons.filled.Add
35 import androidx.compose.material.icons.filled.Remove
36 import androidx.compose.material3.AlertDialog
37 import androidx.compose.material3.Button
38 import androidx.compose.material3.Checkbox
39 import androidx.compose.material3.FilledTonalButton
40 import androidx.compose.material3.Icon
41 import androidx.compose.material3.IconButton
42 import androidx.compose.material3.MaterialTheme
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.DisposableEffect
48 import androidx.compose.runtime.getValue
49 import androidx.compose.runtime.mutableStateOf
50 import androidx.compose.runtime.remember
51 import androidx.compose.runtime.saveable.mapSaver
52 import androidx.compose.runtime.setValue
53 import androidx.compose.ui.Alignment
54 import androidx.compose.ui.Modifier
55 import androidx.compose.ui.draw.clip
56 import androidx.compose.ui.state.ToggleableState
57 import androidx.compose.ui.text.style.TextAlign
58 import androidx.compose.ui.unit.dp
59 import com.android.compose.animation.scene.ProgressConverter
60 import com.android.compose.animation.scene.demo.DemoOverscrollProgress.Tanh
61 import kotlin.math.roundToInt
62 import kotlin.math.tanh
63 
64 data class DemoConfiguration(
65     val notificationsInLockscreen: Int = 2,
66     val notificationsInShade: Int = 10,
67     val quickSettingsRows: Int = 4,
68     val interactiveNotifications: Boolean = false,
69     val showMediaPlayer: Boolean = true,
70     val isFullscreen: Boolean = false,
71     val canChangeSceneOrOverlays: Boolean = true,
72     val transitionInterceptionThreshold: Float = 0.05f,
73     val springConfigurations: DemoSpringConfigurations = DemoSpringConfigurations.presets[0],
74     val useOverscrollSpec: Boolean = true,
75     val overscrollProgressConverter: DemoOverscrollProgress = Tanh(maxProgress = 0.2f, tilt = 3f),
76     val lsToShadeRequiresFullSwipe: ToggleableState = ToggleableState.Indeterminate,
77     val enableOverlays: Boolean = false,
78     val transitionBorder: Boolean = true,
79 ) {
80     companion object {
81         val Saver = run {
82             val notificationsInLockscreenKey = "notificationsInLockscreen"
83             val notificationsInShadeKey = "notificationsInShade"
84             val quickSettingsRowsKey = "quickSettingsRows"
85             val interactiveNotificationsKey = "interactiveNotifications"
86             val showMediaPlayerKey = "showMediaPlayer"
87             val isFullscreenKey = "isFullscreen"
88             val canChangeSceneOrOverlaysKey = "canChangeSceneOrOverlays"
89             val transitionInterceptionThresholdKey = "transitionInterceptionThreshold"
90             val springConfigurationsKey = "springConfigurations"
91             val useOverscrollSpec = "useOverscrollSpec"
92             val overscrollProgress = "overscrollProgress"
93             val lsToShadeRequiresFullSwipe = "lsToShadeRequiresFullSwipe"
94             val enableOverlays = "enableOverlays"
95             val transitionBorder = "transitionBorder"
96 
97             mapSaver(
98                 save = {
99                     mapOf(
100                         notificationsInLockscreenKey to it.notificationsInLockscreen,
101                         notificationsInShadeKey to it.notificationsInShade,
102                         quickSettingsRowsKey to it.quickSettingsRows,
103                         interactiveNotificationsKey to it.interactiveNotifications,
104                         showMediaPlayerKey to it.showMediaPlayer,
105                         isFullscreenKey to it.isFullscreen,
106                         canChangeSceneOrOverlaysKey to it.canChangeSceneOrOverlays,
107                         transitionInterceptionThresholdKey to it.transitionInterceptionThreshold,
108                         springConfigurationsKey to it.springConfigurations.save(),
109                         useOverscrollSpec to it.useOverscrollSpec,
110                         overscrollProgress to it.overscrollProgressConverter.save(),
111                         lsToShadeRequiresFullSwipe to it.lsToShadeRequiresFullSwipe,
112                         enableOverlays to it.enableOverlays,
113                         transitionBorder to it.transitionBorder,
114                     )
115                 },
116                 restore = {
117                     DemoConfiguration(
118                         notificationsInLockscreen = it[notificationsInLockscreenKey] as Int,
119                         notificationsInShade = it[notificationsInShadeKey] as Int,
120                         quickSettingsRows = it[quickSettingsRowsKey] as Int,
121                         interactiveNotifications = it[interactiveNotificationsKey] as Boolean,
122                         showMediaPlayer = it[showMediaPlayerKey] as Boolean,
123                         isFullscreen = it[isFullscreenKey] as Boolean,
124                         canChangeSceneOrOverlays = it[canChangeSceneOrOverlaysKey] as Boolean,
125                         transitionInterceptionThreshold =
126                             it[transitionInterceptionThresholdKey] as Float,
127                         springConfigurations =
128                             it[springConfigurationsKey].restoreSpringConfigurations(),
129                         useOverscrollSpec = it[useOverscrollSpec] as Boolean,
130                         overscrollProgressConverter =
131                             it[overscrollProgress].restoreOverscrollProgress(),
132                         lsToShadeRequiresFullSwipe =
133                             it[lsToShadeRequiresFullSwipe] as ToggleableState,
134                         enableOverlays = it[enableOverlays] as Boolean,
135                         transitionBorder = it[transitionBorder] as Boolean,
136                     )
137                 },
138             )
139         }
140     }
141 }
142 
143 data class DemoSpringConfigurations(
144     val name: String,
145     val systemUiSprings: SpringConfiguration,
146     val notificationSprings: SpringConfiguration,
147 ) {
savenull148     fun save(): String =
149         listOf(
150                 systemUiSprings.stiffness,
151                 systemUiSprings.dampingRatio,
152                 notificationSprings.stiffness,
153                 notificationSprings.dampingRatio,
154             )
155             .joinToString(",")
156 
157     companion object {
158         val presets =
159             listOf(
160                 DemoSpringConfigurations(
161                     name = "Default",
162                     systemUiSprings =
163                         SpringConfiguration(
164                             Spring.StiffnessMediumLow,
165                             Spring.DampingRatioLowBouncy,
166                         ),
167                     notificationSprings =
168                         SpringConfiguration(Spring.StiffnessMediumLow, Spring.DampingRatioLowBouncy),
169                 ),
170                 DemoSpringConfigurations(
171                     name = "NotBouncy Fast",
172                     systemUiSprings =
173                         SpringConfiguration(Spring.StiffnessMedium, Spring.DampingRatioNoBouncy),
174                     notificationSprings =
175                         SpringConfiguration(Spring.StiffnessMedium, Spring.DampingRatioNoBouncy),
176                 ),
177                 DemoSpringConfigurations(
178                     name = "NotBouncy Normal",
179                     systemUiSprings =
180                         SpringConfiguration(Spring.StiffnessMediumLow, Spring.DampingRatioNoBouncy),
181                     notificationSprings =
182                         SpringConfiguration(Spring.StiffnessMediumLow, Spring.DampingRatioNoBouncy),
183                 ),
184                 DemoSpringConfigurations(
185                     name = "SlowBouncy",
186                     systemUiSprings =
187                         SpringConfiguration(
188                             stiffness = (Spring.StiffnessLow + Spring.StiffnessVeryLow) / 2f,
189                             dampingRatio = 0.85f,
190                         ),
191                     notificationSprings =
192                         SpringConfiguration(Spring.StiffnessMediumLow, Spring.DampingRatioLowBouncy),
193                 ),
194                 DemoSpringConfigurations(
195                     name = "Less Bouncy",
196                     systemUiSprings =
197                         SpringConfiguration(
198                             stiffness = (Spring.StiffnessMediumLow + Spring.StiffnessLow) / 2f,
199                             dampingRatio = 0.8f,
200                         ),
201                     notificationSprings =
202                         SpringConfiguration(
203                             stiffness = Spring.StiffnessMediumLow,
204                             dampingRatio = 0.8f,
205                         ),
206                 ),
207                 DemoSpringConfigurations(
208                     name = "Bouncy",
209                     systemUiSprings =
210                         SpringConfiguration(
211                             stiffness = (Spring.StiffnessMediumLow + Spring.StiffnessLow) / 2f,
212                             dampingRatio = Spring.DampingRatioLowBouncy,
213                         ),
214                     notificationSprings =
215                         SpringConfiguration(Spring.StiffnessMediumLow, Spring.DampingRatioLowBouncy),
216                 ),
217                 DemoSpringConfigurations(
218                     name = "VeryBouncy",
219                     systemUiSprings =
220                         SpringConfiguration(Spring.StiffnessLow, Spring.DampingRatioMediumBouncy),
221                     notificationSprings =
222                         SpringConfiguration(
223                             Spring.StiffnessMediumLow,
224                             Spring.DampingRatioMediumBouncy,
225                         ),
226                 ),
227             )
228 
229         private fun List<Pair<Float, String>>.addIntermediateValues(): List<Pair<Float, String>> =
230             flatMapIndexed { index, curr ->
231                 if (index == 0) {
232                     listOf(curr)
233                 } else {
234                     val prev = get(index - 1)
235                     val incr = (curr.first - prev.first) / 4f
236                     listOf(
237                         prev.first + incr * 1f to "${prev.second}+",
238                         prev.first + incr * 2f to "${prev.second}++",
239                         prev.first + incr * 3f to "${prev.second}+++",
240                         curr,
241                     )
242                 }
243             }
244 
245         private val stiffnessPairs =
246             listOf(
247                     Spring.StiffnessVeryLow to "VeryLow",
248                     Spring.StiffnessLow to "Low",
249                     Spring.StiffnessMediumLow to "MediumLow",
250                     Spring.StiffnessMedium to "Medium",
251                     Spring.StiffnessHigh to "High",
252                 )
253                 .addIntermediateValues()
254 
255         val stiffnessNames = stiffnessPairs.toMap()
256         val stiffnessValues = stiffnessPairs.map { it.first }
257 
258         private val dampingRatioPairs =
259             listOf(
260                     Spring.DampingRatioNoBouncy to "NoBouncy",
261                     0.85f to "VeryLow",
262                     Spring.DampingRatioLowBouncy to "Low",
263                     Spring.DampingRatioMediumBouncy to "Medium",
264                     Spring.DampingRatioHighBouncy to "High",
265                 )
266                 .addIntermediateValues()
267         val dampingRatioNames = dampingRatioPairs.toMap()
268         val dampingRatioValues = dampingRatioPairs.map { it.first }
269     }
270 }
271 
272 sealed class DemoOverscrollProgress(val name: String, val params: LinkedHashMap<String, Any>) :
273     ProgressConverter {
274     // Note: the order is guaranteed because we are using an ordered map (LinkedHashMap).
savenull275     fun save(): String = "$name:${params.values.joinToString(",")}"
276 
277     data object Linear : DemoOverscrollProgress("linear", linkedMapOf()) {
278         override fun convert(value: Float) = value
279     }
280 
281     data class RubberBand(val factor: Float) :
282         DemoOverscrollProgress("rubberBand", linkedMapOf("factor" to factor)) {
convertnull283         override fun convert(value: Float) = value * factor
284     }
285 
286     data class Tanh(val maxProgress: Float, val tilt: Float) :
287         DemoOverscrollProgress("tanh", linkedMapOf("maxValue" to maxProgress, "tilt" to tilt)) {
288         override fun convert(value: Float) = maxProgress * tanh(value / (maxProgress * tilt))
289     }
290 
291     companion object {
292         // We are using "by lazy" to avoid a null pointer on "Linear", read more on
293         // https://medium.com/livefront/kotlin-a-tale-of-static-cyclical-initialization-3aea530d2053
<lambda>null294         val presets by lazy {
295             listOf(
296                 Linear,
297                 RubberBand(factor = 0.1f),
298                 RubberBand(factor = 0.15f),
299                 RubberBand(factor = 0.21f),
300                 Tanh(maxProgress = 1f, tilt = 1f),
301                 Tanh(maxProgress = 0.5f, tilt = 1f),
302                 Tanh(maxProgress = 0.5f, tilt = 1.5f),
303                 Tanh(maxProgress = 0.7f, tilt = 2f),
304                 Tanh(maxProgress = 0.2f, tilt = 3f),
305             )
306         }
307     }
308 }
309 
restoreSpringConfigurationsnull310 private fun Any?.restoreSpringConfigurations(): DemoSpringConfigurations {
311     val p = (this as String).split(",").map { it.toFloat() }
312     return DemoSpringConfigurations(
313         name = "Custom",
314         systemUiSprings = SpringConfiguration(stiffness = p[0], dampingRatio = p[1]),
315         notificationSprings = SpringConfiguration(stiffness = p[2], dampingRatio = p[3]),
316     )
317 }
318 
restoreOverscrollProgressnull319 private fun Any?.restoreOverscrollProgress(): DemoOverscrollProgress {
320     val (name, paramsString) = (this as String).split(":")
321     val p = paramsString.split(",")
322     return when (name) {
323         "linear" -> DemoOverscrollProgress.Linear
324         "tanh" -> DemoOverscrollProgress.Tanh(maxProgress = p[0].toFloat(), tilt = p[1].toFloat())
325         "rubberBand" -> DemoOverscrollProgress.RubberBand(factor = p[0].toFloat())
326         else -> error("Unknown OverscrollProgress $name ($paramsString)")
327     }
328 }
329 
330 data class SpringConfiguration(val stiffness: Float, val dampingRatio: Float)
331 
332 @Composable
DemoConfigurationDialognull333 fun DemoConfigurationDialog(
334     configuration: DemoConfiguration,
335     onConfigurationChange: (DemoConfiguration) -> Unit,
336     onDismissRequest: () -> Unit,
337 ) {
338     AlertDialog(
339         onDismissRequest = onDismissRequest,
340         title = { Text("Demo configuration") },
341         text = {
342             Column(Modifier.fillMaxWidth().verticalScroll(rememberScrollState())) {
343                 Text(text = "Generic app settings", style = MaterialTheme.typography.titleMedium)
344 
345                 // Fullscreen.
346                 Checkbox(
347                     label = "Fullscreen",
348                     checked = configuration.isFullscreen,
349                     onCheckedChange = {
350                         onConfigurationChange(
351                             configuration.copy(isFullscreen = !configuration.isFullscreen)
352                         )
353                     },
354                 )
355 
356                 // Can change scene.
357                 Checkbox(
358                     label = "Can change scene or overlays",
359                     checked = configuration.canChangeSceneOrOverlays,
360                     onCheckedChange = {
361                         onConfigurationChange(
362                             configuration.copy(
363                                 canChangeSceneOrOverlays = !configuration.canChangeSceneOrOverlays
364                             )
365                         )
366                     },
367                 )
368 
369                 // Overlays.
370                 Checkbox(
371                     label = "Overlays",
372                     checked = configuration.enableOverlays,
373                     onCheckedChange = {
374                         onConfigurationChange(
375                             configuration.copy(enableOverlays = !configuration.enableOverlays)
376                         )
377                     },
378                 )
379 
380                 // Transition border.
381                 Checkbox(
382                     label = "Transition border",
383                     checked = configuration.transitionBorder,
384                     onCheckedChange = {
385                         onConfigurationChange(
386                             configuration.copy(transitionBorder = !configuration.transitionBorder)
387                         )
388                     },
389                 )
390 
391                 Text(text = "Springs", style = MaterialTheme.typography.titleMedium)
392 
393                 SpringsPicker(
394                     value = configuration.springConfigurations,
395                     onValue = {
396                         onConfigurationChange(configuration.copy(springConfigurations = it))
397                     },
398                 )
399 
400                 Text(text = "Scrollable", style = MaterialTheme.typography.titleMedium)
401 
402                 // Interception threshold.
403                 val thresholdString =
404                     String.format("%.2f", configuration.transitionInterceptionThreshold)
405                 Text(text = "Interception threshold: $thresholdString")
406                 Slider(
407                     value = configuration.transitionInterceptionThreshold,
408                     onValueChange = {
409                         onConfigurationChange(
410                             configuration.copy(transitionInterceptionThreshold = it)
411                         )
412                     },
413                     valueRange = 0f..0.5f,
414                     stepSize = 0.01f,
415                 )
416 
417                 Text(text = "Overscroll", style = MaterialTheme.typography.titleMedium)
418 
419                 Checkbox(
420                     label = "Use Overscroll Spec",
421                     checked = configuration.useOverscrollSpec,
422                     onCheckedChange = {
423                         onConfigurationChange(configuration.copy(useOverscrollSpec = it))
424                     },
425                 )
426 
427                 OverscrollProgressPicker(
428                     value = configuration.overscrollProgressConverter,
429                     onValue = {
430                         onConfigurationChange(configuration.copy(overscrollProgressConverter = it))
431                     },
432                 )
433 
434                 Text(text = "Media", style = MaterialTheme.typography.titleMedium)
435 
436                 // Whether we should show the media player.
437                 Checkbox(
438                     label = "Show media player",
439                     checked = configuration.showMediaPlayer,
440                     onCheckedChange = {
441                         onConfigurationChange(
442                             configuration.copy(showMediaPlayer = !configuration.showMediaPlayer)
443                         )
444                     },
445                 )
446 
447                 Text(text = "Notifications", style = MaterialTheme.typography.titleMedium)
448 
449                 // Whether notifications are interactive
450                 Checkbox(
451                     label = "Interactive notifications",
452                     checked = configuration.interactiveNotifications,
453                     onCheckedChange = {
454                         onConfigurationChange(
455                             configuration.copy(
456                                 interactiveNotifications = !configuration.interactiveNotifications
457                             )
458                         )
459                     },
460                 )
461 
462                 // Number of notifications in the Shade scene.
463                 Counter(
464                     "# notifications in Shade",
465                     configuration.notificationsInShade,
466                     onValueChange = {
467                         onConfigurationChange(configuration.copy(notificationsInShade = it))
468                     },
469                 )
470 
471                 // Number of notifications in the Lockscreen scene.
472                 Counter(
473                     "# notifications in Lockscreen",
474                     configuration.notificationsInLockscreen,
475                     onValueChange = {
476                         onConfigurationChange(configuration.copy(notificationsInLockscreen = it))
477                     },
478                 )
479 
480                 Text(text = "Quick Settings", style = MaterialTheme.typography.titleMedium)
481 
482                 Counter(
483                     "# quick settings rows",
484                     configuration.quickSettingsRows,
485                     onValueChange = {
486                         onConfigurationChange(configuration.copy(quickSettingsRows = it))
487                     },
488                 )
489 
490                 Text(text = "Lockscreen", style = MaterialTheme.typography.titleMedium)
491 
492                 // Whether the LS => Shade transition requires a full distance swipe to be
493                 // committed.
494                 Checkbox(
495                     label = "Require full LS => Shade swipe",
496                     state = configuration.lsToShadeRequiresFullSwipe,
497                     onStateChange = {
498                         onConfigurationChange(configuration.copy(lsToShadeRequiresFullSwipe = it))
499                     },
500                 )
501             }
502         },
503         confirmButton = { Button(onClick = { onDismissRequest() }) { Text("Done") } },
504         dismissButton = {
505             Button(onClick = { onConfigurationChange(DemoConfiguration()) }) { Text("Reset") }
506         },
507     )
508 }
509 
510 @Composable
Checkboxnull511 private fun Checkbox(
512     label: String,
513     checked: Boolean,
514     onCheckedChange: (Boolean) -> Unit,
515     modifier: Modifier = Modifier,
516 ) {
517     Row(
518         modifier
519             .fillMaxWidth()
520             .clip(MaterialTheme.shapes.small)
521             .clickable(onClick = { onCheckedChange(!checked) }),
522         verticalAlignment = Alignment.CenterVertically,
523     ) {
524         Checkbox(checked, onCheckedChange)
525         Text(label, Modifier.padding(start = 8.dp))
526     }
527 }
528 
529 @Composable
Checkboxnull530 private fun Checkbox(
531     label: String,
532     state: ToggleableState,
533     onStateChange: (ToggleableState) -> Unit,
534     modifier: Modifier = Modifier,
535 ) {
536     fun onClick() {
537         onStateChange(
538             when (state) {
539                 ToggleableState.On -> ToggleableState.Off
540                 ToggleableState.Off -> ToggleableState.Indeterminate
541                 ToggleableState.Indeterminate -> ToggleableState.On
542             }
543         )
544     }
545 
546     Row(
547         modifier.fillMaxWidth().clip(MaterialTheme.shapes.small).clickable(onClick = ::onClick),
548         verticalAlignment = Alignment.CenterVertically,
549     ) {
550         TriStateCheckbox(state, onClick = ::onClick)
551         Text(label, Modifier.padding(start = 8.dp))
552     }
553 }
554 
555 @Composable
Counternull556 private fun Counter(
557     label: String,
558     value: Int,
559     onValueChange: (Int) -> Unit,
560     modifier: Modifier = Modifier,
561 ) {
562     Row(modifier, verticalAlignment = Alignment.CenterVertically) {
563         IconButton(onClick = { onValueChange((value - 1).coerceAtLeast(0)) }) {
564             Icon(Icons.Default.Remove, null)
565         }
566         Text(value.toString(), Modifier.width(18.dp), textAlign = TextAlign.Center)
567         IconButton(onClick = { onValueChange((value + 1)) }) { Icon(Icons.Default.Add, null) }
568         Text(label, Modifier.padding(start = 8.dp))
569     }
570 }
571 
572 @Composable
Slidernull573 private fun <T> Slider(
574     value: T,
575     onValueChange: (T) -> Unit,
576     values: List<T>,
577     onValueNotFound: () -> Int = { 0 },
578 ) {
579     Slider(
<lambda>null580         value = (values.indexOf(value).takeIf { it != -1 } ?: onValueNotFound()).toFloat(),
<lambda>null581         onValueChange = { onValueChange(values[it.roundToInt()]) },
582         valueRange = 0f..values.lastIndex.toFloat(),
583         steps = values.lastIndex - 1,
584     )
585 }
586 
587 @Composable
Slidernull588 private fun Slider(
589     value: Float,
590     onValueChange: (Float) -> Unit,
591     valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
592     stepSize: Float,
593 ) {
594     Slider(
595         value = value,
596         onValueChange = onValueChange,
597         valueRange = valueRange,
598         steps = ((valueRange.endInclusive - valueRange.start) / stepSize).toInt() - 1,
599     )
600 }
601 
602 @Composable
SpringsPickernull603 fun SpringsPicker(value: DemoSpringConfigurations, onValue: (DemoSpringConfigurations) -> Unit) {
604     Text(text = "Selected: ${value.name}")
605     Slider(value = value, onValueChange = onValue, values = DemoSpringConfigurations.presets)
606 
607     var isExpanded by remember { mutableStateOf(value.name == "Custom") }
608     DisposableEffect(value) { onDispose { isExpanded = true } }
609     if (!isExpanded) FilledTonalButton(onClick = { isExpanded = true }) { Text("Show more") }
610 
611     AnimatedVisibility(isExpanded, enter = expandVertically()) {
612         Column(
613             Modifier.animateContentSize()
614                 .border(1.dp, color = MaterialTheme.colorScheme.primary, RoundedCornerShape(8.dp))
615                 .padding(4.dp)
616                 .fillMaxWidth()
617         ) {
618             Text(text = "System Ui")
619 
620             Text(
621                 text =
622                     buildString {
623                         append("stiffness: ")
624                         append(String.format("%.2f", value.systemUiSprings.stiffness))
625                         append(" (")
626                         append(
627                             DemoSpringConfigurations.stiffnessNames[value.systemUiSprings.stiffness]
628                         )
629                         append(")")
630                     }
631             )
632 
633             Slider(
634                 value = value.systemUiSprings.stiffness,
635                 onValueChange = {
636                     onValue(
637                         value.copy(
638                             name = "Custom",
639                             systemUiSprings = value.systemUiSprings.copy(stiffness = it),
640                         )
641                     )
642                 },
643                 values = DemoSpringConfigurations.stiffnessValues,
644             )
645 
646             Text(
647                 text =
648                     buildString {
649                         append("dampingRatio: ")
650                         append(String.format("%.2f", value.systemUiSprings.dampingRatio))
651                         append(" (")
652                         append(
653                             DemoSpringConfigurations.dampingRatioNames[
654                                     value.systemUiSprings.dampingRatio]
655                         )
656                         append(")")
657                     }
658             )
659 
660             Slider(
661                 value = value.systemUiSprings.dampingRatio,
662                 onValueChange = {
663                     onValue(
664                         value.copy(
665                             name = "Custom",
666                             systemUiSprings = value.systemUiSprings.copy(dampingRatio = it),
667                         )
668                     )
669                 },
670                 DemoSpringConfigurations.dampingRatioValues,
671             )
672 
673             Text(text = "Notification")
674 
675             Text(
676                 text =
677                     buildString {
678                         append("stiffness: ")
679                         append(String.format("%.2f", value.notificationSprings.stiffness))
680                         append(" (")
681                         append(
682                             DemoSpringConfigurations.stiffnessNames[
683                                     value.notificationSprings.stiffness]
684                         )
685                         append(")")
686                     }
687             )
688 
689             Slider(
690                 value = value.notificationSprings.stiffness,
691                 onValueChange = {
692                     onValue(
693                         value.copy(
694                             name = "Custom",
695                             notificationSprings = value.notificationSprings.copy(stiffness = it),
696                         )
697                     )
698                 },
699                 DemoSpringConfigurations.stiffnessValues,
700             )
701 
702             Text(
703                 text =
704                     buildString {
705                         append("dampingRatio: ")
706                         append(String.format("%.2f", value.notificationSprings.dampingRatio))
707                         append(" (")
708                         append(
709                             DemoSpringConfigurations.dampingRatioNames[
710                                     value.notificationSprings.dampingRatio]
711                         )
712                         append(")")
713                     }
714             )
715 
716             Slider(
717                 value = value.notificationSprings.dampingRatio,
718                 onValueChange = {
719                     onValue(
720                         value.copy(
721                             name = "Custom",
722                             notificationSprings = value.notificationSprings.copy(dampingRatio = it),
723                         )
724                     )
725                 },
726                 DemoSpringConfigurations.dampingRatioValues,
727             )
728         }
729     }
730 }
731 
732 @Composable
OverscrollProgressPickernull733 fun OverscrollProgressPicker(
734     value: DemoOverscrollProgress,
735     onValue: (DemoOverscrollProgress) -> Unit,
736 ) {
737     Text(text = "Overscroll progress")
738     val presets = DemoOverscrollProgress.presets
739     Slider(
740         value = value,
741         onValueChange = onValue,
742         values = DemoOverscrollProgress.presets,
743         onValueNotFound = { presets.indexOfFirst { it.name == value.name } },
744     )
745 
746     var isExpanded by remember { mutableStateOf(false) }
747     DisposableEffect(value) { onDispose { isExpanded = true } }
748 
749     Column(
750         Modifier.animateContentSize()
751             .border(1.dp, color = MaterialTheme.colorScheme.primary, RoundedCornerShape(8.dp))
752             .padding(4.dp)
753             .fillMaxWidth()
754     ) {
755         Text(text = "Function name: ${value.name}")
756         when (value) {
757             DemoOverscrollProgress.Linear -> {}
758             is DemoOverscrollProgress.Tanh -> {
759                 Text(text = "Max progress: ${String.format("%.2f", value.maxProgress)}")
760                 if (isExpanded) {
761                     Slider(
762                         value = value.maxProgress,
763                         onValueChange = { onValue(value.copy(maxProgress = it)) },
764                         valueRange = 0.05f..3f,
765                         stepSize = 0.05f,
766                     )
767                 }
768                 Text(text = "Tilt: ${String.format("%.2f", value.tilt)}")
769                 if (isExpanded) {
770                     Slider(
771                         value = value.tilt,
772                         onValueChange = { onValue(value.copy(tilt = it)) },
773                         valueRange = 1f..5f,
774                         stepSize = 0.1f,
775                     )
776                 }
777             }
778             is DemoOverscrollProgress.RubberBand -> {
779                 Text(text = "Factor: ${String.format("%.2f", value.factor)}")
780                 if (isExpanded) {
781                     Slider(
782                         value = value.factor,
783                         onValueChange = { onValue(value.copy(factor = it)) },
784                         valueRange = 0.01f..1f,
785                         stepSize = 0.01f,
786                     )
787                 }
788             }
789         }
790     }
791 
792     if (!isExpanded) FilledTonalButton(onClick = { isExpanded = true }) { Text("Show more") }
793 }
794