Loading packages/SystemUI/aconfig/systemui.aconfig +10 −0 Original line number Diff line number Diff line Loading @@ -988,6 +988,16 @@ flag { } } flag { name: "qs_material_expressive_tiles" namespace: "systemui" description: "Uses the Material Expressive button groups for QS tiles." bug: "419309247" metadata { purpose: PURPOSE_BUGFIX } } flag { name: "qs_edit_mode_tabs" namespace: "systemui" Loading packages/SystemUI/src/com/android/systemui/qs/flags/QSMaterialExpressiveTiles.kt 0 → 100644 +48 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.qs.flags import com.android.systemui.Flags import com.android.systemui.flags.RefactorFlagUtils /** Object to help check if QS is using Material Expressive tiles. */ @Suppress("NOTHING_TO_INLINE") object QSMaterialExpressiveTiles { /** The aconfig flag name */ const val FLAG_NAME = Flags.FLAG_QS_MATERIAL_EXPRESSIVE_TILES /** Is the tooltip enabled */ @JvmStatic inline val isEnabled get() = Flags.qsMaterialExpressiveTiles() /** * Called to ensure code is only run when the flag is enabled. This protects users from the * unintended behaviors caused by accidentally running new logic, while also crashing on an eng * build to ensure that the refactor author catches issues in testing. */ @JvmStatic inline fun isUnexpectedlyInLegacyMode() = RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) /** * Called to ensure code is only run when the flag is disabled. This will throw an exception if * the flag is enabled to ensure that the refactor author catches issues in testing. */ @JvmStatic inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) } packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt +54 −27 Original line number Diff line number Diff line Loading @@ -30,6 +30,7 @@ import com.android.compose.animation.scene.ContentScope import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.grid.ui.compose.VerticalSpannedGrid import com.android.systemui.qs.composefragment.ui.GridAnchor import com.android.systemui.qs.flags.QSMaterialExpressiveTiles import com.android.systemui.qs.panels.ui.compose.infinitegrid.Tile import com.android.systemui.qs.panels.ui.viewmodel.BounceableTileViewModel import com.android.systemui.qs.panels.ui.viewmodel.QuickQuickSettingsViewModel Loading @@ -42,18 +43,42 @@ fun ContentScope.QuickQuickSettings( modifier: Modifier = Modifier, listening: () -> Boolean, ) { val columns = viewModel.columns val sizedTiles = viewModel.tileViewModels val tiles = sizedTiles.fastMap { it.tile } val bounceables = remember(sizedTiles) { List(sizedTiles.size) { BounceableTileViewModel() } } val squishiness by viewModel.squishinessViewModel.squishiness.collectAsStateWithLifecycle() val scope = rememberCoroutineScope() val spans by remember(sizedTiles) { derivedStateOf { sizedTiles.fastMap { it.width } } } val columns = viewModel.columns Box(modifier = modifier) { GridAnchor() if (QSMaterialExpressiveTiles.isEnabled) { ButtonGroupGrid( sizedTiles = sizedTiles, columns = columns, keys = { it.spec }, elementKey = { it.spec.toElementKey() }, horizontalPadding = dimensionResource(R.dimen.qs_tile_margin_horizontal), ) { sizedTile, interactionSource -> Tile( tile = sizedTile.tile, iconOnly = sizedTile.isIcon, squishiness = { squishiness }, coroutineScope = scope, tileHapticsViewModelFactoryProvider = viewModel.tileHapticsViewModelFactoryProvider, // There should be no QuickQuickSettings when the details // view is enabled. detailsViewModel = null, isVisible = listening, bounceableInfo = null, interactionSource = interactionSource, ) } } else { val bounceables = remember(sizedTiles) { List(sizedTiles.size) { BounceableTileViewModel() } } val spans by remember(sizedTiles) { derivedStateOf { sizedTiles.fastMap { it.width } } } VerticalSpannedGrid( columns = columns, columnSpacing = dimensionResource(R.dimen.qs_tile_margin_horizontal), Loading @@ -63,7 +88,7 @@ fun ContentScope.QuickQuickSettings( keys = { sizedTiles[it].tile.spec }, ) { spanIndex, column, isFirstInColumn, isLastInColumn -> val it = sizedTiles[spanIndex] Element(it.tile.spec.toElementKey(spanIndex), Modifier) { Element(it.tile.spec.toElementKey(), Modifier) { Tile( tile = it.tile, iconOnly = it.isIcon, Loading @@ -83,10 +108,12 @@ fun ContentScope.QuickQuickSettings( // There should be no QuickQuickSettings when the details view is enabled. detailsViewModel = null, isVisible = listening, interactionSource = null, ) } } } } TileListener(tiles, listening) } packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileButtonGroupGrid.kt 0 → 100644 +170 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.qs.panels.ui.compose import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.ButtonGroup import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.runtime.Composable import androidx.compose.runtime.key import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastMap import androidx.compose.ui.util.fastSumBy import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.ElementKey import com.android.systemui.qs.panels.shared.model.SizedTile import com.android.systemui.qs.panels.shared.model.splitInRowsSequence import com.android.systemui.res.R /** * A grid of [SizedTile] implemented using a column of [ButtonGroup]. * * @param sizedTiles The list of [SizedTile] to display. * @param columns The number of columns in the grid. * @param horizontalPadding The horizontal padding of the grid. * @param keys A factory of stable and unique keys representing the item. * @param elementKey A factory for [ElementKey] for each tile. * @param modifier The [Modifier] to be applied to the grid. * @param tileContent The content of each tile. It receives the [SizedTile] to display and the * [MutableInteractionSource] to use for animating the tile's width. Do not use this interaction * source if the tile should not bounce. */ @Composable fun <T> ContentScope.ButtonGroupGrid( sizedTiles: List<SizedTile<T>>, columns: Int, horizontalPadding: Dp, keys: (T) -> Any, elementKey: (T) -> ElementKey, modifier: Modifier = Modifier, tileContent: @Composable ContentScope.(tile: SizedTile<T>, interactionSource: MutableInteractionSource) -> Unit, ) { val rows = remember(sizedTiles, columns) { splitInRowsSequence(sizedTiles, columns).toList() } Column( verticalArrangement = spacedBy(dimensionResource(R.dimen.qs_tile_margin_vertical)), modifier = modifier, ) { for (row in rows) { key(row.fastMap { keys(it.tile) }) { ButtonGroupRow( row, columns, horizontalPadding, keys, elementKey, tileContent = tileContent, ) } } } } @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable private fun <T> ContentScope.ButtonGroupRow( row: List<SizedTile<T>>, columns: Int, horizontalPadding: Dp, keys: (T) -> Any, elementKey: (T) -> ElementKey, modifier: Modifier = Modifier, tileContent: @Composable ContentScope.(tile: SizedTile<T>, interactionSource: MutableInteractionSource) -> Unit, ) { val halfPadding = horizontalPadding / 2 // Avoid setting the horizontal padding with the ButtonGroup to ensure that weight distribution // works properly for all rows. ButtonGroup( overflowIndicator = {}, horizontalArrangement = spacedBy(0.dp), modifier = modifier.fillMaxWidth(), ) { for ((indexInRow, sizedTile) in row.withIndex()) { val column = row.subList(0, indexInRow).fastSumBy { it.width } val onLastColumn = column == columns - sizedTile.width val isFirst = indexInRow == 0 customItem( buttonGroupContent = { key(keys(sizedTile.tile)) { // This interaction source may not be used by tiles that shouldn't bounce. val interactionSource = remember { MutableInteractionSource() } Element( elementKey(sizedTile.tile), Modifier.animateWidth(interactionSource) .weight(sizedTile.width.toFloat()) .rowPadding(isFirst, onLastColumn, halfPadding), ) { tileContent(sizedTile, interactionSource) } } }, menuContent = {}, ) } // If the row isn't filled, add a spacer val columnsLeft = columns - row.fastSumBy { it.width } if (columnsLeft > 0) { customItem( buttonGroupContent = { Spacer( Modifier.weight(columnsLeft.toFloat()) .rowPadding(isFirst = false, onLastColumn = true, halfPadding) ) }, menuContent = {}, ) } } } /** * Adds padding to a Composable within a row, adjusting based on its position within that row. * * This modifier is useful for creating consistent spacing between items in a row, especially when * you want to avoid extra padding at the beginning or end of the row. * * @param isFirst True if this is the first item in the row. If true, no start padding is applied. * @param onLastColumn True if this item is in the last column of the row. The last element of a row * may not be on the last column. If true, no end padding is applied. * @param horizontalPadding The amount of padding to apply to the start (if not first) and end (if * not last) of the item. * @return A [Modifier] that applies the calculated padding. */ private fun Modifier.rowPadding( isFirst: Boolean, onLastColumn: Boolean, horizontalPadding: Dp, ): Modifier { return padding( start = if (isFirst) 0.dp else horizontalPadding, end = if (onLastColumn) 0.dp else horizontalPadding, ) } packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt +64 −25 Original line number Diff line number Diff line Loading @@ -40,8 +40,10 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.grid.ui.compose.VerticalSpannedGrid import com.android.systemui.haptics.msdl.qs.TileHapticsViewModelFactoryProvider import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.qs.flags.QSMaterialExpressiveTiles import com.android.systemui.qs.flags.QsEditModeTabs import com.android.systemui.qs.panels.shared.model.SizedTileImpl import com.android.systemui.qs.panels.ui.compose.ButtonGroupGrid import com.android.systemui.qs.panels.ui.compose.EditTileListState import com.android.systemui.qs.panels.ui.compose.PaginatableGridLayout import com.android.systemui.qs.panels.ui.compose.TileListener Loading Loading @@ -100,16 +102,51 @@ constructor( SizedTileImpl(it, if (largeTiles.contains(it.spec)) largeTilesSpan else 1) } } val bounceables = remember(sizedTiles) { List(sizedTiles.size) { BounceableTileViewModel() } } val squishiness by viewModel.squishinessViewModel.squishiness.collectAsStateWithLifecycle() val scope = rememberCoroutineScope() val spans by remember(sizedTiles) { derivedStateOf { sizedTiles.fastMap { it.width } } } val motionBuilderContext = rememberMotionBuilderContext() val marginBottom = with(LocalDensity.current) { QuickSettingsShade.Dimensions.Padding.toPx() } if (QSMaterialExpressiveTiles.isEnabled) { ButtonGroupGrid( sizedTiles = sizedTiles, columns = columns, keys = { it.spec }, elementKey = { it.spec.toElementKey() }, horizontalPadding = dimensionResource(R.dimen.qs_tile_margin_horizontal), modifier = modifier, ) { sizedTile, interactionSource -> Tile( tile = sizedTile.tile, iconOnly = iconTilesViewModel.isIconTile(sizedTile.tile.spec), squishiness = { squishiness }, tileHapticsViewModelFactoryProvider = tileHapticsViewModelFactoryProvider, coroutineScope = scope, detailsViewModel = detailsViewModel, isVisible = listening, requestToggleTextFeedback = textFeedbackViewModel::requestShowFeedback, modifier = if (revealEffectContainer != null) { Modifier.verticalTactileSurfaceReveal( contentScope = this@TileGrid, motionBuilderContext = motionBuilderContext, container = revealEffectContainer, deltaY = -marginBottom, ) } else { Modifier }, revealEffectContainer = revealEffectContainer, bounceableInfo = null, interactionSource = interactionSource, ) } } else { val bounceables = remember(sizedTiles) { List(sizedTiles.size) { BounceableTileViewModel() } } val spans by remember(sizedTiles) { derivedStateOf { sizedTiles.fastMap { it.width } } } VerticalSpannedGrid( columns = columns, columnSpacing = dimensionResource(R.dimen.qs_tile_margin_horizontal), Loading @@ -120,7 +157,7 @@ constructor( ) { spanIndex, column, isFirstInColumn, isLastInColumn -> val it = sizedTiles[spanIndex] Element(it.tile.spec.toElementKey(spanIndex), Modifier) { Element(it.tile.spec.toElementKey(), Modifier) { Tile( tile = it.tile, iconOnly = iconTilesViewModel.isIconTile(it.tile.spec), Loading Loading @@ -151,9 +188,11 @@ constructor( Modifier }, revealEffectContainer = revealEffectContainer, interactionSource = null, ) } } } TileListener(tiles, listening) } Loading Loading
packages/SystemUI/aconfig/systemui.aconfig +10 −0 Original line number Diff line number Diff line Loading @@ -988,6 +988,16 @@ flag { } } flag { name: "qs_material_expressive_tiles" namespace: "systemui" description: "Uses the Material Expressive button groups for QS tiles." bug: "419309247" metadata { purpose: PURPOSE_BUGFIX } } flag { name: "qs_edit_mode_tabs" namespace: "systemui" Loading
packages/SystemUI/src/com/android/systemui/qs/flags/QSMaterialExpressiveTiles.kt 0 → 100644 +48 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.qs.flags import com.android.systemui.Flags import com.android.systemui.flags.RefactorFlagUtils /** Object to help check if QS is using Material Expressive tiles. */ @Suppress("NOTHING_TO_INLINE") object QSMaterialExpressiveTiles { /** The aconfig flag name */ const val FLAG_NAME = Flags.FLAG_QS_MATERIAL_EXPRESSIVE_TILES /** Is the tooltip enabled */ @JvmStatic inline val isEnabled get() = Flags.qsMaterialExpressiveTiles() /** * Called to ensure code is only run when the flag is enabled. This protects users from the * unintended behaviors caused by accidentally running new logic, while also crashing on an eng * build to ensure that the refactor author catches issues in testing. */ @JvmStatic inline fun isUnexpectedlyInLegacyMode() = RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) /** * Called to ensure code is only run when the flag is disabled. This will throw an exception if * the flag is enabled to ensure that the refactor author catches issues in testing. */ @JvmStatic inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) }
packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt +54 −27 Original line number Diff line number Diff line Loading @@ -30,6 +30,7 @@ import com.android.compose.animation.scene.ContentScope import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.grid.ui.compose.VerticalSpannedGrid import com.android.systemui.qs.composefragment.ui.GridAnchor import com.android.systemui.qs.flags.QSMaterialExpressiveTiles import com.android.systemui.qs.panels.ui.compose.infinitegrid.Tile import com.android.systemui.qs.panels.ui.viewmodel.BounceableTileViewModel import com.android.systemui.qs.panels.ui.viewmodel.QuickQuickSettingsViewModel Loading @@ -42,18 +43,42 @@ fun ContentScope.QuickQuickSettings( modifier: Modifier = Modifier, listening: () -> Boolean, ) { val columns = viewModel.columns val sizedTiles = viewModel.tileViewModels val tiles = sizedTiles.fastMap { it.tile } val bounceables = remember(sizedTiles) { List(sizedTiles.size) { BounceableTileViewModel() } } val squishiness by viewModel.squishinessViewModel.squishiness.collectAsStateWithLifecycle() val scope = rememberCoroutineScope() val spans by remember(sizedTiles) { derivedStateOf { sizedTiles.fastMap { it.width } } } val columns = viewModel.columns Box(modifier = modifier) { GridAnchor() if (QSMaterialExpressiveTiles.isEnabled) { ButtonGroupGrid( sizedTiles = sizedTiles, columns = columns, keys = { it.spec }, elementKey = { it.spec.toElementKey() }, horizontalPadding = dimensionResource(R.dimen.qs_tile_margin_horizontal), ) { sizedTile, interactionSource -> Tile( tile = sizedTile.tile, iconOnly = sizedTile.isIcon, squishiness = { squishiness }, coroutineScope = scope, tileHapticsViewModelFactoryProvider = viewModel.tileHapticsViewModelFactoryProvider, // There should be no QuickQuickSettings when the details // view is enabled. detailsViewModel = null, isVisible = listening, bounceableInfo = null, interactionSource = interactionSource, ) } } else { val bounceables = remember(sizedTiles) { List(sizedTiles.size) { BounceableTileViewModel() } } val spans by remember(sizedTiles) { derivedStateOf { sizedTiles.fastMap { it.width } } } VerticalSpannedGrid( columns = columns, columnSpacing = dimensionResource(R.dimen.qs_tile_margin_horizontal), Loading @@ -63,7 +88,7 @@ fun ContentScope.QuickQuickSettings( keys = { sizedTiles[it].tile.spec }, ) { spanIndex, column, isFirstInColumn, isLastInColumn -> val it = sizedTiles[spanIndex] Element(it.tile.spec.toElementKey(spanIndex), Modifier) { Element(it.tile.spec.toElementKey(), Modifier) { Tile( tile = it.tile, iconOnly = it.isIcon, Loading @@ -83,10 +108,12 @@ fun ContentScope.QuickQuickSettings( // There should be no QuickQuickSettings when the details view is enabled. detailsViewModel = null, isVisible = listening, interactionSource = null, ) } } } } TileListener(tiles, listening) }
packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileButtonGroupGrid.kt 0 → 100644 +170 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.qs.panels.ui.compose import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.ButtonGroup import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.runtime.Composable import androidx.compose.runtime.key import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastMap import androidx.compose.ui.util.fastSumBy import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.ElementKey import com.android.systemui.qs.panels.shared.model.SizedTile import com.android.systemui.qs.panels.shared.model.splitInRowsSequence import com.android.systemui.res.R /** * A grid of [SizedTile] implemented using a column of [ButtonGroup]. * * @param sizedTiles The list of [SizedTile] to display. * @param columns The number of columns in the grid. * @param horizontalPadding The horizontal padding of the grid. * @param keys A factory of stable and unique keys representing the item. * @param elementKey A factory for [ElementKey] for each tile. * @param modifier The [Modifier] to be applied to the grid. * @param tileContent The content of each tile. It receives the [SizedTile] to display and the * [MutableInteractionSource] to use for animating the tile's width. Do not use this interaction * source if the tile should not bounce. */ @Composable fun <T> ContentScope.ButtonGroupGrid( sizedTiles: List<SizedTile<T>>, columns: Int, horizontalPadding: Dp, keys: (T) -> Any, elementKey: (T) -> ElementKey, modifier: Modifier = Modifier, tileContent: @Composable ContentScope.(tile: SizedTile<T>, interactionSource: MutableInteractionSource) -> Unit, ) { val rows = remember(sizedTiles, columns) { splitInRowsSequence(sizedTiles, columns).toList() } Column( verticalArrangement = spacedBy(dimensionResource(R.dimen.qs_tile_margin_vertical)), modifier = modifier, ) { for (row in rows) { key(row.fastMap { keys(it.tile) }) { ButtonGroupRow( row, columns, horizontalPadding, keys, elementKey, tileContent = tileContent, ) } } } } @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable private fun <T> ContentScope.ButtonGroupRow( row: List<SizedTile<T>>, columns: Int, horizontalPadding: Dp, keys: (T) -> Any, elementKey: (T) -> ElementKey, modifier: Modifier = Modifier, tileContent: @Composable ContentScope.(tile: SizedTile<T>, interactionSource: MutableInteractionSource) -> Unit, ) { val halfPadding = horizontalPadding / 2 // Avoid setting the horizontal padding with the ButtonGroup to ensure that weight distribution // works properly for all rows. ButtonGroup( overflowIndicator = {}, horizontalArrangement = spacedBy(0.dp), modifier = modifier.fillMaxWidth(), ) { for ((indexInRow, sizedTile) in row.withIndex()) { val column = row.subList(0, indexInRow).fastSumBy { it.width } val onLastColumn = column == columns - sizedTile.width val isFirst = indexInRow == 0 customItem( buttonGroupContent = { key(keys(sizedTile.tile)) { // This interaction source may not be used by tiles that shouldn't bounce. val interactionSource = remember { MutableInteractionSource() } Element( elementKey(sizedTile.tile), Modifier.animateWidth(interactionSource) .weight(sizedTile.width.toFloat()) .rowPadding(isFirst, onLastColumn, halfPadding), ) { tileContent(sizedTile, interactionSource) } } }, menuContent = {}, ) } // If the row isn't filled, add a spacer val columnsLeft = columns - row.fastSumBy { it.width } if (columnsLeft > 0) { customItem( buttonGroupContent = { Spacer( Modifier.weight(columnsLeft.toFloat()) .rowPadding(isFirst = false, onLastColumn = true, halfPadding) ) }, menuContent = {}, ) } } } /** * Adds padding to a Composable within a row, adjusting based on its position within that row. * * This modifier is useful for creating consistent spacing between items in a row, especially when * you want to avoid extra padding at the beginning or end of the row. * * @param isFirst True if this is the first item in the row. If true, no start padding is applied. * @param onLastColumn True if this item is in the last column of the row. The last element of a row * may not be on the last column. If true, no end padding is applied. * @param horizontalPadding The amount of padding to apply to the start (if not first) and end (if * not last) of the item. * @return A [Modifier] that applies the calculated padding. */ private fun Modifier.rowPadding( isFirst: Boolean, onLastColumn: Boolean, horizontalPadding: Dp, ): Modifier { return padding( start = if (isFirst) 0.dp else horizontalPadding, end = if (onLastColumn) 0.dp else horizontalPadding, ) }
packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt +64 −25 Original line number Diff line number Diff line Loading @@ -40,8 +40,10 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.grid.ui.compose.VerticalSpannedGrid import com.android.systemui.haptics.msdl.qs.TileHapticsViewModelFactoryProvider import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.qs.flags.QSMaterialExpressiveTiles import com.android.systemui.qs.flags.QsEditModeTabs import com.android.systemui.qs.panels.shared.model.SizedTileImpl import com.android.systemui.qs.panels.ui.compose.ButtonGroupGrid import com.android.systemui.qs.panels.ui.compose.EditTileListState import com.android.systemui.qs.panels.ui.compose.PaginatableGridLayout import com.android.systemui.qs.panels.ui.compose.TileListener Loading Loading @@ -100,16 +102,51 @@ constructor( SizedTileImpl(it, if (largeTiles.contains(it.spec)) largeTilesSpan else 1) } } val bounceables = remember(sizedTiles) { List(sizedTiles.size) { BounceableTileViewModel() } } val squishiness by viewModel.squishinessViewModel.squishiness.collectAsStateWithLifecycle() val scope = rememberCoroutineScope() val spans by remember(sizedTiles) { derivedStateOf { sizedTiles.fastMap { it.width } } } val motionBuilderContext = rememberMotionBuilderContext() val marginBottom = with(LocalDensity.current) { QuickSettingsShade.Dimensions.Padding.toPx() } if (QSMaterialExpressiveTiles.isEnabled) { ButtonGroupGrid( sizedTiles = sizedTiles, columns = columns, keys = { it.spec }, elementKey = { it.spec.toElementKey() }, horizontalPadding = dimensionResource(R.dimen.qs_tile_margin_horizontal), modifier = modifier, ) { sizedTile, interactionSource -> Tile( tile = sizedTile.tile, iconOnly = iconTilesViewModel.isIconTile(sizedTile.tile.spec), squishiness = { squishiness }, tileHapticsViewModelFactoryProvider = tileHapticsViewModelFactoryProvider, coroutineScope = scope, detailsViewModel = detailsViewModel, isVisible = listening, requestToggleTextFeedback = textFeedbackViewModel::requestShowFeedback, modifier = if (revealEffectContainer != null) { Modifier.verticalTactileSurfaceReveal( contentScope = this@TileGrid, motionBuilderContext = motionBuilderContext, container = revealEffectContainer, deltaY = -marginBottom, ) } else { Modifier }, revealEffectContainer = revealEffectContainer, bounceableInfo = null, interactionSource = interactionSource, ) } } else { val bounceables = remember(sizedTiles) { List(sizedTiles.size) { BounceableTileViewModel() } } val spans by remember(sizedTiles) { derivedStateOf { sizedTiles.fastMap { it.width } } } VerticalSpannedGrid( columns = columns, columnSpacing = dimensionResource(R.dimen.qs_tile_margin_horizontal), Loading @@ -120,7 +157,7 @@ constructor( ) { spanIndex, column, isFirstInColumn, isLastInColumn -> val it = sizedTiles[spanIndex] Element(it.tile.spec.toElementKey(spanIndex), Modifier) { Element(it.tile.spec.toElementKey(), Modifier) { Tile( tile = it.tile, iconOnly = iconTilesViewModel.isIconTile(it.tile.spec), Loading Loading @@ -151,9 +188,11 @@ constructor( Modifier }, revealEffectContainer = revealEffectContainer, interactionSource = null, ) } } } TileListener(tiles, listening) } Loading