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