1 /*
<lambda>null2 * Copyright 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 android.content.Context
22 import androidx.activity.compose.BackHandler
23 import androidx.compose.foundation.LocalOverscrollFactory
24 import androidx.compose.foundation.OverscrollEffect
25 import androidx.compose.foundation.border
26 import androidx.compose.foundation.clickable
27 import androidx.compose.foundation.horizontalScroll
28 import androidx.compose.foundation.layout.Arrangement
29 import androidx.compose.foundation.layout.Box
30 import androidx.compose.foundation.layout.Column
31 import androidx.compose.foundation.layout.Row
32 import androidx.compose.foundation.layout.fillMaxHeight
33 import androidx.compose.foundation.layout.padding
34 import androidx.compose.foundation.pager.rememberPagerState
35 import androidx.compose.foundation.rememberScrollState
36 import androidx.compose.foundation.shape.RoundedCornerShape
37 import androidx.compose.material.icons.Icons
38 import androidx.compose.material.icons.filled.AirplanemodeInactive
39 import androidx.compose.material.icons.filled.Bedtime
40 import androidx.compose.material.icons.filled.Bluetooth
41 import androidx.compose.material.icons.filled.ChargingStation
42 import androidx.compose.material.icons.filled.CreditCard
43 import androidx.compose.material.icons.filled.DoNotDisturb
44 import androidx.compose.material.icons.filled.FlashlightOff
45 import androidx.compose.material.icons.filled.Home
46 import androidx.compose.material.icons.filled.NearbyOff
47 import androidx.compose.material.icons.filled.NetworkWifi
48 import androidx.compose.material.icons.filled.PowerSettingsNew
49 import androidx.compose.material.icons.filled.ScreenRotation
50 import androidx.compose.material.icons.filled.Settings
51 import androidx.compose.material.icons.filled.Videocam
52 import androidx.compose.material.icons.filled.ZoomOutMap
53 import androidx.compose.material3.Button
54 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
55 import androidx.compose.material3.Icon
56 import androidx.compose.material3.IconButton
57 import androidx.compose.material3.LocalContentColor
58 import androidx.compose.material3.MaterialTheme
59 import androidx.compose.material3.MotionScheme
60 import androidx.compose.material3.Surface
61 import androidx.compose.material3.Text
62 import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
63 import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass
64 import androidx.compose.material3.windowsizeclass.WindowSizeClass
65 import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
66 import androidx.compose.runtime.Composable
67 import androidx.compose.runtime.CompositionLocalProvider
68 import androidx.compose.runtime.getValue
69 import androidx.compose.runtime.key
70 import androidx.compose.runtime.mutableStateOf
71 import androidx.compose.runtime.remember
72 import androidx.compose.runtime.rememberCoroutineScope
73 import androidx.compose.runtime.saveable.Saver
74 import androidx.compose.runtime.saveable.SaverScope
75 import androidx.compose.runtime.saveable.rememberSaveable
76 import androidx.compose.runtime.setValue
77 import androidx.compose.ui.Alignment
78 import androidx.compose.ui.Modifier
79 import androidx.compose.ui.draw.clip
80 import androidx.compose.ui.graphics.Color
81 import androidx.compose.ui.graphics.toComposeRect
82 import androidx.compose.ui.graphics.vector.ImageVector
83 import androidx.compose.ui.platform.LocalConfiguration
84 import androidx.compose.ui.platform.LocalContext
85 import androidx.compose.ui.platform.LocalDensity
86 import androidx.compose.ui.platform.LocalHapticFeedback
87 import androidx.compose.ui.platform.testTag
88 import androidx.compose.ui.semantics.semantics
89 import androidx.compose.ui.semantics.testTagsAsResourceId
90 import androidx.compose.ui.state.ToggleableState
91 import androidx.compose.ui.text.TextMeasurer
92 import androidx.compose.ui.text.rememberTextMeasurer
93 import androidx.compose.ui.unit.Density
94 import androidx.compose.ui.unit.dp
95 import androidx.window.layout.WindowMetricsCalculator
96 import com.android.compose.animation.scene.ContentScope
97 import com.android.compose.animation.scene.DefaultEdgeDetector
98 import com.android.compose.animation.scene.ElementKey
99 import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
100 import com.android.compose.animation.scene.OverlayKey
101 import com.android.compose.animation.scene.SceneKey
102 import com.android.compose.animation.scene.SceneTransitionLayout
103 import com.android.compose.animation.scene.SceneTransitions
104 import com.android.compose.animation.scene.demo.notification.NotificationList
105 import com.android.compose.animation.scene.demo.notification.notifications
106 import com.android.compose.animation.scene.demo.transitions.systemUiTransitions
107 import com.android.compose.gesture.effect.rememberOffsetOverscrollEffectFactory
108 import com.android.compose.modifiers.thenIf
109 import com.android.compose.windowsizeclass.calculateWindowSizeClass
110 import com.android.mechanics.behavior.VerticalExpandContainerSpec
111 import kotlin.math.max
112
113 object Scenes {
114 val AlwaysOnDisplay = SceneKey("AlwaysOnDisplay")
115 val Bouncer = SceneKey("Bouncer")
116 val Camera = SceneKey("Camera")
117 val Launcher = SceneKey("Launcher")
118 val Lockscreen = SceneKey("Lockscreen")
119 val SplitLockscreen = SceneKey("SplitLockscreen")
120 val QuickSettings = SceneKey("QuickSettings")
121 val Shade = SceneKey("Shade")
122 val SplitShade = SceneKey("SplitShade")
123
124 // Stub scenes on the start and end of the lockscreen.
125 val StubStart = SceneKey("StubStart")
126 val StubEnd = SceneKey("StubEnd")
127
128 val AllScenes =
129 listOf(
130 AlwaysOnDisplay,
131 Bouncer,
132 Camera,
133 Launcher,
134 Lockscreen,
135 SplitLockscreen,
136 QuickSettings,
137 Shade,
138 SplitShade,
139 StubStart,
140 StubEnd,
141 )
142 .associateBy { it.debugName }
143
144 /**
145 * A smart saver that restores the right scene depending on the current [lockscreenScene] and
146 * [shadeScene].
147 */
148 class SceneSaver(private val lockscreenScene: SceneKey, private val shadeScene: SceneKey) :
149 Saver<SceneKey, String> {
150 override fun SaverScope.save(value: SceneKey): String = value.debugName
151
152 override fun restore(value: String): SceneKey {
153 return ensureCorrectScene(AllScenes.getValue(value), lockscreenScene, shadeScene)
154 }
155 }
156
157 fun ensureCorrectScene(
158 scene: SceneKey,
159 lockscreenScene: SceneKey,
160 shadeScene: SceneKey,
161 ): SceneKey {
162 return when (scene) {
163 Lockscreen,
164 SplitLockscreen -> lockscreenScene
165 Shade,
166 SplitShade -> shadeScene
167 // We should never be in the QuickSettings page if the SplitShade is a possible scene.
168 QuickSettings -> if (shadeScene == SplitShade) SplitShade else QuickSettings
169 else -> scene
170 }
171 }
172 }
173
174 object Overlays {
175 val Notifications = OverlayKey("NotificationsOverlay")
176 val QuickSettings = OverlayKey("QuickSettingsOverlay")
177 }
178
179 /** A [Saver] that restores a [MutableSceneTransitionLayoutState] to its previous [currentScene]. */
180 class MutableSceneTransitionLayoutSaver(
181 private val sceneSaver: Scenes.SceneSaver,
182 private val transitions: SceneTransitions,
183 private val canChangeScene: (SceneKey) -> Boolean,
184 private val motionScheme: MotionScheme,
185 ) : Saver<MutableSceneTransitionLayoutState, String> {
savenull186 override fun SaverScope.save(state: MutableSceneTransitionLayoutState): String {
187 val currentScene = state.transitionState.currentScene
188 return with(sceneSaver) { save(currentScene) }
189 }
190
restorenull191 override fun restore(value: String): MutableSceneTransitionLayoutState {
192 val currentScene = sceneSaver.restore(value)
193 return MutableSceneTransitionLayoutState(
194 initialScene = currentScene,
195 motionScheme = motionScheme,
196 transitions = transitions,
197 canChangeScene = canChangeScene,
198 )
199 }
200 }
201
202 @Composable
SystemUinull203 fun SystemUi(modifier: Modifier = Modifier) {
204 var configuration by
205 rememberSaveable(stateSaver = DemoConfiguration.Saver) {
206 mutableStateOf(DemoConfiguration())
207 }
208 SystemUi(configuration, { configuration = it }, modifier)
209 }
210
211 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
212 @Composable
SystemUinull213 fun SystemUi(
214 configuration: DemoConfiguration,
215 onConfigurationChange: (DemoConfiguration) -> Unit,
216 modifier: Modifier = Modifier,
217 initialScene: SceneKey? = null,
218 ) {
219 val windowSizeClass = calculateWindowSizeClass()
220 val shouldUseSplitScenes = shouldUseSplitScenes(windowSizeClass)
221
222 val lockscreenScene: SceneKey
223 val shadeScene: SceneKey
224 val launcherColumns: Int
225 if (shouldUseSplitScenes) {
226 lockscreenScene = Scenes.SplitLockscreen
227 shadeScene = Scenes.SplitShade
228 launcherColumns = 8
229 } else {
230 lockscreenScene = Scenes.Lockscreen
231 shadeScene = Scenes.Shade
232 launcherColumns = 4
233 }
234
235 val notificationCountInLockscreen = configuration.notificationsInLockscreen
236 val notificationCount = max(notificationCountInLockscreen, configuration.notificationsInShade)
237 val interactiveNotifications = configuration.interactiveNotifications
238 val notificationTextMeasurer = rememberTextMeasurer(cacheSize = notificationCount * 2)
239 val motionScheme = MaterialTheme.motionScheme
240 val notifications =
241 remember(
242 interactiveNotifications,
243 notificationCount,
244 notificationCountInLockscreen,
245 notificationTextMeasurer,
246 motionScheme,
247 ) {
248 notifications(
249 interactiveNotifications,
250 notificationCount,
251 notificationCountInLockscreen,
252 notificationTextMeasurer,
253 motionScheme,
254 )
255 }
256 val expectedQsSize = 12
257 val quickSettingsTextMeasurer = rememberTextMeasurer(cacheSize = expectedQsSize * 2)
258 val quickSettingsTiles = remember { quickSettingsTiles(quickSettingsTextMeasurer) }
259 check(expectedQsSize == quickSettingsTiles.size)
260
261 var isLockscreenDismissable by remember { mutableStateOf(false) }
262 var isLockscreenDismissed by remember { mutableStateOf(false) }
263 var showConfigurationDialog by remember { mutableStateOf(false) }
264
265 val nQuickSettingsColumns =
266 if (configuration.enableOverlays) {
267 2
268 } else {
269 when (windowSizeClass.widthSizeClass) {
270 WindowWidthSizeClass.Compact -> 2
271 WindowWidthSizeClass.Medium,
272 WindowWidthSizeClass.Expanded ->
273 when (windowSizeClass.heightSizeClass) {
274 // Phone landscape.
275 WindowHeightSizeClass.Compact -> 2
276 else -> 3
277 }
278
279 else -> error("Unknown size class: ${windowSizeClass.widthSizeClass}")
280 }
281 }
282 val nQuickSettingsRow = configuration.quickSettingsRows
283 val nQuickSettingsSplitShadeRows = nQuickSettingsColumns
284
285 // The state of the quick settings pager in the phone (one column) layout.
286 val nQuickSettingsPages =
287 nQuickSettingsPages(
288 nTiles = quickSettingsTiles.size,
289 nRows = nQuickSettingsRow,
290 nColumns = nQuickSettingsColumns,
291 )
292 val quickSettingsPagerState = rememberPagerState { nQuickSettingsPages }
293
294 val hapticFeedback = LocalHapticFeedback.current
295 val revealHaptics = remember(hapticFeedback) { DemoContainerRevealHaptics(hapticFeedback) }
296 val shadeMotionSpec =
297 remember(shouldUseSplitScenes) {
298 VerticalExpandContainerSpec(isFloating = shouldUseSplitScenes)
299 }
300 val transitions =
301 remember(quickSettingsPagerState, revealHaptics, shouldUseSplitScenes) {
302 systemUiTransitions(quickSettingsPagerState, revealHaptics, shadeMotionSpec)
303 }
304
305 val sceneSaver =
306 remember(lockscreenScene, shadeScene) { Scenes.SceneSaver(lockscreenScene, shadeScene) }
307
308 fun maybeUpdateLockscreenDismissed(scene: SceneKey) {
309 when (scene) {
310 Scenes.Launcher -> isLockscreenDismissed = true
311 Scenes.Lockscreen,
312 Scenes.SplitLockscreen -> isLockscreenDismissed = false
313
314 else -> {}
315 }
316 }
317
318 val canChangeScene =
319 remember(configuration) {
320 { scene: SceneKey ->
321 if (configuration.canChangeSceneOrOverlays) {
322 maybeUpdateLockscreenDismissed(scene)
323 true
324 } else {
325 false
326 }
327 }
328 }
329
330 val stateSaver =
331 remember(sceneSaver, transitions, canChangeScene, motionScheme) {
332 MutableSceneTransitionLayoutSaver(
333 sceneSaver = sceneSaver,
334 transitions = transitions,
335 canChangeScene = canChangeScene,
336 motionScheme = motionScheme,
337 )
338 }
339 val layoutState =
340 rememberSaveable(
341 transitions,
342 canChangeScene,
343 configuration,
344 motionScheme,
345 saver = stateSaver,
346 ) {
347 val initialScene =
348 initialScene?.let {
349 Scenes.ensureCorrectScene(
350 initialScene,
351 lockscreenScene = lockscreenScene,
352 shadeScene = shadeScene,
353 )
354 } ?: lockscreenScene
355
356 MutableSceneTransitionLayoutState(
357 initialScene = initialScene,
358 motionScheme = motionScheme,
359 transitions = transitions,
360 canChangeScene = canChangeScene,
361 canShowOverlay = { configuration.canChangeSceneOrOverlays },
362 canHideOverlay = { configuration.canChangeSceneOrOverlays },
363 canReplaceOverlay = { _, _ -> configuration.canChangeSceneOrOverlays },
364 deferTransitionProgress = configuration.deferTransitionProgress,
365 )
366 }
367
368 val coroutineScope = rememberCoroutineScope()
369 fun onChangeScene(scene: SceneKey) {
370 maybeUpdateLockscreenDismissed(scene)
371
372 // Enforce that we are going to the right shade/lockscreen here depending on the windows
373 // size class.
374 layoutState.setTargetScene(
375 Scenes.ensureCorrectScene(scene, lockscreenScene, shadeScene),
376 coroutineScope,
377 )
378 }
379
380 fun onPowerButtonClicked() {
381 isLockscreenDismissable = false
382 isLockscreenDismissed = false
383
384 if (layoutState.transitionState.currentScene == Scenes.AlwaysOnDisplay) {
385 onChangeScene(lockscreenScene)
386 } else {
387 onChangeScene(Scenes.AlwaysOnDisplay)
388 }
389 }
390
391 fun onSettingsButtonClicked() {
392 showConfigurationDialog = true
393 }
394
395 fun onExpandButtonClicked() {
396 onConfigurationChange(configuration.copy(isFullscreen = true))
397 }
398
399 @Composable
400 fun ContentScope.NotificationList(
401 maxNotificationCount: Int,
402 isScrollable: Boolean = true,
403 overscrollEffect: OverscrollEffect? = null,
404 ) {
405 NotificationList(
406 notifications = notifications,
407 maxNotificationCount = maxNotificationCount,
408 demoConfiguration = configuration,
409 isScrollable = isScrollable,
410 overscrollEffect = overscrollEffect,
411 )
412 }
413
414 if (showConfigurationDialog) {
415 DemoConfigurationDialog(
416 configuration,
417 onConfigurationChange,
418 onDismissRequest = { showConfigurationDialog = false },
419 )
420 }
421
422 Column(modifier) {
423 if (!configuration.isFullscreen) {
424 Row(Modifier.horizontalScroll(rememberScrollState())) {
425 IconButton(::onPowerButtonClicked) { Icon(Icons.Default.PowerSettingsNew, null) }
426 IconButton(::onSettingsButtonClicked) { Icon(Icons.Default.Settings, null) }
427 IconButton(::onExpandButtonClicked) { Icon(Icons.Default.ZoomOutMap, null) }
428
429 Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
430 listOf(
431 Scenes.AlwaysOnDisplay to "AOD",
432 Scenes.Lockscreen to "Lock",
433 Scenes.Bouncer to "Bouncer",
434 Scenes.Launcher to "Gone",
435 Scenes.Shade to "Shade",
436 Scenes.QuickSettings to "QS",
437 )
438 .forEach { (scene, name) ->
439 Button(onClick = { onChangeScene(scene) }) { Text(name) }
440 }
441
442 listOf(Overlays.Notifications to "NS", Overlays.QuickSettings to "QSS")
443 .forEach { (overlay, name) ->
444 Button(
445 onClick = {
446 if (layoutState.currentOverlays.contains(overlay)) {
447 layoutState.hideOverlay(overlay, coroutineScope)
448 } else {
449 layoutState.showOverlay(overlay, coroutineScope)
450 }
451 }
452 ) {
453 Text(name)
454 }
455 }
456 }
457 }
458 }
459
460 // Provide an easy way to leave full screen mode by going back.
461 BackHandler(enabled = configuration.isFullscreen) {
462 onConfigurationChange(configuration.copy(isFullscreen = false))
463 }
464
465 val shape = RoundedCornerShape(Shade.Dimensions.ScrimCornerSize)
466 val borderColor = MaterialTheme.colorScheme.onSurface
467
468 Surface(
469 Modifier.semantics { testTagsAsResourceId = true }
470 .thenIf(!configuration.isFullscreen) {
471 Modifier.padding(3.dp)
472 .then(
473 if (configuration.transitionBorder) {
474 Modifier.border(
475 5.dp,
476 if (layoutState.isTransitioning()) Color.Red else Color.Green,
477 shape,
478 )
479 } else {
480 Modifier.border(1.dp, borderColor, shape)
481 }
482 )
483 .clip(shape)
484 },
485 color = MaterialTheme.colorScheme.surfaceVariant,
486 ) {
487 val stretchOverscrollFactory = LocalOverscrollFactory.current
488 CompositionLocalProvider(
489 LocalContentColor provides MaterialTheme.colorScheme.onSurface,
490 LocalOverscrollFactory provides rememberOffsetOverscrollEffectFactory(),
491 ) {
492 var isMediaPlayerPlaying by remember { mutableStateOf(false) }
493 val mediaPlayer:
494 (@Composable
495 ContentScope.(presentationStyle: DemoMediaPresentationStyle) -> Unit)? =
496 if (configuration.showMediaPlayer) {
497 { presentationStyle ->
498 MediaPlayer(
499 presentationStyle = presentationStyle,
500 isPlaying = isMediaPlayerPlaying,
501 onIsPlayingChange = { isMediaPlayerPlaying = it },
502 onVisibilityChange = { isVisible ->
503 onConfigurationChange(
504 configuration.copy(showMediaPlayer = isVisible)
505 )
506 },
507 )
508 }
509 } else {
510 null
511 }
512 val defaultMediaPlayer: (@Composable ContentScope.() -> Unit)? =
513 mediaPlayer?.let { { it(DemoMediaPresentationStyle.Default) } }
514 val compactMediaPlayer: (@Composable ContentScope.() -> Unit)? =
515 mediaPlayer?.let { { it(DemoMediaPresentationStyle.Compact) } }
516
517 val qsPager: (@Composable ContentScope.() -> Unit) = {
518 QuickSettingsPager(
519 pagerState = quickSettingsPagerState,
520 tiles = quickSettingsTiles,
521 nRows = nQuickSettingsRow,
522 nColumns = nQuickSettingsColumns,
523 )
524 }
525
526 // SceneTransitionLayout can only be bound to one SceneTransitionLayoutState, so
527 // make sure we recompose it fully when we create a new state object.
528 key(layoutState) {
529 val overlayEffectFactory =
530 if (shouldUseSplitScenes) null else stretchOverscrollFactory
531
532 SceneTransitionLayout(
533 state = layoutState,
534 transitionInterceptionThreshold =
535 configuration.transitionInterceptionThreshold,
536 modifier =
537 // Make this layout accessible to UiAutomator.
538 Modifier.thenIf(layoutState.currentTransition == null) {
539 Modifier.testTag("SystemUiSceneTransitionLayout:idle")
540 },
541 swipeSourceDetector =
542 if (configuration.enableOverlays) {
543 remember { SceneContainerSwipeDetector(edgeSize = 60.dp) }
544 } else {
545 DefaultEdgeDetector
546 },
547 implicitTestTags = true,
548 ) {
549 scene(Scenes.Launcher, Launcher.userActions(shadeScene, configuration)) {
550 FirstCompositionDelay(configuration)
551 Launcher(launcherColumns)
552 }
553 scene(
554 Scenes.Lockscreen,
555 Lockscreen.userActions(
556 isLockscreenDismissable,
557 shadeScene,
558 requiresFullDistanceSwipeToShade =
559 when (configuration.lsToShadeRequiresFullSwipe) {
560 ToggleableState.On -> true
561 ToggleableState.Off -> false
562 ToggleableState.Indeterminate ->
563 configuration.interactiveNotifications
564 },
565 configuration,
566 ),
567 ) {
568 FirstCompositionDelay(configuration)
569 Lockscreen(
570 notificationList = {
571 NotificationList(
572 maxNotificationCount =
573 configuration.notificationsInLockscreen
574 )
575 },
576 mediaPlayer = defaultMediaPlayer,
577 isDismissable = isLockscreenDismissable,
578 onToggleDismissable = {
579 isLockscreenDismissable = !isLockscreenDismissable
580 },
581 ::onChangeScene,
582 )
583 }
584 scene(
585 Scenes.SplitLockscreen,
586 SplitLockscreen.userActions(
587 isLockscreenDismissable,
588 shadeScene,
589 configuration,
590 ),
591 ) {
592 FirstCompositionDelay(configuration)
593 SplitLockscreen(
594 notificationList = {
595 NotificationList(
596 maxNotificationCount =
597 configuration.notificationsInLockscreen
598 )
599 },
600 mediaPlayer = defaultMediaPlayer,
601 isDismissable = isLockscreenDismissable,
602 onToggleDismissable = {
603 isLockscreenDismissable = !isLockscreenDismissable
604 },
605 ::onChangeScene,
606 configuration = configuration,
607 )
608 }
609 scene(Scenes.StubStart, Stub.startUserActions(lockscreenScene)) {
610 FirstCompositionDelay(configuration)
611 Stub(
612 rootKey = Stub.Elements.SceneStart,
613 textKey = Stub.Elements.TextStart,
614 text = "Stub scene (start)",
615 )
616 }
617 scene(Scenes.StubEnd, Stub.endUserActions(lockscreenScene)) {
618 FirstCompositionDelay(configuration)
619 Stub(
620 rootKey = Stub.Elements.SceneEnd,
621 textKey = Stub.Elements.TextEnd,
622 text = "Stub scene (end)",
623 )
624 }
625 scene(Scenes.Camera, Camera.userActions(lockscreenScene)) {
626 FirstCompositionDelay(configuration)
627 Camera()
628 }
629 scene(Scenes.Bouncer, Bouncer.userActions(lockscreenScene)) {
630 FirstCompositionDelay(configuration)
631 Bouncer(
632 onBouncerCancelled = { onChangeScene(lockscreenScene) },
633 onBouncerSolved = { onChangeScene(Scenes.Launcher) },
634 )
635 }
636 scene(
637 Scenes.QuickSettings,
638 QuickSettings.userActions(
639 shadeScene,
640 lockscreenScene,
641 isLockscreenDismissed,
642 ),
643 ) {
644 FirstCompositionDelay(configuration)
645 QuickSettings(
646 qsPager,
647 mediaPlayer = defaultMediaPlayer,
648 ::onSettingsButtonClicked,
649 ::onPowerButtonClicked,
650 )
651 }
652 scene(
653 Scenes.Shade,
654 Shade.userActions(isLockscreenDismissed, lockscreenScene),
655 ) {
656 FirstCompositionDelay(configuration)
657 Shade(
658 notificationList = { overscrollEffect ->
659 NotificationList(
660 maxNotificationCount = configuration.notificationsInShade,
661 overscrollEffect = overscrollEffect,
662 )
663 },
664 mediaPlayer = defaultMediaPlayer,
665 quickSettingsTiles,
666 nQuickSettingsColumns,
667 )
668 }
669 scene(
670 Scenes.SplitShade,
671 SplitShade.userActions(isLockscreenDismissed, lockscreenScene),
672 ) {
673 FirstCompositionDelay(configuration)
674 SplitShade(
675 notificationList = {
676 NotificationList(
677 maxNotificationCount = configuration.notificationsInShade
678 )
679 },
680 mediaPlayer = defaultMediaPlayer,
681 quickSettingsTiles,
682 nQuickSettingsSplitShadeRows,
683 nQuickSettingsColumns,
684 ::onSettingsButtonClicked,
685 ::onPowerButtonClicked,
686 )
687 }
688
689 scene(Scenes.AlwaysOnDisplay) {
690 FirstCompositionDelay(configuration)
691 AlwaysOnDisplay(Modifier.clickable { onChangeScene(lockscreenScene) })
692 }
693
694 overlay(
695 Overlays.QuickSettings,
696 userActions = QuickSettingsShade.UserActions,
697 alignment = Alignment.TopEnd,
698 effectFactory = overlayEffectFactory,
699 ) {
700 FirstCompositionDelay(configuration)
701 QuickSettingsShade(qsPager, compactMediaPlayer)
702 }
703
704 overlay(
705 Overlays.Notifications,
706 userActions = NotificationShade.UserActions,
707 alignment = Alignment.TopStart,
708 effectFactory = overlayEffectFactory,
709 ) {
710 FirstCompositionDelay(configuration)
711 NotificationShade(
712 clock =
713 if (shouldUseSplitScenes) {
714 null
715 } else {
716 { Clock(MaterialTheme.colorScheme.onSurfaceVariant) }
717 },
718 mediaPlayer = defaultMediaPlayer,
719 notificationList = {
720 NotificationList(
721 maxNotificationCount = configuration.notificationsInShade,
722 isScrollable = false,
723 )
724 },
725 )
726 }
727 }
728
729 // Add 2 empty boxes for each half of the STL. This is used by overlay benchmark
730 // tests to swipe on the start or end half of the STL.
731 Row {
732 Box(Modifier.testTag("StlStartHalf").fillMaxHeight().weight(1f))
733 Box(Modifier.testTag("StlEndHalf").fillMaxHeight().weight(1f))
734 }
735 }
736 }
737 }
738 }
739 }
740
741 @Composable
FirstCompositionDelaynull742 private fun FirstCompositionDelay(configuration: DemoConfiguration) {
743 val delay = configuration.firstCompositionDelay
744 if (delay > 0L) {
745 remember<Any> { Thread.sleep(delay) }
746 }
747 }
748
749 // Adapted from [androidx.compose.material3.windowsizeclass.calculateWindowSizeClass].
750 @Composable
calculateWindowSizeClassnull751 internal fun calculateWindowSizeClass(): WindowSizeClass {
752 // Observe view configuration changes and recalculate the size class on each change. We can't
753 // use Activity#onConfigurationChanged as this will sometimes fail to be called on different
754 // API levels, hence why this function needs to be @Composable so we can observe the
755 // ComposeView's configuration changes.
756 LocalConfiguration.current
757
758 return calculateWindowSizeClass(LocalContext.current, LocalDensity.current)
759 }
760
761 @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
calculateWindowSizeClassnull762 fun calculateWindowSizeClass(context: Context, density: Density): WindowSizeClass {
763 val metrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(context)
764 val size = with(density) { metrics.bounds.toComposeRect().size.toDpSize() }
765 return WindowSizeClass.calculateFromSize(size)
766 }
767
shouldUseSplitScenesnull768 fun shouldUseSplitScenes(windowSizeClass: WindowSizeClass): Boolean {
769 return when (windowSizeClass.widthSizeClass) {
770 WindowWidthSizeClass.Compact,
771 WindowWidthSizeClass.Medium -> false
772 WindowWidthSizeClass.Expanded -> true
773 else -> error("Unknown size class: ${windowSizeClass.widthSizeClass}")
774 }
775 }
776
quickSettingsTilesnull777 private fun quickSettingsTiles(textMeasurer: TextMeasurer): List<QuickSettingsTileViewModel> {
778 return listOf(
779 quickSettingsTile(
780 textMeasurer = textMeasurer,
781 icon = Icons.Default.NetworkWifi,
782 title = "Internet",
783 description = "Google Guest",
784 isActive = true,
785 showChevron = true,
786 ),
787 quickSettingsTile(
788 textMeasurer = textMeasurer,
789 icon = Icons.Default.Bluetooth,
790 title = "Bluetooth",
791 isActive = true,
792 description = "On",
793 inactiveDescription = "Off",
794 ),
795 quickSettingsTile(
796 textMeasurer = textMeasurer,
797 icon = Icons.Default.DoNotDisturb,
798 title = "Do Not Disturb",
799 description = "On",
800 inactiveDescription = "Off",
801 ),
802 quickSettingsTile(
803 textMeasurer = textMeasurer,
804 icon = Icons.Default.FlashlightOff,
805 title = "Flashlight",
806 description = "On",
807 inactiveDescription = "Off",
808 ),
809 quickSettingsTile(
810 textMeasurer = textMeasurer,
811 icon = Icons.Default.AirplanemodeInactive,
812 title = "Airplane mode",
813 description = "On",
814 inactiveDescription = "Off",
815 ),
816 quickSettingsTile(
817 textMeasurer = textMeasurer,
818 icon = Icons.Default.Home,
819 title = "Home",
820 description = "1600 Amphitheatre Pkwy",
821 isActive = true,
822 showChevron = true,
823 ),
824 quickSettingsTile(
825 textMeasurer = textMeasurer,
826 icon = Icons.Default.CreditCard,
827 title = "GPay",
828 description = "•••• 0061",
829 isActive = true,
830 ),
831 quickSettingsTile(
832 textMeasurer = textMeasurer,
833 icon = Icons.Default.ScreenRotation,
834 title = "Auto-rotate",
835 isActive = true,
836 description = "On",
837 inactiveDescription = "Off",
838 ),
839 quickSettingsTile(
840 textMeasurer = textMeasurer,
841 icon = Icons.Default.Bedtime,
842 title = "Night Light",
843 description = "On",
844 inactiveDescription = "Off",
845 ),
846 quickSettingsTile(
847 textMeasurer = textMeasurer,
848 icon = Icons.Default.Videocam,
849 title = "Screen record",
850 description = "Start",
851 ),
852 quickSettingsTile(
853 textMeasurer = textMeasurer,
854 icon = Icons.Default.NearbyOff,
855 title = "Nearby Share",
856 showChevron = true,
857 ),
858 quickSettingsTile(
859 textMeasurer = textMeasurer,
860 icon = Icons.Default.ChargingStation,
861 title = "Battery Share",
862 description = "On",
863 inactiveDescription = "Off",
864 ),
865 )
866 }
867
quickSettingsTilenull868 private fun quickSettingsTile(
869 textMeasurer: TextMeasurer,
870 icon: ImageVector,
871 title: String,
872 key: ElementKey = ElementKey("Tile:$title", identity = QuickSettingsTileIdentity()),
873 isActive: Boolean = false,
874 description: String? = null,
875 inactiveDescription: String? = description,
876 showChevron: Boolean = false,
877 ): QuickSettingsTileViewModel {
878 val activeDescription = description
879
880 var isActive by mutableStateOf(isActive)
881 var description by mutableStateOf(if (isActive) activeDescription else inactiveDescription)
882
883 return object : QuickSettingsTileViewModel {
884 override val key: ElementKey = key
885 override val isActive: Boolean
886 get() = isActive
887
888 override val icon: ImageVector = icon
889 override val title: String = title
890 override val description: String?
891 get() = description
892
893 override val showChevron: Boolean = showChevron
894 override val onClick: () -> Unit = {
895 isActive = !isActive
896 description =
897 if (isActive) {
898 activeDescription
899 } else {
900 inactiveDescription
901 }
902 }
903
904 override val textMeasurer: TextMeasurer = textMeasurer
905 }
906 }
907