• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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