• 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 package com.android.systemui.shade.ui.composable
18 
19 import androidx.compose.foundation.layout.WindowInsets
20 import androidx.compose.ui.layout.Measurable
21 import androidx.compose.ui.layout.MeasurePolicy
22 import androidx.compose.ui.layout.MeasureResult
23 import androidx.compose.ui.layout.MeasureScope
24 import androidx.compose.ui.layout.Placeable
25 import androidx.compose.ui.layout.layoutId
26 import androidx.compose.ui.unit.Constraints
27 import androidx.compose.ui.unit.offset
28 import androidx.compose.ui.util.fastFirstOrNull
29 import com.android.systemui.shade.ui.composable.SingleShadeMeasurePolicy.LayoutId
30 import kotlin.math.max
31 
32 /**
33  * Lays out elements from the [LayoutId] in the shade. This policy supports the case when the QS and
34  * UMO share the same row and when they should be one below another.
35  */
36 class SingleShadeMeasurePolicy(
37     private val isMediaInRow: Boolean,
38     private val mediaOffset: MeasureScope.() -> Int,
39     private val onNotificationsTopChanged: (Int) -> Unit,
40     private val mediaZIndex: () -> Float,
41     private val cutoutInsetsProvider: () -> WindowInsets?,
42 ) : MeasurePolicy {
43 
44     enum class LayoutId {
45         QuickSettings,
46         Media,
47         Notifications,
48         ShadeHeader,
49     }
50 
measurenull51     override fun MeasureScope.measure(
52         measurables: List<Measurable>,
53         constraints: Constraints,
54     ): MeasureResult {
55         val cutoutInsets: WindowInsets? = cutoutInsetsProvider()
56         val constraintsWithCutout = applyCutout(constraints, cutoutInsets)
57         val insetsLeft = cutoutInsets?.getLeft(this, layoutDirection) ?: 0
58         val insetsTop = cutoutInsets?.getTop(this) ?: 0
59 
60         val shadeHeaderPlaceable =
61             measurables
62                 .fastFirstOrNull { it.layoutId == LayoutId.ShadeHeader }
63                 ?.measure(constraintsWithCutout)
64         val mediaPlaceable =
65             measurables
66                 .fastFirstOrNull { it.layoutId == LayoutId.Media }
67                 ?.measure(applyMediaConstraints(constraintsWithCutout, isMediaInRow))
68         val quickSettingsPlaceable =
69             measurables
70                 .fastFirstOrNull { it.layoutId == LayoutId.QuickSettings }
71                 ?.measure(constraintsWithCutout)
72         val notificationsPlaceable =
73             measurables
74                 .fastFirstOrNull { it.layoutId == LayoutId.Notifications }
75                 ?.measure(constraints)
76 
77         val notificationsTop =
78             calculateNotificationsTop(
79                 statusBarHeaderPlaceable = shadeHeaderPlaceable,
80                 quickSettingsPlaceable = quickSettingsPlaceable,
81                 mediaPlaceable = mediaPlaceable,
82                 insetsTop = insetsTop,
83                 isMediaInRow = isMediaInRow,
84             )
85         onNotificationsTopChanged(notificationsTop)
86 
87         return layout(constraints.maxWidth, constraints.maxHeight) {
88             shadeHeaderPlaceable?.placeRelative(x = insetsLeft, y = insetsTop)
89             val statusBarHeaderHeight = shadeHeaderPlaceable?.height ?: 0
90             quickSettingsPlaceable?.placeRelative(
91                 x = insetsLeft,
92                 y = insetsTop + statusBarHeaderHeight,
93             )
94 
95             if (mediaPlaceable != null) {
96                 val quickSettingsHeight = quickSettingsPlaceable?.height ?: 0
97 
98                 if (isMediaInRow) {
99                     // mediaPlaceable height ranges from 0 to qsHeight. We want it to be centered
100                     // vertically when it's smaller than the QS
101                     val mediaCenteringOffset = (quickSettingsHeight - mediaPlaceable.height) / 2
102                     mediaPlaceable.placeRelative(
103                         x = insetsLeft + constraintsWithCutout.maxWidth / 2,
104                         y =
105                             insetsTop +
106                                 statusBarHeaderHeight +
107                                 mediaCenteringOffset +
108                                 mediaOffset(),
109                         zIndex = mediaZIndex(),
110                     )
111                 } else {
112                     mediaPlaceable.placeRelative(
113                         x = insetsLeft,
114                         y = insetsTop + statusBarHeaderHeight + quickSettingsHeight,
115                         zIndex = mediaZIndex(),
116                     )
117                 }
118             }
119 
120             // Notifications don't need to accommodate for horizontal insets
121             notificationsPlaceable?.placeRelative(x = 0, y = notificationsTop)
122         }
123     }
124 
calculateNotificationsTopnull125     private fun calculateNotificationsTop(
126         statusBarHeaderPlaceable: Placeable?,
127         quickSettingsPlaceable: Placeable?,
128         mediaPlaceable: Placeable?,
129         insetsTop: Int,
130         isMediaInRow: Boolean,
131     ): Int {
132         val mediaHeight = mediaPlaceable?.height ?: 0
133         val statusBarHeaderHeight = statusBarHeaderPlaceable?.height ?: 0
134         val quickSettingsHeight = quickSettingsPlaceable?.height ?: 0
135 
136         return insetsTop +
137             statusBarHeaderHeight +
138             if (isMediaInRow) {
139                 max(quickSettingsHeight, mediaHeight)
140             } else {
141                 quickSettingsHeight + mediaHeight
142             }
143     }
144 
applyMediaConstraintsnull145     private fun applyMediaConstraints(
146         constraints: Constraints,
147         isMediaInRow: Boolean,
148     ): Constraints {
149         return if (isMediaInRow) {
150             constraints.copy(maxWidth = constraints.maxWidth / 2)
151         } else {
152             constraints
153         }
154     }
155 
MeasureScopenull156     private fun MeasureScope.applyCutout(
157         constraints: Constraints,
158         cutoutInsets: WindowInsets?,
159     ): Constraints {
160         return if (cutoutInsets == null) {
161             constraints
162         } else {
163             val left = cutoutInsets.getLeft(this, layoutDirection)
164             val top = cutoutInsets.getTop(this)
165             val right = cutoutInsets.getRight(this, layoutDirection)
166             val bottom = cutoutInsets.getBottom(this)
167 
168             constraints.offset(horizontal = -(left + right), vertical = -(top + bottom))
169         }
170     }
171 }
172