1 /*
2 * Copyright (C) 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 @file:OptIn(ExperimentalLayoutApi::class)
18
19 package com.android.systemui.shade.ui.composable
20
21 import androidx.compose.foundation.background
22 import androidx.compose.foundation.clickable
23 import androidx.compose.foundation.layout.Box
24 import androidx.compose.foundation.layout.ExperimentalLayoutApi
25 import androidx.compose.foundation.layout.PaddingValues
26 import androidx.compose.foundation.layout.Spacer
27 import androidx.compose.foundation.layout.WindowInsets
28 import androidx.compose.foundation.layout.asPaddingValues
29 import androidx.compose.foundation.layout.calculateEndPadding
30 import androidx.compose.foundation.layout.calculateStartPadding
31 import androidx.compose.foundation.layout.displayCutout
32 import androidx.compose.foundation.layout.fillMaxSize
33 import androidx.compose.foundation.layout.fillMaxWidth
34 import androidx.compose.foundation.layout.padding
35 import androidx.compose.foundation.layout.systemBarsIgnoringVisibility
36 import androidx.compose.foundation.layout.waterfall
37 import androidx.compose.foundation.layout.width
38 import androidx.compose.foundation.shape.RoundedCornerShape
39 import androidx.compose.material3.MaterialTheme
40 import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
41 import androidx.compose.runtime.Composable
42 import androidx.compose.runtime.ReadOnlyComposable
43 import androidx.compose.runtime.getValue
44 import androidx.compose.ui.Alignment
45 import androidx.compose.ui.Modifier
46 import androidx.compose.ui.draw.clip
47 import androidx.compose.ui.graphics.Color
48 import androidx.compose.ui.platform.LocalLayoutDirection
49 import androidx.compose.ui.unit.dp
50 import androidx.lifecycle.compose.collectAsStateWithLifecycle
51 import com.android.compose.animation.scene.ElementKey
52 import com.android.compose.animation.scene.LowestZIndexScenePicker
53 import com.android.compose.animation.scene.SceneScope
54 import com.android.compose.windowsizeclass.LocalWindowSizeClass
55 import com.android.systemui.keyguard.ui.composable.LockscreenContent
56 import com.android.systemui.scene.shared.model.Scenes
57 import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel
58 import com.android.systemui.util.kotlin.getOrNull
59 import dagger.Lazy
60 import java.util.Optional
61
62 /** The overlay shade renders a lightweight shade UI container on top of a background scene. */
63 @Composable
SceneScopenull64 fun SceneScope.OverlayShade(
65 viewModel: OverlayShadeViewModel,
66 panelAlignment: Alignment,
67 lockscreenContent: Lazy<Optional<LockscreenContent>>,
68 modifier: Modifier = Modifier,
69 content: @Composable () -> Unit,
70 ) {
71 val backgroundScene by viewModel.backgroundScene.collectAsStateWithLifecycle()
72
73 Box(modifier) {
74 if (backgroundScene == Scenes.Lockscreen) {
75 // Lockscreen content is optionally injected, because variants of System UI without a
76 // lockscreen cannot provide it.
77 val lockscreenContentOrNull = lockscreenContent.get().getOrNull()
78 lockscreenContentOrNull?.apply { Content(Modifier.fillMaxSize()) }
79 }
80
81 Scrim(onClicked = viewModel::onScrimClicked)
82
83 Box(
84 modifier = Modifier.fillMaxSize().panelPadding(),
85 contentAlignment = panelAlignment,
86 ) {
87 Panel(
88 modifier = Modifier.element(OverlayShade.Elements.Panel).panelSize(),
89 content = content
90 )
91 }
92 }
93 }
94
95 @Composable
Scrimnull96 private fun SceneScope.Scrim(
97 onClicked: () -> Unit,
98 modifier: Modifier = Modifier,
99 ) {
100 Spacer(
101 modifier =
102 modifier
103 .element(OverlayShade.Elements.Scrim)
104 .fillMaxSize()
105 .background(OverlayShade.Colors.ScrimBackground)
106 .clickable(onClick = onClicked, interactionSource = null, indication = null)
107 )
108 }
109
110 @Composable
SceneScopenull111 private fun SceneScope.Panel(
112 modifier: Modifier = Modifier,
113 content: @Composable () -> Unit,
114 ) {
115 Box(modifier = modifier.clip(OverlayShade.Shapes.RoundedCornerPanel)) {
116 Spacer(
117 modifier =
118 Modifier.element(OverlayShade.Elements.PanelBackground)
119 .matchParentSize()
120 .background(
121 color = OverlayShade.Colors.PanelBackground,
122 shape = OverlayShade.Shapes.RoundedCornerPanel,
123 ),
124 )
125
126 // This content is intentionally rendered as a separate element from the background in order
127 // to allow for more flexibility when defining transitions.
128 content()
129 }
130 }
131
132 @Composable
panelSizenull133 private fun Modifier.panelSize(): Modifier {
134 val widthSizeClass = LocalWindowSizeClass.current.widthSizeClass
135
136 return this.then(
137 when (widthSizeClass) {
138 WindowWidthSizeClass.Compact -> Modifier.fillMaxWidth()
139 WindowWidthSizeClass.Medium -> Modifier.width(OverlayShade.Dimensions.PanelWidthMedium)
140 WindowWidthSizeClass.Expanded -> Modifier.width(OverlayShade.Dimensions.PanelWidthLarge)
141 else -> error("Unsupported WindowWidthSizeClass \"$widthSizeClass\"")
142 }
143 )
144 }
145
146 @Composable
panelPaddingnull147 private fun Modifier.panelPadding(): Modifier {
148 val widthSizeClass = LocalWindowSizeClass.current.widthSizeClass
149 val systemBars = WindowInsets.systemBarsIgnoringVisibility
150 val displayCutout = WindowInsets.displayCutout
151 val waterfall = WindowInsets.waterfall
152 val contentPadding = PaddingValues(all = OverlayShade.Dimensions.ScrimContentPadding)
153
154 val combinedPadding =
155 combinePaddings(
156 systemBars.asPaddingValues(),
157 displayCutout.asPaddingValues(),
158 waterfall.asPaddingValues(),
159 contentPadding
160 )
161
162 return if (widthSizeClass == WindowWidthSizeClass.Compact) {
163 padding(bottom = combinedPadding.calculateBottomPadding())
164 } else {
165 padding(combinedPadding)
166 }
167 }
168
169 /** Creates a union of [paddingValues] by using the max padding of each edge. */
170 @Composable
combinePaddingsnull171 private fun combinePaddings(vararg paddingValues: PaddingValues): PaddingValues {
172 val layoutDirection = LocalLayoutDirection.current
173
174 return PaddingValues(
175 start = paddingValues.maxOfOrNull { it.calculateStartPadding(layoutDirection) } ?: 0.dp,
176 top = paddingValues.maxOfOrNull { it.calculateTopPadding() } ?: 0.dp,
177 end = paddingValues.maxOfOrNull { it.calculateEndPadding(layoutDirection) } ?: 0.dp,
178 bottom = paddingValues.maxOfOrNull { it.calculateBottomPadding() } ?: 0.dp
179 )
180 }
181
182 object OverlayShade {
183 object Elements {
184 val Scrim = ElementKey("OverlayShadeScrim", scenePicker = LowestZIndexScenePicker)
185 val Panel = ElementKey("OverlayShadePanel", scenePicker = LowestZIndexScenePicker)
186 val PanelBackground =
187 ElementKey("OverlayShadePanelBackground", scenePicker = LowestZIndexScenePicker)
188 }
189
190 object Colors {
191 val ScrimBackground = Color(0, 0, 0, alpha = 255 / 3)
192 val PanelBackground: Color
193 @Composable @ReadOnlyComposable get() = MaterialTheme.colorScheme.surfaceContainer
194 }
195
196 object Dimensions {
197 val ScrimContentPadding = 16.dp
198 val PanelCornerRadius = 46.dp
199 val PanelWidthMedium = 390.dp
200 val PanelWidthLarge = 474.dp
201 val OverscrollLimit = 32.dp
202 }
203
204 object Shapes {
205 val RoundedCornerPanel = RoundedCornerShape(Dimensions.PanelCornerRadius)
206 }
207 }
208