1 /* <lambda>null2 * 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.qs.panels.ui.compose 18 19 import androidx.compose.foundation.layout.Arrangement 20 import androidx.compose.foundation.layout.Box 21 import androidx.compose.foundation.layout.Column 22 import androidx.compose.foundation.layout.Row 23 import androidx.compose.foundation.layout.Spacer 24 import androidx.compose.foundation.layout.fillMaxSize 25 import androidx.compose.foundation.layout.fillMaxWidth 26 import androidx.compose.foundation.layout.height 27 import androidx.compose.foundation.layout.padding 28 import androidx.compose.foundation.lazy.grid.GridCells 29 import androidx.compose.foundation.lazy.grid.GridItemSpan 30 import androidx.compose.foundation.lazy.grid.LazyGridScope 31 import androidx.compose.foundation.rememberScrollState 32 import androidx.compose.foundation.shape.RoundedCornerShape 33 import androidx.compose.foundation.verticalScroll 34 import androidx.compose.material3.MaterialTheme 35 import androidx.compose.material3.Switch 36 import androidx.compose.material3.Text 37 import androidx.compose.runtime.Composable 38 import androidx.compose.runtime.DisposableEffect 39 import androidx.compose.runtime.getValue 40 import androidx.compose.runtime.rememberUpdatedState 41 import androidx.compose.ui.Modifier 42 import androidx.compose.ui.draw.drawWithContent 43 import androidx.compose.ui.graphics.Color 44 import androidx.compose.ui.graphics.Path 45 import androidx.compose.ui.graphics.PathEffect 46 import androidx.compose.ui.graphics.Shape 47 import androidx.compose.ui.graphics.addOutline 48 import androidx.compose.ui.graphics.drawscope.Stroke 49 import androidx.compose.ui.res.dimensionResource 50 import androidx.compose.ui.text.font.FontWeight 51 import androidx.compose.ui.unit.Dp 52 import androidx.compose.ui.unit.dp 53 import androidx.lifecycle.compose.collectAsStateWithLifecycle 54 import com.android.compose.modifiers.background 55 import com.android.systemui.dagger.SysUISingleton 56 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel 57 import com.android.systemui.qs.panels.ui.viewmodel.PartitionedGridViewModel 58 import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel 59 import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor 60 import com.android.systemui.qs.pipeline.shared.TileSpec 61 import com.android.systemui.res.R 62 import javax.inject.Inject 63 64 @SysUISingleton 65 class PartitionedGridLayout @Inject constructor(private val viewModel: PartitionedGridViewModel) : 66 GridLayout { 67 @Composable 68 override fun TileGrid(tiles: List<TileViewModel>, modifier: Modifier) { 69 DisposableEffect(tiles) { 70 val token = Any() 71 tiles.forEach { it.startListening(token) } 72 onDispose { tiles.forEach { it.stopListening(token) } } 73 } 74 val columns by viewModel.columns.collectAsStateWithLifecycle() 75 val showLabels by viewModel.showLabels.collectAsStateWithLifecycle() 76 val largeTileHeight = tileHeight() 77 val iconTileHeight = tileHeight(showLabels) 78 val (smallTiles, largeTiles) = tiles.partition { viewModel.isIconTile(it.spec) } 79 80 TileLazyGrid(modifier = modifier, columns = GridCells.Fixed(columns)) { 81 // Large tiles 82 items(largeTiles.size, span = { GridItemSpan(2) }) { index -> 83 Tile( 84 tile = largeTiles[index], 85 iconOnly = false, 86 modifier = Modifier.height(largeTileHeight) 87 ) 88 } 89 fillUpRow(nTiles = largeTiles.size, columns = columns / 2) 90 91 // Small tiles 92 items(smallTiles.size) { index -> 93 Tile( 94 tile = smallTiles[index], 95 iconOnly = true, 96 showLabels = showLabels, 97 modifier = Modifier.height(iconTileHeight) 98 ) 99 } 100 } 101 } 102 103 @Composable 104 override fun EditTileGrid( 105 tiles: List<EditTileViewModel>, 106 modifier: Modifier, 107 onAddTile: (TileSpec, Int) -> Unit, 108 onRemoveTile: (TileSpec) -> Unit 109 ) { 110 val columns by viewModel.columns.collectAsStateWithLifecycle() 111 val showLabels by viewModel.showLabels.collectAsStateWithLifecycle() 112 113 val (currentTiles, otherTiles) = tiles.partition { it.isCurrent } 114 val addTileToEnd: (TileSpec) -> Unit by rememberUpdatedState { 115 onAddTile(it, CurrentTilesInteractor.POSITION_AT_END) 116 } 117 val largeTileHeight = tileHeight() 118 val iconTileHeight = tileHeight(showLabels) 119 val tilePadding = dimensionResource(R.dimen.qs_tile_margin_vertical) 120 121 Column( 122 verticalArrangement = Arrangement.spacedBy(tilePadding), 123 modifier = modifier.fillMaxSize().verticalScroll(rememberScrollState()) 124 ) { 125 Row( 126 modifier = 127 Modifier.background( 128 color = MaterialTheme.colorScheme.surfaceVariant, 129 alpha = { 1f }, 130 shape = RoundedCornerShape(dimensionResource(R.dimen.qs_corner_radius)) 131 ) 132 .padding(tilePadding) 133 ) { 134 Column(Modifier.padding(start = tilePadding)) { 135 Text( 136 text = "Show text labels", 137 color = MaterialTheme.colorScheme.onBackground, 138 fontWeight = FontWeight.Bold 139 ) 140 Text( 141 text = "Display names under each tile", 142 color = MaterialTheme.colorScheme.onBackground 143 ) 144 } 145 Spacer(modifier = Modifier.weight(1f)) 146 Switch(checked = showLabels, onCheckedChange = { viewModel.setShowLabels(it) }) 147 } 148 149 CurrentTiles( 150 tiles = currentTiles, 151 largeTileHeight = largeTileHeight, 152 iconTileHeight = iconTileHeight, 153 tilePadding = tilePadding, 154 onRemoveTile = onRemoveTile, 155 isIconOnly = viewModel::isIconTile, 156 columns = columns, 157 showLabels = showLabels, 158 ) 159 AvailableTiles( 160 tiles = otherTiles, 161 largeTileHeight = largeTileHeight, 162 iconTileHeight = iconTileHeight, 163 tilePadding = tilePadding, 164 addTileToEnd = addTileToEnd, 165 isIconOnly = viewModel::isIconTile, 166 showLabels = showLabels, 167 columns = columns, 168 ) 169 } 170 } 171 172 @Composable 173 private fun CurrentTiles( 174 tiles: List<EditTileViewModel>, 175 largeTileHeight: Dp, 176 iconTileHeight: Dp, 177 tilePadding: Dp, 178 onRemoveTile: (TileSpec) -> Unit, 179 isIconOnly: (TileSpec) -> Boolean, 180 showLabels: Boolean, 181 columns: Int, 182 ) { 183 val (smallTiles, largeTiles) = tiles.partition { isIconOnly(it.tileSpec) } 184 185 val largeGridHeight = gridHeight(largeTiles.size, largeTileHeight, columns / 2, tilePadding) 186 val smallGridHeight = gridHeight(smallTiles.size, iconTileHeight, columns, tilePadding) 187 188 CurrentTilesContainer { 189 TileLazyGrid( 190 columns = GridCells.Fixed(columns), 191 modifier = Modifier.height(largeGridHeight), 192 ) { 193 editTiles( 194 largeTiles, 195 ClickAction.REMOVE, 196 onRemoveTile, 197 { false }, 198 indicatePosition = true 199 ) 200 } 201 } 202 CurrentTilesContainer { 203 TileLazyGrid( 204 columns = GridCells.Fixed(columns), 205 modifier = Modifier.height(smallGridHeight), 206 ) { 207 editTiles( 208 smallTiles, 209 ClickAction.REMOVE, 210 onRemoveTile, 211 { true }, 212 showLabels = showLabels, 213 indicatePosition = true 214 ) 215 } 216 } 217 } 218 219 @Composable 220 private fun AvailableTiles( 221 tiles: List<EditTileViewModel>, 222 largeTileHeight: Dp, 223 iconTileHeight: Dp, 224 tilePadding: Dp, 225 addTileToEnd: (TileSpec) -> Unit, 226 isIconOnly: (TileSpec) -> Boolean, 227 showLabels: Boolean, 228 columns: Int, 229 ) { 230 val (tilesStock, tilesCustom) = tiles.partition { it.appName == null } 231 val (smallTiles, largeTiles) = tilesStock.partition { isIconOnly(it.tileSpec) } 232 233 val largeGridHeight = gridHeight(largeTiles.size, largeTileHeight, columns / 2, tilePadding) 234 val smallGridHeight = gridHeight(smallTiles.size, iconTileHeight, columns, tilePadding) 235 val largeGridHeightCustom = 236 gridHeight(tilesCustom.size, iconTileHeight, columns, tilePadding) 237 238 // Add up the height of all three grids and add padding in between 239 val gridHeight = 240 largeGridHeight + smallGridHeight + largeGridHeightCustom + (tilePadding * 2) 241 242 AvailableTilesContainer { 243 TileLazyGrid( 244 columns = GridCells.Fixed(columns), 245 modifier = Modifier.height(gridHeight), 246 ) { 247 // Large tiles 248 editTiles(largeTiles, ClickAction.ADD, addTileToEnd, isIconOnly) 249 fillUpRow(nTiles = largeTiles.size, columns = columns / 2) 250 251 // Small tiles 252 editTiles( 253 smallTiles, 254 ClickAction.ADD, 255 addTileToEnd, 256 isIconOnly, 257 showLabels = showLabels 258 ) 259 fillUpRow(nTiles = smallTiles.size, columns = columns) 260 261 // Custom tiles, all icons 262 editTiles( 263 tilesCustom, 264 ClickAction.ADD, 265 addTileToEnd, 266 isIconOnly, 267 showLabels = showLabels 268 ) 269 } 270 } 271 } 272 273 @Composable 274 private fun CurrentTilesContainer(content: @Composable () -> Unit) { 275 Box( 276 Modifier.fillMaxWidth() 277 .dashedBorder( 278 color = MaterialTheme.colorScheme.onBackground.copy(alpha = .5f), 279 shape = Dimensions.ContainerShape, 280 ) 281 .padding(dimensionResource(R.dimen.qs_tile_margin_vertical)) 282 ) { 283 content() 284 } 285 } 286 287 @Composable 288 private fun AvailableTilesContainer(content: @Composable () -> Unit) { 289 Box( 290 Modifier.fillMaxWidth() 291 .background( 292 color = MaterialTheme.colorScheme.background, 293 alpha = { 1f }, 294 shape = Dimensions.ContainerShape, 295 ) 296 .padding(dimensionResource(R.dimen.qs_tile_margin_vertical)) 297 ) { 298 content() 299 } 300 } 301 302 private fun gridHeight(nTiles: Int, tileHeight: Dp, columns: Int, padding: Dp): Dp { 303 val rows = (nTiles + columns - 1) / columns 304 return ((tileHeight + padding) * rows) - padding 305 } 306 307 /** Fill up the rest of the row if it's not complete. */ 308 private fun LazyGridScope.fillUpRow(nTiles: Int, columns: Int) { 309 if (nTiles % columns != 0) { 310 item(span = { GridItemSpan(maxCurrentLineSpan) }) { Spacer(Modifier) } 311 } 312 } 313 314 private fun Modifier.dashedBorder( 315 color: Color, 316 shape: Shape, 317 ): Modifier { 318 return this.drawWithContent { 319 val outline = shape.createOutline(size, layoutDirection, this) 320 val path = Path() 321 path.addOutline(outline) 322 val stroke = 323 Stroke( 324 width = 1.dp.toPx(), 325 pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f)) 326 ) 327 this.drawContent() 328 drawPath(path = path, style = stroke, color = color) 329 } 330 } 331 332 private object Dimensions { 333 // Corner radius is half the height of a tile + padding 334 val ContainerShape = RoundedCornerShape(48.dp) 335 } 336 } 337