Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6dba7047 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add tabs at the bottom of edit mode" into main

parents 6cf83a68 86117970
Loading
Loading
Loading
Loading
+12 −0
Original line number Diff line number Diff line
@@ -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>

+208 −82
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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
@@ -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
@@ -281,6 +287,7 @@ fun DefaultEditTileGrid(
    }

    Scaffold(
        modifier = modifier,
        containerColor = Color.Transparent,
        topBar = {
            EditModeTopBar(onStopEditing = onStopEditing, modifier = Modifier.statusBarsPadding()) {
@@ -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()
@@ -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(
@@ -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,
@@ -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>,
+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))
            }
        }
    }
}
+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
    }
}
+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