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