diff options
| author | 2024-09-19 10:07:38 -0400 | |
|---|---|---|
| committer | 2024-09-19 15:47:29 -0400 | |
| commit | 0cb05f05e545dee6bf14859dede076de4e9c5ac1 (patch) | |
| tree | 11136fe91f130f7ede1a2121272636f28bd8fbf8 | |
| parent | 5cf50a5f8d72087766042d2c5786560faa59ba8a (diff) | |
Add expansion animation to QSFragmentCompose
This uses an STL with a single transition between QQS and QS, and
progress set between 0 and 1.
This only tracks the expansion value passed from NPVC and not the other
values (like translation or squishiness). Those will be added in a
followup CL.
Test: atest QSFragmentComposeViewModelTest
Test: atest PlatformScenarioTests
Flag: com.android.systemui.qs_ui_refactor_compose_fragment
Bug: 353254353
Change-Id: I7f86affab32d54f0c15e11a108e0f72e1383fe89
13 files changed, 355 insertions, 128 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt index f8d0588c9ae6..8e6cb3fe9fb9 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt @@ -22,6 +22,7 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -34,6 +35,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.ContentScope +import com.android.compose.animation.scene.SceneScope import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.battery.BatteryMeterViewController @@ -41,6 +43,7 @@ import com.android.systemui.brightness.ui.compose.BrightnessSliderContainer import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.dagger.SysUISingleton import com.android.systemui.lifecycle.rememberViewModel +import com.android.systemui.qs.composefragment.ui.GridAnchor import com.android.systemui.qs.panels.ui.compose.EditMode import com.android.systemui.qs.panels.ui.compose.TileGrid import com.android.systemui.qs.ui.viewmodel.QuickSettingsContainerViewModel @@ -79,16 +82,11 @@ constructor( } @Composable - override fun ContentScope.Content( - modifier: Modifier, - ) { + override fun ContentScope.Content(modifier: Modifier) { val viewModel = rememberViewModel("QuickSettingsShadeOverlay") { contentViewModelFactory.create() } - OverlayShade( - modifier = modifier, - onScrimClicked = viewModel::onScrimClicked, - ) { + OverlayShade(modifier = modifier, onScrimClicked = viewModel::onScrimClicked) { Column { ExpandedShadeHeader( viewModelFactory = viewModel.shadeHeaderViewModelFactory, @@ -98,40 +96,36 @@ constructor( modifier = Modifier.padding(QuickSettingsShade.Dimensions.Padding), ) - ShadeBody( - viewModel = viewModel.quickSettingsContainerViewModel, - ) + ShadeBody(viewModel = viewModel.quickSettingsContainerViewModel) } } } } @Composable -fun ShadeBody( - viewModel: QuickSettingsContainerViewModel, -) { +fun SceneScope.ShadeBody(viewModel: QuickSettingsContainerViewModel) { val isEditing by viewModel.editModeViewModel.isEditing.collectAsStateWithLifecycle() AnimatedContent( targetState = isEditing, - transitionSpec = { fadeIn(tween(500)) togetherWith fadeOut(tween(500)) } + transitionSpec = { fadeIn(tween(500)) togetherWith fadeOut(tween(500)) }, ) { editing -> if (editing) { EditMode( viewModel = viewModel.editModeViewModel, - modifier = Modifier.fillMaxWidth().padding(QuickSettingsShade.Dimensions.Padding) + modifier = Modifier.fillMaxWidth().padding(QuickSettingsShade.Dimensions.Padding), ) } else { QuickSettingsLayout( viewModel = viewModel, - modifier = Modifier.sysuiResTag("quick_settings_panel") + modifier = Modifier.sysuiResTag("quick_settings_panel"), ) } } } @Composable -private fun QuickSettingsLayout( +private fun SceneScope.QuickSettingsLayout( viewModel: QuickSettingsContainerViewModel, modifier: Modifier = Modifier, ) { @@ -143,15 +137,18 @@ private fun QuickSettingsLayout( BrightnessSliderContainer( viewModel = viewModel.brightnessSliderViewModel, modifier = - Modifier.fillMaxWidth() - .height(QuickSettingsShade.Dimensions.BrightnessSliderHeight), - ) - TileGrid( - viewModel = viewModel.tileGridViewModel, - modifier = - Modifier.fillMaxWidth().heightIn(max = QuickSettingsShade.Dimensions.GridMaxHeight), - viewModel.editModeViewModel::startEditing, + Modifier.fillMaxWidth().height(QuickSettingsShade.Dimensions.BrightnessSliderHeight), ) + Box { + GridAnchor() + TileGrid( + viewModel = viewModel.tileGridViewModel, + modifier = + Modifier.fillMaxWidth() + .heightIn(max = QuickSettingsShade.Dimensions.GridMaxHeight), + viewModel.editModeViewModel::startEditing, + ) + } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt index 7203b61ecc9f..6f20e70f84a8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt @@ -78,8 +78,6 @@ class QSFragmentComposeViewModelTest : SysuiTestCase() { Dispatchers.resetMain() } - // For now the state changes at 0.5f expansion. This will change once we implement animation - // (and this test will fail) @Test fun qsExpansionValueChanges_correctExpansionState() = with(kosmos) { @@ -87,18 +85,27 @@ class QSFragmentComposeViewModelTest : SysuiTestCase() { val expansionState by collectLastValue(underTest.expansionState) underTest.qsExpansionValue = 0f - assertThat(expansionState) - .isEqualTo(QSFragmentComposeViewModel.QSExpansionState.QQS) + assertThat(expansionState!!.progress).isEqualTo(0f) underTest.qsExpansionValue = 0.3f - assertThat(expansionState) - .isEqualTo(QSFragmentComposeViewModel.QSExpansionState.QQS) - - underTest.qsExpansionValue = 0.7f - assertThat(expansionState).isEqualTo(QSFragmentComposeViewModel.QSExpansionState.QS) + assertThat(expansionState!!.progress).isEqualTo(0.3f) underTest.qsExpansionValue = 1f - assertThat(expansionState).isEqualTo(QSFragmentComposeViewModel.QSExpansionState.QS) + assertThat(expansionState!!.progress).isEqualTo(1f) + } + } + + @Test + fun qsExpansionValueChanges_clamped() = + with(kosmos) { + testScope.testWithinLifecycle { + val expansionState by collectLastValue(underTest.expansionState) + + underTest.qsExpansionValue = -1f + assertThat(expansionState!!.progress).isEqualTo(0f) + + underTest.qsExpansionValue = 2f + assertThat(expansionState!!.progress).isEqualTo(1f) } } @@ -110,7 +117,7 @@ class QSFragmentComposeViewModelTest : SysuiTestCase() { testableContext.orCreateTestableResources.addOverride( R.bool.config_use_large_screen_shade_header, - true + true, ) fakeConfigurationRepository.onConfigurationChange() @@ -126,7 +133,7 @@ class QSFragmentComposeViewModelTest : SysuiTestCase() { testableContext.orCreateTestableResources.addOverride( R.bool.config_use_large_screen_shade_header, - false + false, ) fakeConfigurationRepository.onConfigurationChange() diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt index af167d4f6918..c174038aafe4 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt @@ -26,7 +26,6 @@ import android.view.ViewGroup import androidx.activity.OnBackPressedDispatcher import androidx.activity.OnBackPressedDispatcherOwner import androidx.activity.setViewTreeOnBackPressedDispatcherOwner -import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -38,10 +37,14 @@ import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.positionInRoot @@ -51,11 +54,18 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.CustomAccessibilityAction import androidx.compose.ui.semantics.customActions import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.round import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle +import com.android.compose.animation.scene.MutableSceneTransitionLayoutState +import com.android.compose.animation.scene.SceneKey +import com.android.compose.animation.scene.SceneScope +import com.android.compose.animation.scene.SceneTransitionLayout +import com.android.compose.animation.scene.content.state.TransitionState +import com.android.compose.animation.scene.transitions import com.android.compose.modifiers.height import com.android.compose.modifiers.padding import com.android.compose.modifiers.thenIf @@ -70,11 +80,17 @@ import com.android.systemui.media.dagger.MediaModule.QS_PANEL import com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL import com.android.systemui.plugins.qs.QS import com.android.systemui.plugins.qs.QSContainerController +import com.android.systemui.qs.composefragment.SceneKeys.QuickQuickSettings +import com.android.systemui.qs.composefragment.SceneKeys.QuickSettings +import com.android.systemui.qs.composefragment.SceneKeys.toIdleSceneKey import com.android.systemui.qs.composefragment.ui.notificationScrimClip +import com.android.systemui.qs.composefragment.ui.quickQuickSettingsToQuickSettings import com.android.systemui.qs.composefragment.viewmodel.QSFragmentComposeViewModel import com.android.systemui.qs.flags.QSComposeFragment import com.android.systemui.qs.footer.ui.compose.FooterActions import com.android.systemui.qs.panels.ui.compose.QuickQuickSettings +import com.android.systemui.qs.shared.ui.ElementKeys +import com.android.systemui.qs.ui.composable.QuickSettingsShade import com.android.systemui.qs.ui.composable.QuickSettingsTheme import com.android.systemui.qs.ui.composable.ShadeBody import com.android.systemui.res.R @@ -86,11 +102,13 @@ import java.io.PrintWriter import java.util.function.Consumer import javax.inject.Inject import javax.inject.Named +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @SuppressLint("ValidFragment") @@ -166,33 +184,48 @@ constructor( setContent { PlatformTheme { val visible by viewModel.qsVisible.collectAsStateWithLifecycle() - val qsState by viewModel.expansionState.collectAsStateWithLifecycle() AnimatedVisibility( visible = visible, modifier = - Modifier.windowInsetsPadding(WindowInsets.navigationBars).thenIf( - notificationScrimClippingParams.isEnabled - ) { - Modifier.notificationScrimClip( - notificationScrimClippingParams.leftInset, - notificationScrimClippingParams.top, - notificationScrimClippingParams.rightInset, - notificationScrimClippingParams.bottom, - notificationScrimClippingParams.radius, - ) - }, - ) { - AnimatedContent(targetState = qsState) { - when (it) { - QSFragmentComposeViewModel.QSExpansionState.QQS -> { - QuickQuickSettingsElement() - } - QSFragmentComposeViewModel.QSExpansionState.QS -> { - QuickSettingsElement() + Modifier.windowInsetsPadding(WindowInsets.navigationBars) + .thenIf(notificationScrimClippingParams.isEnabled) { + Modifier.notificationScrimClip( + notificationScrimClippingParams.leftInset, + notificationScrimClippingParams.top, + notificationScrimClippingParams.rightInset, + notificationScrimClippingParams.bottom, + notificationScrimClippingParams.radius, + ) } - else -> {} - } + .graphicsLayer { elevation = 4.dp.toPx() }, + ) { + val sceneState = remember { + MutableSceneTransitionLayoutState( + viewModel.expansionState.value.toIdleSceneKey(), + transitions = + transitions { + from(QuickQuickSettings, QuickSettings) { + quickQuickSettingsToQuickSettings() + } + }, + ) + } + + LaunchedEffect(Unit) { + synchronizeQsState( + sceneState, + viewModel.expansionState.map { it.progress }, + ) + } + + SceneTransitionLayout( + state = sceneState, + modifier = Modifier.fillMaxSize(), + ) { + scene(QuickSettings) { QuickSettingsElement() } + + scene(QuickQuickSettings) { QuickQuickSettingsElement() } } } } @@ -420,7 +453,7 @@ constructor( } @Composable - private fun QuickQuickSettingsElement() { + private fun SceneScope.QuickQuickSettingsElement() { val qqsPadding by viewModel.qqsHeaderHeight.collectAsStateWithLifecycle() val bottomPadding = dimensionResource(id = R.dimen.qqs_layout_padding_bottom) DisposableEffect(Unit) { @@ -450,8 +483,15 @@ constructor( viewModel = viewModel.containerViewModel.quickQuickSettingsViewModel, modifier = Modifier.collapseExpandSemanticAction( - stringResource(id = R.string.accessibility_quick_settings_expand) - ), + stringResource( + id = R.string.accessibility_quick_settings_expand + ) + ) + .padding( + horizontal = { + QuickSettingsShade.Dimensions.Padding.roundToPx() + } + ), ) } } @@ -460,7 +500,7 @@ constructor( } @Composable - private fun QuickSettingsElement() { + private fun SceneScope.QuickSettingsElement() { val qqsPadding by viewModel.qqsHeaderHeight.collectAsStateWithLifecycle() val qsExtraPadding = dimensionResource(R.dimen.qs_panel_padding_top) Column( @@ -471,7 +511,10 @@ constructor( ) { val qsEnabled by viewModel.qsEnabled.collectAsStateWithLifecycle() if (qsEnabled) { - Box(modifier = Modifier.fillMaxSize().weight(1f)) { + Box( + modifier = + Modifier.element(ElementKeys.QuickSettingsContent).fillMaxSize().weight(1f) + ) { Column { Spacer( modifier = Modifier.height { qqsPadding + qsExtraPadding.roundToPx() } @@ -483,7 +526,9 @@ constructor( FooterActions( viewModel = viewModel.footerActionsViewModel, qsVisibilityLifecycleOwner = this@QSFragmentCompose, - modifier = Modifier.sysuiResTag("qs_footer_actions"), + modifier = + Modifier.sysuiResTag("qs_footer_actions") + .element(ElementKeys.FooterActions), ) } } @@ -590,3 +635,85 @@ private val instanceProvider = return currentId++ } } + +object SceneKeys { + val QuickQuickSettings = SceneKey("QuickQuickSettingsScene") + val QuickSettings = SceneKey("QuickSettingsScene") + + fun QSFragmentComposeViewModel.QSExpansionState.toIdleSceneKey(): SceneKey { + return when { + progress < 0.5f -> QuickQuickSettings + else -> QuickSettings + } + } +} + +suspend fun synchronizeQsState(state: MutableSceneTransitionLayoutState, expansion: Flow<Float>) { + coroutineScope { + val animationScope = this + + var currentTransition: ExpansionTransition? = null + + fun snapTo(scene: SceneKey) { + state.snapToScene(scene) + currentTransition = null + } + + expansion.collectLatest { progress -> + when (progress) { + 0f -> snapTo(QuickQuickSettings) + 1f -> snapTo(QuickSettings) + else -> { + val transition = currentTransition + if (transition != null) { + transition.progress = progress + return@collectLatest + } + + val newTransition = + ExpansionTransition(progress).also { currentTransition = it } + state.startTransitionImmediately( + animationScope = animationScope, + transition = newTransition, + ) + } + } + } + } +} + +private class ExpansionTransition(currentProgress: Float) : + TransitionState.Transition.ChangeScene( + fromScene = QuickQuickSettings, + toScene = QuickSettings, + ) { + override val currentScene: SceneKey + get() { + // This should return the logical scene. If the QS STLState is only driven by + // synchronizeQSState() then it probably does not matter which one we return, this is + // only used to compute the current user actions of a STL. + return QuickQuickSettings + } + + override var progress: Float by mutableFloatStateOf(currentProgress) + + override val progressVelocity: Float + get() = 0f + + override val isInitiatedByUserInput: Boolean + get() = true + + override val isUserInputOngoing: Boolean + get() = true + + private val finishCompletable = CompletableDeferred<Unit>() + + override suspend fun run() { + // This transition runs until it is interrupted by another one. + finishCompletable.await() + } + + override fun freezeAndAnimateToCurrentState() { + finishCompletable.complete(Unit) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/FromQuickQuickSettingsToQuickSettings.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/FromQuickQuickSettingsToQuickSettings.kt new file mode 100644 index 000000000000..1514986d16d9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/FromQuickQuickSettingsToQuickSettings.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 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.composefragment.ui + +import com.android.compose.animation.scene.TransitionBuilder +import com.android.systemui.qs.shared.ui.ElementKeys + +fun TransitionBuilder.quickQuickSettingsToQuickSettings() { + + fractionRange(start = 0.5f) { fade(ElementKeys.QuickSettingsContent) } + + fractionRange(start = 0.9f) { fade(ElementKeys.FooterActions) } + + anchoredTranslate(ElementKeys.QuickSettingsContent, ElementKeys.GridAnchor) +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/GridAnchor.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/GridAnchor.kt new file mode 100644 index 000000000000..f0f46d33b83d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/GridAnchor.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 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.composefragment.ui + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.android.compose.animation.scene.SceneScope +import com.android.systemui.qs.shared.ui.ElementKeys + +/** + * This composable is used at the start of the tiles in QQS and QS to anchor the expansion and be + * able to have relative anchor translation of elements that appear in QS. + */ +@Composable +fun SceneScope.GridAnchor(modifier: Modifier = Modifier) { + // The size of this anchor does not matter, as the tiles don't change size on expansion. + Spacer(modifier.element(ElementKeys.GridAnchor)) +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt index 7ab11d22ee49..7300ee1053ff 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt @@ -147,7 +147,7 @@ constructor( .stateIn( lifecycleScope, SharingStarted.WhileSubscribed(), - disableFlagsRepository.disableFlags.value.isQuickSettingsEnabled() + disableFlagsRepository.disableFlags.value.isQuickSettingsEnabled(), ) private val _showCollapsedOnKeyguard = MutableStateFlow(false) @@ -213,19 +213,11 @@ constructor( } val expansionState: StateFlow<QSExpansionState> = - combine( - _stackScrollerOverscrolling, - _qsExpanded, - _qsExpansion, - ) { args: Array<Any> -> + combine(_stackScrollerOverscrolling, _qsExpanded, _qsExpansion) { args: Array<Any> -> val expansion = args[2] as Float - if (expansion > 0.5f) { - QSExpansionState.QS - } else { - QSExpansionState.QQS - } + QSExpansionState(expansion.coerceIn(0f, 1f)) } - .stateIn(lifecycleScope, SharingStarted.WhileSubscribed(), QSExpansionState.QQS) + .stateIn(lifecycleScope, SharingStarted.WhileSubscribed(), QSExpansionState(0f)) /** * Accessibility action for collapsing/expanding QS. The provided runnable is responsible for @@ -262,13 +254,6 @@ constructor( fun create(lifecycleScope: LifecycleCoroutineScope): QSFragmentComposeViewModel } - sealed interface QSExpansionState { - data object QQS : QSExpansionState - - data object QS : QSExpansionState - - @JvmInline value class Expanding(val progress: Float) : QSExpansionState - - @JvmInline value class Collapsing(val progress: Float) : QSExpansionState - } + // In the future, this will have other relevant elements like squishiness. + data class QSExpansionState(@FloatRange(0.0, 1.0) val progress: Float) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt index fd276c2dd220..0c02b400646c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt @@ -18,6 +18,7 @@ package com.android.systemui.qs.panels.ui.compose import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.android.compose.animation.scene.SceneScope import com.android.systemui.qs.panels.shared.model.SizedTile import com.android.systemui.qs.panels.shared.model.TileRow import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel @@ -27,7 +28,7 @@ import com.android.systemui.qs.pipeline.shared.TileSpec /** A layout of tiles, indicating how they should be composed when showing in QS or in edit mode. */ interface GridLayout { @Composable - fun TileGrid( + fun SceneScope.TileGrid( tiles: List<TileViewModel>, modifier: Modifier, editModeStart: () -> Unit, @@ -66,7 +67,7 @@ interface PaginatableGridLayout : GridLayout { */ fun splitInRows( tiles: List<SizedTile<TileViewModel>>, - columns: Int + columns: Int, ): List<List<SizedTile<TileViewModel>>> { val row = TileRow<TileViewModel>(columns) diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt index 08a56bf29f66..083f529a21da 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt @@ -39,6 +39,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.compose.animation.scene.SceneScope import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.qs.panels.dagger.PaginatedBaseLayoutType import com.android.systemui.qs.panels.ui.compose.PaginatedGridLayout.Dimensions.FooterHeight @@ -55,7 +56,7 @@ constructor( @PaginatedBaseLayoutType private val delegateGridLayout: PaginatableGridLayout, ) : GridLayout by delegateGridLayout { @Composable - override fun TileGrid( + override fun SceneScope.TileGrid( tiles: List<TileViewModel>, modifier: Modifier, editModeStart: () -> Unit, @@ -85,16 +86,16 @@ constructor( ) { val page = pages[it] - delegateGridLayout.TileGrid(tiles = page, modifier = Modifier, editModeStart = {}) + with(delegateGridLayout) { + TileGrid(tiles = page, modifier = Modifier, editModeStart = {}) + } } - Box( - modifier = Modifier.height(FooterHeight).fillMaxWidth(), - ) { + Box(modifier = Modifier.height(FooterHeight).fillMaxWidth()) { PagerDots( pagerState = pagerState, activeColor = MaterialTheme.colorScheme.primary, nonActiveColor = MaterialTheme.colorScheme.surfaceVariant, - modifier = Modifier.align(Alignment.Center) + modifier = Modifier.align(Alignment.Center), ) CompositionLocalProvider(value = LocalContentColor provides Color.White) { IconButton( @@ -103,7 +104,7 @@ constructor( ) { Icon( imageVector = Icons.Default.Edit, - contentDescription = stringResource(id = R.string.qs_edit) + contentDescription = stringResource(id = R.string.qs_edit), ) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt index f4acbec1063c..8998a7f5d815 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt @@ -16,21 +16,28 @@ package com.android.systemui.qs.panels.ui.compose -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.util.fastMap import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.compose.animation.scene.SceneScope 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.panels.ui.compose.infinitegrid.Tile -import com.android.systemui.qs.panels.ui.compose.infinitegrid.TileLazyGrid import com.android.systemui.qs.panels.ui.viewmodel.QuickQuickSettingsViewModel +import com.android.systemui.qs.shared.ui.ElementKeys.toElementKey +import com.android.systemui.res.R @Composable -fun QuickQuickSettings(viewModel: QuickQuickSettingsViewModel, modifier: Modifier = Modifier) { +fun SceneScope.QuickQuickSettings( + viewModel: QuickQuickSettingsViewModel, + modifier: Modifier = Modifier, +) { val sizedTiles by viewModel.tileViewModels.collectAsStateWithLifecycle(initialValue = emptyList()) val tiles = sizedTiles.fastMap { it.tile } @@ -41,20 +48,20 @@ fun QuickQuickSettings(viewModel: QuickQuickSettingsViewModel, modifier: Modifie onDispose { tiles.forEach { it.stopListening(token) } } } val columns by viewModel.columns.collectAsStateWithLifecycle() - - TileLazyGrid( - modifier = modifier.sysuiResTag("qqs_tile_layout"), - columns = GridCells.Fixed(columns), - ) { - items( - sizedTiles.size, - key = { index -> sizedTiles[index].tile.spec.spec }, - span = { index -> GridItemSpan(sizedTiles[index].width) }, - ) { index -> + Box(modifier = modifier) { + GridAnchor() + VerticalSpannedGrid( + columns = columns, + columnSpacing = dimensionResource(R.dimen.qs_tile_margin_horizontal), + rowSpacing = dimensionResource(R.dimen.qs_tile_margin_vertical), + spans = sizedTiles.fastMap { it.width }, + modifier = Modifier.sysuiResTag("qqs_tile_layout"), + ) { spanIndex -> + val it = sizedTiles[spanIndex] Tile( - tile = sizedTiles[index].tile, - iconOnly = sizedTiles[index].isIcon, - modifier = Modifier, + tile = it.tile, + iconOnly = it.isIcon, + modifier = Modifier.element(it.tile.spec.toElementKey(spanIndex)), ) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileGrid.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileGrid.kt index 8c57d41b2123..1a5297b10e37 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileGrid.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileGrid.kt @@ -20,16 +20,17 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.compose.animation.scene.SceneScope import com.android.systemui.qs.panels.ui.viewmodel.TileGridViewModel @Composable -fun TileGrid( +fun SceneScope.TileGrid( viewModel: TileGridViewModel, modifier: Modifier = Modifier, - editModeStart: () -> Unit + editModeStart: () -> Unit, ) { val gridLayout by viewModel.gridLayout.collectAsStateWithLifecycle() val tiles by viewModel.tileViewModels.collectAsStateWithLifecycle(emptyList()) - gridLayout.TileGrid(tiles, modifier, editModeStart) + with(gridLayout) { TileGrid(tiles, modifier, editModeStart) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt index f96c27dc3ef6..f3b283ed8679 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt @@ -16,15 +16,17 @@ package com.android.systemui.qs.panels.ui.compose.infinitegrid -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.util.fastMap import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.compose.animation.scene.SceneScope import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.grid.ui.compose.VerticalSpannedGrid import com.android.systemui.qs.panels.shared.model.SizedTileImpl import com.android.systemui.qs.panels.ui.compose.PaginatableGridLayout import com.android.systemui.qs.panels.ui.compose.rememberEditListState @@ -33,6 +35,8 @@ import com.android.systemui.qs.panels.ui.viewmodel.FixedColumnsSizeViewModel import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModel import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.shared.ui.ElementKeys.toElementKey +import com.android.systemui.res.R import javax.inject.Inject @SysUISingleton @@ -44,7 +48,7 @@ constructor( ) : PaginatableGridLayout { @Composable - override fun TileGrid( + override fun SceneScope.TileGrid( tiles: List<TileViewModel>, modifier: Modifier, editModeStart: () -> Unit, @@ -57,15 +61,18 @@ constructor( val columns by gridSizeViewModel.columns.collectAsStateWithLifecycle() val sizedTiles = tiles.map { SizedTileImpl(it, it.spec.width()) } - TileLazyGrid(modifier = modifier, columns = GridCells.Fixed(columns)) { - items(sizedTiles.size, span = { index -> GridItemSpan(sizedTiles[index].width) }) { - index -> - Tile( - tile = sizedTiles[index].tile, - iconOnly = iconTilesViewModel.isIconTile(sizedTiles[index].tile.spec), - modifier = Modifier, - ) - } + VerticalSpannedGrid( + columns = columns, + columnSpacing = dimensionResource(R.dimen.qs_tile_margin_horizontal), + rowSpacing = dimensionResource(R.dimen.qs_tile_margin_vertical), + spans = sizedTiles.fastMap { it.width }, + ) { spanIndex -> + val it = sizedTiles[spanIndex] + Tile( + tile = it.tile, + iconOnly = iconTilesViewModel.isIconTile(it.tile.spec), + modifier = Modifier.element(it.tile.spec.toElementKey(spanIndex)), + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt index aa6c08eecd76..45aad825a70f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt @@ -140,6 +140,7 @@ fun Tile(tile: TileViewModel, iconOnly: Boolean, modifier: Modifier) { } }, onLongClick = { tile.onLongClick(expandable) }, + accessibilityUiState = uiState.accessibilityUiState, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/shared/ui/ElementKeys.kt b/packages/SystemUI/src/com/android/systemui/qs/shared/ui/ElementKeys.kt new file mode 100644 index 000000000000..625459d1c6fa --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/shared/ui/ElementKeys.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 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.shared.ui + +import com.android.compose.animation.scene.ElementKey +import com.android.systemui.qs.pipeline.shared.TileSpec + +/** Element keys to be used by the compose implementation of QS for animations. */ +object ElementKeys { + val QuickSettingsContent = ElementKey("QuickSettingsContent") + val GridAnchor = ElementKey("QuickSettingsGridAnchor") + val FooterActions = ElementKey("FooterActions") + + class TileElementKey(spec: TileSpec, val position: Int) : ElementKey(spec.spec, spec.spec) + + fun TileSpec.toElementKey(positionInGrid: Int) = TileElementKey(this, positionInGrid) +} |