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.Column
25 import androidx.compose.foundation.layout.ExperimentalLayoutApi
26 import androidx.compose.foundation.layout.PaddingValues
27 import androidx.compose.foundation.layout.Spacer
28 import androidx.compose.foundation.layout.WindowInsets
29 import androidx.compose.foundation.layout.asPaddingValues
30 import androidx.compose.foundation.layout.calculateEndPadding
31 import androidx.compose.foundation.layout.calculateStartPadding
32 import androidx.compose.foundation.layout.displayCutout
33 import androidx.compose.foundation.layout.fillMaxSize
34 import androidx.compose.foundation.layout.fillMaxWidth
35 import androidx.compose.foundation.layout.padding
36 import androidx.compose.foundation.layout.systemBarsIgnoringVisibility
37 import androidx.compose.foundation.layout.waterfall
38 import androidx.compose.foundation.layout.width
39 import androidx.compose.foundation.overscroll
40 import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
41 import androidx.compose.runtime.Composable
42 import androidx.compose.runtime.ReadOnlyComposable
43 import androidx.compose.runtime.remember
44 import androidx.compose.ui.Alignment
45 import androidx.compose.ui.Modifier
46 import androidx.compose.ui.graphics.Color
47 import androidx.compose.ui.platform.LocalLayoutDirection
48 import androidx.compose.ui.platform.LocalResources
49 import androidx.compose.ui.res.dimensionResource
50 import androidx.compose.ui.unit.Dp
51 import androidx.compose.ui.unit.dp
52 import com.android.compose.animation.scene.ContentScope
53 import com.android.compose.animation.scene.ElementKey
54 import com.android.compose.animation.scene.LowestZIndexContentPicker
55 import com.android.compose.windowsizeclass.LocalWindowSizeClass
56 import com.android.mechanics.behavior.VerticalExpandContainerSpec
57 import com.android.mechanics.behavior.verticalExpandContainerBackground
58 import com.android.systemui.Flags
59 import com.android.systemui.res.R
60 import com.android.systemui.shade.ui.ShadeColors.notificationScrim
61 import com.android.systemui.shade.ui.ShadeColors.shadePanel
62 import com.android.systemui.shade.ui.composable.OverlayShade.rememberShadeExpansionMotion
63
64 /** Renders a lightweight shade UI container, as an overlay. */
65 @Composable
ContentScopenull66 fun ContentScope.OverlayShade(
67 panelElement: ElementKey,
68 alignmentOnWideScreens: Alignment,
69 onScrimClicked: () -> Unit,
70 modifier: Modifier = Modifier,
71 header: @Composable () -> Unit,
72 content: @Composable () -> Unit,
73 ) {
74 val isFullWidth = isFullWidthShade()
75 Box(modifier) {
76 Scrim(onClicked = onScrimClicked)
77
78 Box(
79 modifier = Modifier.fillMaxSize().panelContainerPadding(isFullWidth),
80 contentAlignment = if (isFullWidth) Alignment.TopCenter else alignmentOnWideScreens,
81 ) {
82 Panel(
83 modifier =
84 Modifier.overscroll(verticalOverscrollEffect)
85 .element(panelElement)
86 .panelWidth(isFullWidth),
87 header = header.takeIf { isFullWidth },
88 content = content,
89 )
90 }
91
92 if (!isFullWidth) {
93 header()
94 }
95 }
96 }
97
98 @Composable
ContentScopenull99 private fun ContentScope.Scrim(onClicked: () -> Unit, modifier: Modifier = Modifier) {
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
Panelnull111 private fun ContentScope.Panel(
112 modifier: Modifier = Modifier,
113 header: (@Composable () -> Unit)?,
114 content: @Composable () -> Unit,
115 ) {
116 Box(
117 modifier =
118 modifier
119 .disableSwipesWhenScrolling()
120 .verticalExpandContainerBackground(
121 backgroundColor = OverlayShade.Colors.PanelBackground,
122 spec = rememberShadeExpansionMotion(isFullWidthShade()),
123 )
124 ) {
125 Column {
126 header?.invoke()
127 content()
128 }
129 }
130 }
131
132 @Composable
Modifiernull133 private fun Modifier.panelWidth(isFullWidthPanel: Boolean): Modifier {
134 return if (isFullWidthPanel) {
135 fillMaxWidth()
136 } else {
137 width(dimensionResource(id = R.dimen.shade_panel_width))
138 }
139 }
140
141 @Composable
142 @ReadOnlyComposable
isFullWidthShadenull143 internal fun isFullWidthShade(): Boolean {
144 return LocalWindowSizeClass.current.widthSizeClass == WindowWidthSizeClass.Compact
145 }
146
147 @Composable
panelContainerPaddingnull148 private fun Modifier.panelContainerPadding(isFullWidthPanel: Boolean): Modifier {
149 if (isFullWidthPanel) {
150 return this
151 }
152 val systemBars = WindowInsets.systemBarsIgnoringVisibility
153 val displayCutout = WindowInsets.displayCutout
154 val waterfall = WindowInsets.waterfall
155 val horizontalPadding =
156 PaddingValues(horizontal = dimensionResource(id = R.dimen.shade_panel_margin_horizontal))
157 return padding(
158 combinePaddings(
159 systemBars.asPaddingValues(),
160 displayCutout.asPaddingValues(),
161 waterfall.asPaddingValues(),
162 horizontalPadding,
163 )
164 )
165 }
166
167 /** Creates a union of [paddingValues] by using the max padding of each edge. */
168 @Composable
combinePaddingsnull169 private fun combinePaddings(vararg paddingValues: PaddingValues): PaddingValues {
170 return if (paddingValues.isEmpty()) {
171 PaddingValues(0.dp)
172 } else {
173 val layoutDirection = LocalLayoutDirection.current
174 PaddingValues(
175 start = paddingValues.maxOf { it.calculateStartPadding(layoutDirection) },
176 top = paddingValues.maxOf { it.calculateTopPadding() },
177 end = paddingValues.maxOf { it.calculateEndPadding(layoutDirection) },
178 bottom = paddingValues.maxOf { it.calculateBottomPadding() },
179 )
180 }
181 }
182
183 object OverlayShade {
184 object Elements {
185 val Scrim = ElementKey("OverlayShadeScrim", contentPicker = LowestZIndexContentPicker)
186 val Panel =
187 ElementKey(
188 "OverlayShadePanel",
189 contentPicker = LowestZIndexContentPicker,
190 placeAllCopies = true,
191 )
192 }
193
194 object Colors {
195 val ScrimBackground: Color
196 @Composable
197 @ReadOnlyComposable
198 get() = Color(LocalResources.current.notificationScrim(Flags.notificationShadeBlur()))
199
200 val PanelBackground: Color
201 @Composable
202 @ReadOnlyComposable
203 get() = Color(LocalResources.current.shadePanel(Flags.notificationShadeBlur()))
204 }
205
206 object Dimensions {
207 val PanelCornerRadius: Dp
208 @Composable
209 @ReadOnlyComposable
210 get() = dimensionResource(R.dimen.overlay_shade_panel_shape_radius)
211 }
212
213 @Composable
rememberShadeExpansionMotionnull214 fun rememberShadeExpansionMotion(isFullWidth: Boolean): VerticalExpandContainerSpec {
215 val radius = Dimensions.PanelCornerRadius
216 return remember(radius, isFullWidth) {
217 VerticalExpandContainerSpec(isFloating = !isFullWidth, radius = radius)
218 }
219 }
220 }
221