Loading packages/SystemUI/res/values/strings.xml +12 −0 Original line number Diff line number Diff line Loading @@ -2684,6 +2684,18 @@ <!-- Label for header of customize QS to indicate that tiles can be selected to be edited [CHAR LIMIT=60] --> <string name="select_to_rearrange_tiles">Select tiles to rearrange and resize</string> <!-- Label for header of customize QS to indicate that tiles can be tapped to be removed [CHAR LIMIT=60] --> <string name="tap_to_remove_tiles">Tap to remove tiles</string> <!-- Label for header of customize QS to indicate that tiles can be resized and reordered [CHAR LIMIT=60] --> <string name="resize_and_reorder_tiles">Resize and reorder layout</string> <!-- Label for tab of customize QS used to add and remove tiles [CHAR LIMIT=60] --> <string name="qs_edit_edit_tab">Edit</string> <!-- Label for tab of customize QS used to reorder tiles [CHAR LIMIT=60] --> <string name="qs_edit_layout_tab">Layout</string> <!-- Label for placing tiles in edit mode for QS [CHAR LIMIT=60] --> <string name="tap_to_position_tile">Tap to position tile</string> Loading packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt +208 −82 Original line number Diff line number Diff line Loading @@ -18,8 +18,10 @@ package com.android.systemui.qs.panels.ui.compose.infinitegrid import androidx.annotation.StringRes import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.Crossfade import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.LinearEasing Loading @@ -45,6 +47,7 @@ import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row Loading Loading @@ -151,6 +154,7 @@ import com.android.compose.theme.LocalAndroidColorScheme import com.android.compose.ui.graphics.painter.rememberDrawablePainter import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.ui.compose.load import com.android.systemui.qs.flags.QsEditModeTabs import com.android.systemui.qs.panels.shared.model.SizedTileImpl import com.android.systemui.qs.panels.ui.compose.DragAndDropState import com.android.systemui.qs.panels.ui.compose.DragType Loading Loading @@ -181,9 +185,11 @@ import com.android.systemui.qs.panels.ui.compose.selection.TileState import com.android.systemui.qs.panels.ui.compose.selection.rememberResizingState import com.android.systemui.qs.panels.ui.compose.selection.rememberSelectionState import com.android.systemui.qs.panels.ui.compose.selection.selectableTile import com.android.systemui.qs.panels.ui.compose.tabs.EditModeTabs import com.android.systemui.qs.panels.ui.model.GridCell import com.android.systemui.qs.panels.ui.model.SpacerGridCell import com.android.systemui.qs.panels.ui.model.TileGridCell import com.android.systemui.qs.panels.ui.viewmodel.EditModeTabViewModel import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModelConstants.APP_ICON_INLINE_CONTENT_ID import com.android.systemui.qs.panels.ui.viewmodel.InfiniteGridSnapshotViewModel Loading Loading @@ -281,6 +287,7 @@ fun DefaultEditTileGrid( } Scaffold( modifier = modifier, containerColor = Color.Transparent, topBar = { EditModeTopBar(onStopEditing = onStopEditing, modifier = Modifier.statusBarsPadding()) { Loading Loading @@ -319,9 +326,71 @@ fun DefaultEditTileGrid( } } if (QsEditModeTabs.isEnabled) { val editModeTabViewModel = remember { EditModeTabViewModel() } EditModeScrollableColumnWithTabs( listState = listState, selectionState = selectionState, innerPadding = innerPadding, scrollState = scrollState, onEditAction = onEditAction, editModeTabViewModel = editModeTabViewModel, ) { CurrentTilesGrid(listState, selectionState, onEditAction) AnimatedAvailableTilesGrid( allTiles = allTiles, listState = listState, selectionState = selectionState, onEditAction = onEditAction, showAvailableTiles = editModeTabViewModel.selectedTab.isTilesEditingAllowed, ) } } else { EditModeScrollableColumn( listState = listState, selectionState = selectionState, innerPadding = innerPadding, scrollState = scrollState, onEditAction = onEditAction, ) { CurrentTilesGridHeader( listState, selectionState, modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp), ) CurrentTilesGrid(listState, selectionState, onEditAction) // Only show available tiles when a drag or placement isn't in progress, OR the // drag is within the current tiles grid AnimatedAvailableTilesGrid( allTiles = allTiles, listState = listState, selectionState = selectionState, onEditAction = onEditAction, showAvailableTiles = !(listState.dragInProgress || selectionState.placementEnabled) || listState.dragType == DragType.Move, ) } } } } } @Composable private fun EditModeScrollableColumn( listState: EditTileListState, selectionState: MutableSelectionState, innerPadding: PaddingValues, scrollState: ScrollState, onEditAction: (EditAction) -> Unit, modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit, ) { Column( verticalArrangement = spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)), verticalArrangement = spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)), modifier = modifier .fillMaxSize() Loading @@ -341,74 +410,66 @@ fun DefaultEditTileGrid( } }, ) { CurrentTilesGridHeader( listState, selectionState, modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp), ) CurrentTilesGrid(listState, selectionState, onEditAction) content() // Sets a minimum height to be used when available tiles are hidden Box( Modifier.fillMaxWidth() .requiredHeightIn(AvailableTilesGridMinHeight) .animateContentSize() ) { // Only show available tiles when a drag or placement isn't in progress, OR // the drag is within the current tiles grid val showAvailableTiles = !(listState.dragInProgress || selectionState.placementEnabled) || listState.dragType == DragType.Move Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars)) } } // Using the fully qualified name here as a workaround for AnimatedVisibility // not being available from a Box androidx.compose.animation.AnimatedVisibility( visible = showAvailableTiles, enter = fadeIn(), exit = fadeOut(), @Composable private fun EditModeScrollableColumnWithTabs( listState: EditTileListState, selectionState: MutableSelectionState, innerPadding: PaddingValues, scrollState: ScrollState, onEditAction: (EditAction) -> Unit, editModeTabViewModel: EditModeTabViewModel, modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit, ) { // Hide available tiles when dragging Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)), modifier = modifier .fillMaxSize() // Apply top padding before the scroll so the scrollable doesn't show under // the top bar .padding(top = innerPadding.calculateTopPadding()), ) { Column( verticalArrangement = spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)), modifier = modifier.fillMaxSize(), ) { AvailableTileGrid( allTiles, selectionState, listState.columns, { onEditAction(EditAction.AddTile(it)) }, // Add to the end listState, ) TextButton( onClick = { selectionState.unSelect() onEditAction(EditAction.ResetGrid) modifier = Modifier.weight(1f) .fillMaxWidth() .clipScrollableContainer(Orientation.Vertical) .clip(RoundedCornerShape(GridBackgroundCornerRadius)) .verticalScroll(scrollState) .dragAndDropRemoveZone(listState) { spec, removalEnabled -> if (removalEnabled) { // If removal is enabled, remove the tile onEditAction(EditAction.RemoveTile(spec)) } else { // Otherwise submit the new tile ordering onEditAction(EditAction.SetTiles(listState.tileSpecs())) selectionState.select(spec) } }, colors = ButtonDefaults.textButtonColors( containerColor = MaterialTheme.colorScheme.primary, contentColor = MaterialTheme.colorScheme.onPrimary, ), modifier = Modifier.padding(vertical = 16.dp), ) { Text( text = stringResource(id = com.android.internal.R.string.reset), style = MaterialTheme.typography.labelLarge, TabGridHeader( editModeTabViewModel.selectedTab.headerResId, modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp), ) content() } } } } EditModeTabs(editModeTabViewModel) { selectionState.unSelect() } Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars)) } } } } @Composable private fun AutoScrollGrid( Loading Loading @@ -512,6 +573,13 @@ private fun CurrentTilesGridHeader( } } @Composable private fun TabGridHeader(@StringRes headerResId: Int, modifier: Modifier = Modifier) { Crossfade(targetState = headerResId, label = "QSEditHeader", modifier = modifier) { EditGridHeader { EditGridCenteredText(text = stringResource(id = it)) } } } @Composable private fun EditGridHeader( modifier: Modifier = Modifier, Loading Loading @@ -604,6 +672,64 @@ private fun CurrentTilesGrid( } } @Composable private fun AnimatedAvailableTilesGrid( allTiles: List<EditTileViewModel>, listState: EditTileListState, selectionState: MutableSelectionState, showAvailableTiles: Boolean, onEditAction: (EditAction) -> Unit, modifier: Modifier = Modifier, ) { // Sets a minimum height to be used when available tiles are hidden Box( Modifier.fillMaxWidth().requiredHeightIn(AvailableTilesGridMinHeight).animateContentSize() ) { // Using the fully qualified name here as a workaround for AnimatedVisibility not being // available from a Box androidx.compose.animation.AnimatedVisibility( visible = showAvailableTiles, enter = fadeIn(), exit = fadeOut(), ) { // Hide available tiles when dragging Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)), modifier = modifier.fillMaxSize(), ) { AvailableTileGrid( allTiles, selectionState, listState.columns, { onEditAction(EditAction.AddTile(it)) }, // Add to the end listState, ) TextButton( onClick = { selectionState.unSelect() onEditAction(EditAction.ResetGrid) }, colors = ButtonDefaults.textButtonColors( containerColor = MaterialTheme.colorScheme.primary, contentColor = MaterialTheme.colorScheme.onPrimary, ), modifier = Modifier.padding(vertical = 16.dp), ) { Text( text = stringResource(id = com.android.internal.R.string.reset), style = MaterialTheme.typography.labelLarge, ) } } } } } @Composable private fun AvailableTileGrid( tiles: List<EditTileViewModel>, Loading packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/tabs/EditModeTabs.kt 0 → 100644 +107 −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.tabs import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.updateTransition import androidx.compose.animation.expandIn import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkOut import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.FloatingToolbarDefaults import androidx.compose.material3.HorizontalFloatingToolbar import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.android.compose.modifiers.padding import com.android.compose.theme.LocalAndroidColorScheme import com.android.systemui.qs.panels.ui.viewmodel.EditModeTabViewModel @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun EditModeTabs( viewModel: EditModeTabViewModel, modifier: Modifier = Modifier, onTabChanged: () -> Unit = {}, ) { val containerColor = LocalAndroidColorScheme.current.surfaceEffect1 val selectedButtonColor = LocalAndroidColorScheme.current.surfaceEffect2 HorizontalFloatingToolbar( modifier = modifier.height(60.dp), expanded = true, contentPadding = PaddingValues(horizontal = 7.dp, vertical = 8.dp), colors = FloatingToolbarDefaults.standardFloatingToolbarColors( toolbarContainerColor = containerColor ), ) { viewModel.tabs.forEach { tab -> val isSelected = updateTransition(viewModel.selectedTab == tab) val selectionBackgroundAlpha by isSelected.animateFloat { if (it) 1f else 0f } Row( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxHeight() .clickable { if (!isSelected.currentState) { onTabChanged() } viewModel.selectTab(tab) } .padding(horizontal = 5.dp) .drawBehind { drawRoundRect( color = selectedButtonColor, alpha = selectionBackgroundAlpha, cornerRadius = CornerRadius(size.height / 2), ) } .padding(horizontal = 16.dp), ) { isSelected.AnimatedVisibility( visible = { it }, enter = (fadeIn() + expandIn(expandFrom = Alignment.Center)), exit = (fadeOut() + shrinkOut(shrinkTowards = Alignment.Center)), ) { Icon( imageVector = tab.titleIcon, contentDescription = null, modifier = Modifier.padding(end = 8.dp), ) } Text(stringResource(tab.titleResId)) } } } } packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/EditModeTab.kt 0 → 100644 +62 −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.model import androidx.annotation.StringRes import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Dashboard import androidx.compose.runtime.Stable import androidx.compose.ui.graphics.vector.ImageVector import com.android.systemui.qs.panels.ui.compose.icons.Edit import com.android.systemui.res.R /** Represents a tab from Quick Settings edit mode. */ @Stable sealed interface EditModeTab { /** Icon to use next to the title in the tabs group. */ val titleIcon: ImageVector /** Title to display in the tabs group. */ @get:StringRes val titleResId: Int /** Text to display on top of the edit page. */ @get:StringRes val headerResId: Int /** Whether tiles can be removed and added. */ val isTilesEditingAllowed: Boolean /** Whether tiles can be resized and reordered. */ val isTilesLayoutAllowed: Boolean /** A tab where tiles can be removed/added only. */ data object EditingTab : EditModeTab { override val titleIcon: ImageVector = Edit override val titleResId: Int = R.string.qs_edit_edit_tab override val headerResId: Int = R.string.tap_to_remove_tiles override val isTilesEditingAllowed: Boolean = true override val isTilesLayoutAllowed: Boolean = false } /** A tab where tiles can be resized/reordered only. */ data object LayoutTab : EditModeTab { override val titleIcon: ImageVector = Icons.Default.Dashboard override val titleResId: Int = R.string.qs_edit_layout_tab override val headerResId: Int = R.string.resize_and_reorder_tiles override val isTilesEditingAllowed: Boolean = false override val isTilesLayoutAllowed: Boolean = true } } packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeTabViewModel.kt 0 → 100644 +37 −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.viewmodel import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.android.systemui.qs.panels.ui.model.EditModeTab @Stable class EditModeTabViewModel { val tabs: List<EditModeTab> = listOf(EditModeTab.EditingTab, EditModeTab.LayoutTab) var selectedTab: EditModeTab by mutableStateOf(tabs.first()) private set fun selectTab(tab: EditModeTab) { if (tab in tabs) { selectedTab = tab } } } Loading
packages/SystemUI/res/values/strings.xml +12 −0 Original line number Diff line number Diff line Loading @@ -2684,6 +2684,18 @@ <!-- Label for header of customize QS to indicate that tiles can be selected to be edited [CHAR LIMIT=60] --> <string name="select_to_rearrange_tiles">Select tiles to rearrange and resize</string> <!-- Label for header of customize QS to indicate that tiles can be tapped to be removed [CHAR LIMIT=60] --> <string name="tap_to_remove_tiles">Tap to remove tiles</string> <!-- Label for header of customize QS to indicate that tiles can be resized and reordered [CHAR LIMIT=60] --> <string name="resize_and_reorder_tiles">Resize and reorder layout</string> <!-- Label for tab of customize QS used to add and remove tiles [CHAR LIMIT=60] --> <string name="qs_edit_edit_tab">Edit</string> <!-- Label for tab of customize QS used to reorder tiles [CHAR LIMIT=60] --> <string name="qs_edit_layout_tab">Layout</string> <!-- Label for placing tiles in edit mode for QS [CHAR LIMIT=60] --> <string name="tap_to_position_tile">Tap to position tile</string> Loading
packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt +208 −82 Original line number Diff line number Diff line Loading @@ -18,8 +18,10 @@ package com.android.systemui.qs.panels.ui.compose.infinitegrid import androidx.annotation.StringRes import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.Crossfade import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.LinearEasing Loading @@ -45,6 +47,7 @@ import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row Loading Loading @@ -151,6 +154,7 @@ import com.android.compose.theme.LocalAndroidColorScheme import com.android.compose.ui.graphics.painter.rememberDrawablePainter import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.ui.compose.load import com.android.systemui.qs.flags.QsEditModeTabs import com.android.systemui.qs.panels.shared.model.SizedTileImpl import com.android.systemui.qs.panels.ui.compose.DragAndDropState import com.android.systemui.qs.panels.ui.compose.DragType Loading Loading @@ -181,9 +185,11 @@ import com.android.systemui.qs.panels.ui.compose.selection.TileState import com.android.systemui.qs.panels.ui.compose.selection.rememberResizingState import com.android.systemui.qs.panels.ui.compose.selection.rememberSelectionState import com.android.systemui.qs.panels.ui.compose.selection.selectableTile import com.android.systemui.qs.panels.ui.compose.tabs.EditModeTabs import com.android.systemui.qs.panels.ui.model.GridCell import com.android.systemui.qs.panels.ui.model.SpacerGridCell import com.android.systemui.qs.panels.ui.model.TileGridCell import com.android.systemui.qs.panels.ui.viewmodel.EditModeTabViewModel import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModelConstants.APP_ICON_INLINE_CONTENT_ID import com.android.systemui.qs.panels.ui.viewmodel.InfiniteGridSnapshotViewModel Loading Loading @@ -281,6 +287,7 @@ fun DefaultEditTileGrid( } Scaffold( modifier = modifier, containerColor = Color.Transparent, topBar = { EditModeTopBar(onStopEditing = onStopEditing, modifier = Modifier.statusBarsPadding()) { Loading Loading @@ -319,9 +326,71 @@ fun DefaultEditTileGrid( } } if (QsEditModeTabs.isEnabled) { val editModeTabViewModel = remember { EditModeTabViewModel() } EditModeScrollableColumnWithTabs( listState = listState, selectionState = selectionState, innerPadding = innerPadding, scrollState = scrollState, onEditAction = onEditAction, editModeTabViewModel = editModeTabViewModel, ) { CurrentTilesGrid(listState, selectionState, onEditAction) AnimatedAvailableTilesGrid( allTiles = allTiles, listState = listState, selectionState = selectionState, onEditAction = onEditAction, showAvailableTiles = editModeTabViewModel.selectedTab.isTilesEditingAllowed, ) } } else { EditModeScrollableColumn( listState = listState, selectionState = selectionState, innerPadding = innerPadding, scrollState = scrollState, onEditAction = onEditAction, ) { CurrentTilesGridHeader( listState, selectionState, modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp), ) CurrentTilesGrid(listState, selectionState, onEditAction) // Only show available tiles when a drag or placement isn't in progress, OR the // drag is within the current tiles grid AnimatedAvailableTilesGrid( allTiles = allTiles, listState = listState, selectionState = selectionState, onEditAction = onEditAction, showAvailableTiles = !(listState.dragInProgress || selectionState.placementEnabled) || listState.dragType == DragType.Move, ) } } } } } @Composable private fun EditModeScrollableColumn( listState: EditTileListState, selectionState: MutableSelectionState, innerPadding: PaddingValues, scrollState: ScrollState, onEditAction: (EditAction) -> Unit, modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit, ) { Column( verticalArrangement = spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)), verticalArrangement = spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)), modifier = modifier .fillMaxSize() Loading @@ -341,74 +410,66 @@ fun DefaultEditTileGrid( } }, ) { CurrentTilesGridHeader( listState, selectionState, modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp), ) CurrentTilesGrid(listState, selectionState, onEditAction) content() // Sets a minimum height to be used when available tiles are hidden Box( Modifier.fillMaxWidth() .requiredHeightIn(AvailableTilesGridMinHeight) .animateContentSize() ) { // Only show available tiles when a drag or placement isn't in progress, OR // the drag is within the current tiles grid val showAvailableTiles = !(listState.dragInProgress || selectionState.placementEnabled) || listState.dragType == DragType.Move Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars)) } } // Using the fully qualified name here as a workaround for AnimatedVisibility // not being available from a Box androidx.compose.animation.AnimatedVisibility( visible = showAvailableTiles, enter = fadeIn(), exit = fadeOut(), @Composable private fun EditModeScrollableColumnWithTabs( listState: EditTileListState, selectionState: MutableSelectionState, innerPadding: PaddingValues, scrollState: ScrollState, onEditAction: (EditAction) -> Unit, editModeTabViewModel: EditModeTabViewModel, modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit, ) { // Hide available tiles when dragging Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)), modifier = modifier .fillMaxSize() // Apply top padding before the scroll so the scrollable doesn't show under // the top bar .padding(top = innerPadding.calculateTopPadding()), ) { Column( verticalArrangement = spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)), modifier = modifier.fillMaxSize(), ) { AvailableTileGrid( allTiles, selectionState, listState.columns, { onEditAction(EditAction.AddTile(it)) }, // Add to the end listState, ) TextButton( onClick = { selectionState.unSelect() onEditAction(EditAction.ResetGrid) modifier = Modifier.weight(1f) .fillMaxWidth() .clipScrollableContainer(Orientation.Vertical) .clip(RoundedCornerShape(GridBackgroundCornerRadius)) .verticalScroll(scrollState) .dragAndDropRemoveZone(listState) { spec, removalEnabled -> if (removalEnabled) { // If removal is enabled, remove the tile onEditAction(EditAction.RemoveTile(spec)) } else { // Otherwise submit the new tile ordering onEditAction(EditAction.SetTiles(listState.tileSpecs())) selectionState.select(spec) } }, colors = ButtonDefaults.textButtonColors( containerColor = MaterialTheme.colorScheme.primary, contentColor = MaterialTheme.colorScheme.onPrimary, ), modifier = Modifier.padding(vertical = 16.dp), ) { Text( text = stringResource(id = com.android.internal.R.string.reset), style = MaterialTheme.typography.labelLarge, TabGridHeader( editModeTabViewModel.selectedTab.headerResId, modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp), ) content() } } } } EditModeTabs(editModeTabViewModel) { selectionState.unSelect() } Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars)) } } } } @Composable private fun AutoScrollGrid( Loading Loading @@ -512,6 +573,13 @@ private fun CurrentTilesGridHeader( } } @Composable private fun TabGridHeader(@StringRes headerResId: Int, modifier: Modifier = Modifier) { Crossfade(targetState = headerResId, label = "QSEditHeader", modifier = modifier) { EditGridHeader { EditGridCenteredText(text = stringResource(id = it)) } } } @Composable private fun EditGridHeader( modifier: Modifier = Modifier, Loading Loading @@ -604,6 +672,64 @@ private fun CurrentTilesGrid( } } @Composable private fun AnimatedAvailableTilesGrid( allTiles: List<EditTileViewModel>, listState: EditTileListState, selectionState: MutableSelectionState, showAvailableTiles: Boolean, onEditAction: (EditAction) -> Unit, modifier: Modifier = Modifier, ) { // Sets a minimum height to be used when available tiles are hidden Box( Modifier.fillMaxWidth().requiredHeightIn(AvailableTilesGridMinHeight).animateContentSize() ) { // Using the fully qualified name here as a workaround for AnimatedVisibility not being // available from a Box androidx.compose.animation.AnimatedVisibility( visible = showAvailableTiles, enter = fadeIn(), exit = fadeOut(), ) { // Hide available tiles when dragging Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)), modifier = modifier.fillMaxSize(), ) { AvailableTileGrid( allTiles, selectionState, listState.columns, { onEditAction(EditAction.AddTile(it)) }, // Add to the end listState, ) TextButton( onClick = { selectionState.unSelect() onEditAction(EditAction.ResetGrid) }, colors = ButtonDefaults.textButtonColors( containerColor = MaterialTheme.colorScheme.primary, contentColor = MaterialTheme.colorScheme.onPrimary, ), modifier = Modifier.padding(vertical = 16.dp), ) { Text( text = stringResource(id = com.android.internal.R.string.reset), style = MaterialTheme.typography.labelLarge, ) } } } } } @Composable private fun AvailableTileGrid( tiles: List<EditTileViewModel>, Loading
packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/tabs/EditModeTabs.kt 0 → 100644 +107 −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.tabs import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.updateTransition import androidx.compose.animation.expandIn import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkOut import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.FloatingToolbarDefaults import androidx.compose.material3.HorizontalFloatingToolbar import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.android.compose.modifiers.padding import com.android.compose.theme.LocalAndroidColorScheme import com.android.systemui.qs.panels.ui.viewmodel.EditModeTabViewModel @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun EditModeTabs( viewModel: EditModeTabViewModel, modifier: Modifier = Modifier, onTabChanged: () -> Unit = {}, ) { val containerColor = LocalAndroidColorScheme.current.surfaceEffect1 val selectedButtonColor = LocalAndroidColorScheme.current.surfaceEffect2 HorizontalFloatingToolbar( modifier = modifier.height(60.dp), expanded = true, contentPadding = PaddingValues(horizontal = 7.dp, vertical = 8.dp), colors = FloatingToolbarDefaults.standardFloatingToolbarColors( toolbarContainerColor = containerColor ), ) { viewModel.tabs.forEach { tab -> val isSelected = updateTransition(viewModel.selectedTab == tab) val selectionBackgroundAlpha by isSelected.animateFloat { if (it) 1f else 0f } Row( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxHeight() .clickable { if (!isSelected.currentState) { onTabChanged() } viewModel.selectTab(tab) } .padding(horizontal = 5.dp) .drawBehind { drawRoundRect( color = selectedButtonColor, alpha = selectionBackgroundAlpha, cornerRadius = CornerRadius(size.height / 2), ) } .padding(horizontal = 16.dp), ) { isSelected.AnimatedVisibility( visible = { it }, enter = (fadeIn() + expandIn(expandFrom = Alignment.Center)), exit = (fadeOut() + shrinkOut(shrinkTowards = Alignment.Center)), ) { Icon( imageVector = tab.titleIcon, contentDescription = null, modifier = Modifier.padding(end = 8.dp), ) } Text(stringResource(tab.titleResId)) } } } }
packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/EditModeTab.kt 0 → 100644 +62 −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.model import androidx.annotation.StringRes import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Dashboard import androidx.compose.runtime.Stable import androidx.compose.ui.graphics.vector.ImageVector import com.android.systemui.qs.panels.ui.compose.icons.Edit import com.android.systemui.res.R /** Represents a tab from Quick Settings edit mode. */ @Stable sealed interface EditModeTab { /** Icon to use next to the title in the tabs group. */ val titleIcon: ImageVector /** Title to display in the tabs group. */ @get:StringRes val titleResId: Int /** Text to display on top of the edit page. */ @get:StringRes val headerResId: Int /** Whether tiles can be removed and added. */ val isTilesEditingAllowed: Boolean /** Whether tiles can be resized and reordered. */ val isTilesLayoutAllowed: Boolean /** A tab where tiles can be removed/added only. */ data object EditingTab : EditModeTab { override val titleIcon: ImageVector = Edit override val titleResId: Int = R.string.qs_edit_edit_tab override val headerResId: Int = R.string.tap_to_remove_tiles override val isTilesEditingAllowed: Boolean = true override val isTilesLayoutAllowed: Boolean = false } /** A tab where tiles can be resized/reordered only. */ data object LayoutTab : EditModeTab { override val titleIcon: ImageVector = Icons.Default.Dashboard override val titleResId: Int = R.string.qs_edit_layout_tab override val headerResId: Int = R.string.resize_and_reorder_tiles override val isTilesEditingAllowed: Boolean = false override val isTilesLayoutAllowed: Boolean = true } }
packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeTabViewModel.kt 0 → 100644 +37 −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.viewmodel import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.android.systemui.qs.panels.ui.model.EditModeTab @Stable class EditModeTabViewModel { val tabs: List<EditModeTab> = listOf(EditModeTab.EditingTab, EditModeTab.LayoutTab) var selectedTab: EditModeTab by mutableStateOf(tabs.first()) private set fun selectTab(tab: EditModeTab) { if (tab in tabs) { selectedTab = tab } } }