diff options
30 files changed, 864 insertions, 167 deletions
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt index aa567364d130..76931a2b4d41 100644 --- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt @@ -22,12 +22,15 @@ import android.view.View import android.view.WindowInsets import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.LifecycleOwner +import com.android.compose.theme.LocalAndroidColorScheme import com.android.compose.theme.PlatformTheme import com.android.compose.ui.platform.DensityAwareComposeView import com.android.internal.policy.ScreenDecorationsUtils @@ -89,12 +92,18 @@ object ComposeFacade : BaseComposeFacade { ) { activity.setContent { PlatformTheme { - CommunalHub( - viewModel = viewModel, - onOpenWidgetPicker = onOpenWidgetPicker, - widgetConfigurator = widgetConfigurator, - onEditDone = onEditDone, - ) + Box( + modifier = + Modifier.fillMaxSize() + .background(LocalAndroidColorScheme.current.outlineVariant), + ) { + CommunalHub( + viewModel = viewModel, + onOpenWidgetPicker = onOpenWidgetPicker, + widgetConfigurator = widgetConfigurator, + onEditDone = onEditDone, + ) + } } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index bc85513ae296..be5aa8a4c3b9 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -1,6 +1,7 @@ package com.android.systemui.communal.ui.compose import androidx.compose.animation.core.tween +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable @@ -12,6 +13,7 @@ import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.FixedSizeEdgeDetector +import com.android.compose.animation.scene.LowestZIndexScenePicker import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.SceneScope @@ -21,6 +23,7 @@ import com.android.compose.animation.scene.SwipeDirection import com.android.compose.animation.scene.observableTransitionState import com.android.compose.animation.scene.transitions import com.android.compose.animation.scene.updateSceneTransitionLayoutState +import com.android.compose.theme.LocalAndroidColorScheme import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState import com.android.systemui.communal.ui.compose.extensions.allowGestures @@ -31,16 +34,25 @@ import kotlinx.coroutines.flow.transform object Communal { object Elements { + val Scrim = ElementKey("Scrim", scenePicker = LowestZIndexScenePicker) val Content = ElementKey("CommunalContent") } } val sceneTransitions = transitions { - from(TransitionSceneKey.Blank, to = TransitionSceneKey.Communal) { - spec = tween(durationMillis = 500) - + to(TransitionSceneKey.Communal) { + spec = tween(durationMillis = 1000) + translate(Communal.Elements.Content, Edge.Right) + timestampRange(startMillis = 167, endMillis = 334) { + fade(Communal.Elements.Scrim) + fade(Communal.Elements.Content) + } + } + to(TransitionSceneKey.Blank) { + spec = tween(durationMillis = 1000) translate(Communal.Elements.Content, Edge.Right) - fade(Communal.Elements.Content) + timestampRange(endMillis = 167) { fade(Communal.Elements.Content) } + timestampRange(startMillis = 167, endMillis = 334) { fade(Communal.Elements.Scrim) } } } @@ -111,6 +123,12 @@ private fun SceneScope.CommunalScene( viewModel: BaseCommunalViewModel, modifier: Modifier = Modifier, ) { + Box( + modifier = + Modifier.element(Communal.Elements.Scrim) + .fillMaxSize() + .background(LocalAndroidColorScheme.current.outlineVariant), + ) Box(modifier.element(Communal.Elements.Content)) { CommunalHub(viewModel = viewModel) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index 090750e046a7..cddd4fa1c33d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -141,7 +141,6 @@ fun CommunalHub( modifier = modifier .fillMaxSize() - .background(LocalAndroidColorScheme.current.outlineVariant) .pointerInput(gridState, contentOffset, contentListState) { // If not in edit mode, don't allow selecting items. if (!viewModel.isEditMode) return@pointerInput diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt index 8a35ef11a364..a6715dfcec24 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt @@ -8,7 +8,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.complication.ComplicationHostViewController -import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel +import com.android.systemui.dreams.ui.viewmodel.DreamOverlayViewModel import com.android.systemui.log.core.FakeLogBuffer import com.android.systemui.statusbar.BlurUtils import com.android.systemui.statusbar.policy.ConfigurationController @@ -47,7 +47,7 @@ class DreamOverlayAnimationsControllerTest : SysuiTestCase() { @Mock private lateinit var statusBarViewController: DreamOverlayStatusBarViewController @Mock private lateinit var stateController: DreamOverlayStateController @Mock private lateinit var configController: ConfigurationController - @Mock private lateinit var transitionViewModel: DreamingToLockscreenTransitionViewModel + @Mock private lateinit var transitionViewModel: DreamOverlayViewModel private val logBuffer = FakeLogBuffer.Factory.create() private lateinit var controller: DreamOverlayAnimationsController diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModelTest.kt new file mode 100644 index 000000000000..4defe8a08d3d --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModelTest.kt @@ -0,0 +1,97 @@ +/* + * 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.keyguard.ui.viewmodel + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectValues +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.google.common.collect.Range +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class DreamingToGlanceableHubTransitionViewModelTest : SysuiTestCase() { + val kosmos = testKosmos() + val testScope = kosmos.testScope + + val underTest by lazy { kosmos.dreamingToGlanceableHubTransitionViewModel } + + @Test + fun dreamOverlayAlpha() = + testScope.runTest { + val values by collectValues(underTest.dreamOverlayAlpha) + assertThat(values).isEmpty() + + kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( + listOf( + // Should start running here... + step(0f, TransitionState.STARTED), + step(0f), + step(0.1f), + step(0.5f), + // Up to here... + step(1f), + ), + testScope, + ) + + assertThat(values).hasSize(4) + values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) } + } + + @Test + fun dreamOverlayTranslationX() = + testScope.runTest { + val values by collectValues(underTest.dreamOverlayTranslationX(100)) + assertThat(values).isEmpty() + + kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( + listOf( + step(0f, TransitionState.STARTED), + step(0.3f), + step(0.6f), + ), + testScope, + ) + + assertThat(values).hasSize(3) + values.forEach { assertThat(it).isIn(Range.closed(-100f, 0f)) } + } + + private fun step( + value: Float, + state: TransitionState = TransitionState.RUNNING + ): TransitionStep { + return TransitionStep( + from = KeyguardState.DREAMING, + to = KeyguardState.GLANCEABLE_HUB, + value = value, + transitionState = state, + ownerName = "DreamingToGlanceableHubTransitionViewModelTest" + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelTest.kt new file mode 100644 index 000000000000..64125f139a2e --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelTest.kt @@ -0,0 +1,114 @@ +/* + * 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.keyguard.ui.viewmodel + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository +import com.android.systemui.coroutines.collectValues +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.kosmos.testScope +import com.android.systemui.res.R +import com.android.systemui.testKosmos +import com.google.common.collect.Range +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class GlanceableHubToLockscreenTransitionViewModelTest : SysuiTestCase() { + val kosmos = testKosmos() + val testScope = kosmos.testScope + + val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository + val configurationRepository = kosmos.fakeConfigurationRepository + val underTest by lazy { kosmos.glanceableHubToLockscreenTransitionViewModel } + + @Test + fun lockscreenFadeIn() = + testScope.runTest { + val values by collectValues(underTest.keyguardAlpha) + assertThat(values).containsExactly(0f) + + keyguardTransitionRepository.sendTransitionSteps( + listOf( + step(0f, TransitionState.STARTED), + // Should start running here... + step(0.1f), + step(0.2f), + step(0.3f), + step(0.4f), + // ...up to here + step(0.5f), + step(0.6f), + step(0.7f), + step(0.8f), + step(1f), + ), + testScope, + ) + + assertThat(values).hasSize(5) + values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) } + } + + @Test + fun lockscreenTranslationX() = + testScope.runTest { + configurationRepository.setDimensionPixelSize( + R.dimen.hub_to_lockscreen_transition_lockscreen_translation_x, + 100 + ) + val values by collectValues(underTest.keyguardTranslationX) + assertThat(values).isEmpty() + + keyguardTransitionRepository.sendTransitionSteps( + listOf( + step(0f, TransitionState.STARTED), + step(0f), + step(0.3f), + step(0.5f), + step(1f), + ), + testScope, + ) + + assertThat(values).hasSize(5) + values.forEach { assertThat(it.value).isIn(Range.closed(-100f, 0f)) } + } + + private fun step( + value: Float, + state: TransitionState = TransitionState.RUNNING + ): TransitionStep { + return TransitionStep( + from = KeyguardState.GLANCEABLE_HUB, + to = KeyguardState.LOCKSCREEN, + value = value, + transitionState = state, + ownerName = this::class.java.simpleName + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModelTest.kt new file mode 100644 index 000000000000..241d0b818193 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModelTest.kt @@ -0,0 +1,115 @@ +/* + * 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.keyguard.ui.viewmodel + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository +import com.android.systemui.coroutines.collectValues +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.kosmos.testScope +import com.android.systemui.res.R +import com.android.systemui.testKosmos +import com.google.common.collect.Range +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class LockscreenToGlanceableHubTransitionViewModelTest : SysuiTestCase() { + val kosmos = testKosmos() + val testScope = kosmos.testScope + + val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository + val configurationRepository = kosmos.fakeConfigurationRepository + val underTest by lazy { kosmos.lockscreenToGlanceableHubTransitionViewModel } + + @Test + fun lockscreenFadeOut() = + testScope.runTest { + val values by collectValues(underTest.keyguardAlpha) + assertThat(values).containsExactly(1f) + + keyguardTransitionRepository.sendTransitionSteps( + listOf( + // Should start running here + step(0f, TransitionState.STARTED), + step(0.1f), + step(0.2f), + // ...up to here + step(0.3f), + step(0.4f), + step(0.5f), + step(0.6f), + step(0.7f), + step(0.8f), + // ...up to here + step(1f), + ), + testScope, + ) + + assertThat(values).hasSize(4) + values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) } + } + + @Test + fun lockscreenTranslationX() = + testScope.runTest { + configurationRepository.setDimensionPixelSize( + R.dimen.lockscreen_to_hub_transition_lockscreen_translation_x, + -100 + ) + val values by collectValues(underTest.keyguardTranslationX) + assertThat(values).isEmpty() + + keyguardTransitionRepository.sendTransitionSteps( + listOf( + step(0f, TransitionState.STARTED), + step(0f), + step(0.3f), + step(0.5f), + step(1f), + ), + testScope, + ) + + assertThat(values).hasSize(5) + values.forEach { assertThat(it.value).isIn(Range.closed(-100f, 0f)) } + } + + private fun step( + value: Float, + state: TransitionState = TransitionState.RUNNING + ): TransitionStep { + return TransitionStep( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GLANCEABLE_HUB, + value = value, + transitionState = state, + ownerName = this::class.java.simpleName + ) + } +} diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index a681da3adc4e..65120a99a6b5 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1514,6 +1514,12 @@ <!-- The amount of vertical offset for the keyguard during the full shade transition. --> <dimen name="lockscreen_shade_keyguard_transition_vertical_offset">0dp</dimen> + <!-- LOCKSCREEN -> GLANCEABLE_HUB transition: Amount to shift lockscreen content on entering --> + <dimen name="lockscreen_to_hub_transition_lockscreen_translation_x">-824dp</dimen> + + <!-- GLANCEABLE_HUB -> LOCKSCREEN transition: Amount to shift lockscreen content on entering --> + <dimen name="hub_to_lockscreen_transition_lockscreen_translation_x">824dp</dimen> + <!-- Distance that the full shade transition takes in order for media to fully transition to the shade --> <dimen name="lockscreen_shade_media_transition_distance">120dp</dimen> @@ -1857,6 +1863,7 @@ <dimen name="dream_overlay_y_offset">80dp</dimen> <dimen name="dream_overlay_entry_y_offset">40dp</dimen> <dimen name="dream_overlay_exit_y_offset">40dp</dimen> + <dimen name="dream_overlay_exit_x_offset">824dp</dimen> <dimen name="status_view_margin_horizontal">0dp</dimen> diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt index 557ad132bc9f..9000da33312c 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt @@ -33,20 +33,16 @@ import com.android.systemui.complication.ComplicationLayoutParams.POSITION_BOTTO import com.android.systemui.complication.ComplicationLayoutParams.POSITION_TOP import com.android.systemui.complication.ComplicationLayoutParams.Position import com.android.systemui.dreams.dagger.DreamOverlayModule -import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel +import com.android.systemui.dreams.ui.viewmodel.DreamOverlayViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.DreamLog -import com.android.systemui.res.R import com.android.systemui.statusbar.BlurUtils import com.android.systemui.statusbar.CrossFadeHelper import com.android.systemui.statusbar.policy.ConfigurationController -import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener import javax.inject.Inject import javax.inject.Named -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch /** Controller for dream overlay animations. */ @@ -58,7 +54,7 @@ constructor( private val mStatusBarViewController: DreamOverlayStatusBarViewController, private val mOverlayStateController: DreamOverlayStateController, @Named(DreamOverlayModule.DREAM_BLUR_RADIUS) private val mDreamBlurRadius: Int, - private val transitionViewModel: DreamingToLockscreenTransitionViewModel, + private val dreamOverlayViewModel: DreamOverlayViewModel, private val configController: ConfigurationController, @Named(DreamOverlayModule.DREAM_IN_BLUR_ANIMATION_DURATION) private val mDreamInBlurAnimDurationMs: Long, @@ -91,59 +87,45 @@ constructor( this.view = view view.repeatWhenAttached { - val configurationBasedDimensions = MutableStateFlow(loadFromResources(view)) - val configCallback = - object : ConfigurationListener { - override fun onDensityOrFontScaleChanged() { - configurationBasedDimensions.value = loadFromResources(view) + repeatOnLifecycle(Lifecycle.State.CREATED) { + launch { + dreamOverlayViewModel.dreamOverlayTranslationY.collect { px -> + ComplicationLayoutParams.iteratePositions( + { position: Int -> setElementsTranslationYAtPosition(px, position) }, + POSITION_TOP or POSITION_BOTTOM + ) } } - configController.addCallback(configCallback) - - try { - repeatOnLifecycle(Lifecycle.State.CREATED) { - /* Translation animations, when moving from DREAMING->LOCKSCREEN state */ - launch { - configurationBasedDimensions - .flatMapLatest { - transitionViewModel.dreamOverlayTranslationY(it.translationYPx) - } - .collect { px -> - ComplicationLayoutParams.iteratePositions( - { position: Int -> - setElementsTranslationYAtPosition(px, position) - }, - POSITION_TOP or POSITION_BOTTOM - ) - } + launch { + dreamOverlayViewModel.dreamOverlayTranslationX.collect { px -> + ComplicationLayoutParams.iteratePositions( + { position: Int -> setElementsTranslationXAtPosition(px, position) }, + POSITION_TOP or POSITION_BOTTOM + ) } + } - /* Alpha animations, when moving from DREAMING->LOCKSCREEN state */ - launch { - transitionViewModel.dreamOverlayAlpha.collect { alpha -> - ComplicationLayoutParams.iteratePositions( - { position: Int -> - setElementsAlphaAtPosition( - alpha = alpha, - position = position, - fadingOut = true, - ) - }, - POSITION_TOP or POSITION_BOTTOM - ) - } + launch { + dreamOverlayViewModel.dreamOverlayAlpha.collect { alpha -> + ComplicationLayoutParams.iteratePositions( + { position: Int -> + setElementsAlphaAtPosition( + alpha = alpha, + position = position, + fadingOut = true, + ) + }, + POSITION_TOP or POSITION_BOTTOM + ) } + } - launch { - transitionViewModel.transitionEnded.collect { _ -> - mOverlayStateController.setExitAnimationsRunning(false) - } + launch { + dreamOverlayViewModel.transitionEnded.collect { _ -> + mOverlayStateController.setExitAnimationsRunning(false) } } - } finally { - // Ensure the callback is removed when cancellation happens - configController.removeCallback(configCallback) } } } @@ -373,14 +355,10 @@ constructor( } } - private fun loadFromResources(view: View): ConfigurationBasedDimensions { - return ConfigurationBasedDimensions( - translationYPx = - view.resources.getDimensionPixelSize(R.dimen.dream_overlay_exit_y_offset), - ) + /** Sets x translation of complications at the specified position. */ + private fun setElementsTranslationXAtPosition(translationX: Float, position: Int) { + mComplicationHostViewController.getViewsAtPosition(position).forEach { v -> + v.translationX = translationX + } } - - private data class ConfigurationBasedDimensions( - val translationYPx: Int, - ) } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamOverlayViewModel.kt new file mode 100644 index 000000000000..dd67a4c8706c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamOverlayViewModel.kt @@ -0,0 +1,61 @@ +/* + * 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.dreams.ui.viewmodel + +import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.ui.viewmodel.DreamingToGlanceableHubTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel +import com.android.systemui.res.R +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.merge + +@OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton +class DreamOverlayViewModel +@Inject +constructor( + configurationInteractor: ConfigurationInteractor, + private val toGlanceableHubTransitionViewModel: DreamingToGlanceableHubTransitionViewModel, + private val toLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel, +) { + + val dreamOverlayTranslationX: Flow<Float> = + configurationInteractor + .dimensionPixelSize(R.dimen.dream_overlay_exit_x_offset) + .flatMapLatest { px: Int -> + toGlanceableHubTransitionViewModel.dreamOverlayTranslationX(px) + } + + val dreamOverlayTranslationY: Flow<Float> = + configurationInteractor + .dimensionPixelSize(R.dimen.dream_overlay_exit_y_offset) + .flatMapLatest { px: Int -> + toLockscreenTransitionViewModel.dreamOverlayTranslationY(px) + } + + val dreamOverlayAlpha: Flow<Float> = + merge( + toLockscreenTransitionViewModel.dreamOverlayAlpha, + toGlanceableHubTransitionViewModel.dreamOverlayAlpha, + ) + + val transitionEnded = toLockscreenTransitionViewModel.transitionEnded +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt index 13ffd6396cda..c6594ef317d6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.domain.interactor import android.animation.ValueAnimator import com.android.app.animation.Interpolators +import com.android.systemui.Flags.communalHub import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main @@ -44,6 +45,7 @@ constructor( @Background bgDispatcher: CoroutineDispatcher, @Main mainDispatcher: CoroutineDispatcher, private val keyguardInteractor: KeyguardInteractor, + private val glanceableHubTransitions: GlanceableHubTransitions, ) : TransitionInteractor( fromState = KeyguardState.DREAMING, @@ -57,6 +59,17 @@ constructor( listenForDreamingToGone() listenForDreamingToAodOrDozing() listenForTransitionToCamera(scope, keyguardInteractor) + listenForDreamingToGlanceableHub() + } + + private fun listenForDreamingToGlanceableHub() { + if (!communalHub()) return + glanceableHubTransitions.listenForGlanceableHubTransition( + transitionName = "listenForDreamingToGlanceableHub", + transitionOwnerName = TAG, + fromState = KeyguardState.DREAMING, + toState = KeyguardState.GLANCEABLE_HUB, + ) } fun startToLockscreenTransition() { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt index 71d941ad8d22..fbf195eb0952 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt @@ -20,7 +20,6 @@ import android.animation.ValueAnimator import com.android.app.animation.Interpolators import com.android.app.tracing.coroutines.launch import com.android.systemui.Flags -import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main @@ -31,7 +30,7 @@ import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleMultiple import com.android.systemui.util.kotlin.sample import javax.inject.Inject -import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -71,7 +70,11 @@ constructor( override fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator { return ValueAnimator().apply { interpolator = Interpolators.LINEAR - duration = DEFAULT_DURATION.inWholeMilliseconds + duration = + when (toState) { + KeyguardState.LOCKSCREEN -> TO_LOCKSCREEN_DURATION + else -> DEFAULT_DURATION + }.inWholeMilliseconds } } @@ -80,10 +83,11 @@ constructor( * transition. */ private fun listenForHubToLockscreen() { - glanceableHubTransitions.listenForLockscreenAndHubTransition( + glanceableHubTransitions.listenForGlanceableHubTransition( transitionName = "listenForHubToLockscreen", transitionOwnerName = TAG, - toScene = CommunalSceneKey.Blank, + fromState = KeyguardState.GLANCEABLE_HUB, + toState = KeyguardState.LOCKSCREEN, ) } @@ -175,7 +179,7 @@ constructor( companion object { const val TAG = "FromGlanceableHubTransitionInteractor" - val DEFAULT_DURATION = 400.milliseconds + val DEFAULT_DURATION = 1.seconds val TO_LOCKSCREEN_DURATION = DEFAULT_DURATION } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt index 57e9ac707965..40b2c638823d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt @@ -19,7 +19,6 @@ package com.android.systemui.keyguard.domain.interactor import android.animation.ValueAnimator import android.util.MathUtils import com.android.app.animation.Interpolators -import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main @@ -39,6 +38,7 @@ import com.android.systemui.util.kotlin.sample import java.util.UUID import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -361,10 +361,11 @@ constructor( return } - glanceableHubTransitions.listenForLockscreenAndHubTransition( + glanceableHubTransitions.listenForGlanceableHubTransition( transitionName = "listenForLockscreenToGlanceableHub", transitionOwnerName = TAG, - toScene = CommunalSceneKey.Communal + fromState = KeyguardState.LOCKSCREEN, + toState = KeyguardState.GLANCEABLE_HUB, ) } @@ -380,6 +381,7 @@ constructor( KeyguardState.AOD -> TO_AOD_DURATION KeyguardState.DOZING -> TO_DOZING_DURATION KeyguardState.DREAMING_LOCKSCREEN_HOSTED -> TO_DREAMING_HOSTED_DURATION + KeyguardState.GLANCEABLE_HUB -> TO_GLANCEABLE_HUB_DURATION else -> DEFAULT_DURATION }.inWholeMilliseconds } @@ -395,6 +397,6 @@ constructor( val TO_AOD_DURATION = 500.milliseconds val TO_PRIMARY_BOUNCER_DURATION = DEFAULT_DURATION val TO_GONE_DURATION = DEFAULT_DURATION - val TO_GLANCEABLE_HUB_DURATION = DEFAULT_DURATION + val TO_GLANCEABLE_HUB_DURATION = 1.seconds } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt index ca661536d988..809c0aee9882 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt @@ -52,20 +52,18 @@ constructor( * externally. The progress is used for both transitions caused by user touch input or by * programmatic changes. */ - fun listenForLockscreenAndHubTransition( + fun listenForGlanceableHubTransition( transitionName: String, transitionOwnerName: String, - toScene: CommunalSceneKey + fromState: KeyguardState, + toState: KeyguardState, ) { - val fromState: KeyguardState - val toState: KeyguardState - if (toScene == CommunalSceneKey.Blank) { - fromState = KeyguardState.GLANCEABLE_HUB - toState = KeyguardState.LOCKSCREEN - } else { - fromState = KeyguardState.LOCKSCREEN - toState = KeyguardState.GLANCEABLE_HUB - } + val toScene = + if (toState == KeyguardState.GLANCEABLE_HUB) { + CommunalSceneKey.Communal + } else { + CommunalSceneKey.Blank + } var transitionId: UUID? = null scope.launch("$transitionOwnerName#$transitionName") { communalInteractor diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt index 8b278cdb9a6c..b8ba09801ee8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt @@ -185,13 +185,16 @@ constructor( return getOrCreateFlow(edge) .map { step -> StateToValue( - step.transitionState, - when (step.transitionState) { - STARTED -> stepToValue(step) - RUNNING -> stepToValue(step) - CANCELED -> onCancel?.invoke() - FINISHED -> onFinish?.invoke() - } + from = step.from, + to = step.to, + transitionState = step.transitionState, + value = + when (step.transitionState) { + STARTED -> stepToValue(step) + RUNNING -> stepToValue(step) + CANCELED -> onCancel?.invoke() + FINISHED -> onFinish?.invoke() + } ) .also { logger.logTransitionStep(name, step, it.value) } } @@ -208,6 +211,10 @@ constructor( } data class StateToValue( + val from: KeyguardState? = null, + val to: KeyguardState? = null, val transitionState: TransitionState = TransitionState.FINISHED, val value: Float? = 0f, -) +) { + fun isToOrFrom(state: KeyguardState) = from == state || to == state +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index c58a03c05a09..ed488487f524 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -43,6 +43,7 @@ import com.android.systemui.common.shared.model.Text import com.android.systemui.common.shared.model.TintedIcon import com.android.systemui.common.ui.ConfigurationState import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel @@ -101,6 +102,10 @@ object KeyguardRootViewBinder { val burnInLayerId = R.id.burn_in_layer val aodNotificationIconContainerId = R.id.aod_notification_icon_container val largeClockId = R.id.lockscreen_clock_view_large + val indicationArea = R.id.keyguard_indication_area + val startButton = R.id.start_button + val endButton = R.id.end_button + val lockIcon = R.id.lock_icon_view if (keyguardBottomAreaRefactor()) { view.setOnTouchListener { _, event -> @@ -200,10 +205,29 @@ object KeyguardRootViewBinder { launch { burnInParams .flatMapLatest { params -> viewModel.translationX(params) } - .collect { x -> - childViews[burnInLayerId]?.translationX = x - childViews[largeClockId]?.translationX = x - childViews[aodNotificationIconContainerId]?.translationX = x + .collect { state -> + val px = state.value ?: return@collect + when { + state.isToOrFrom(KeyguardState.AOD) -> { + childViews[largeClockId]?.translationX = px + childViews[burnInLayerId]?.translationX = px + childViews[aodNotificationIconContainerId] + ?.translationX = px + } + state.isToOrFrom(KeyguardState.GLANCEABLE_HUB) -> { + for ((key, childView) in childViews.entries) { + when (key) { + indicationArea, + startButton, + endButton, + lockIcon -> { + // Do not move these views + } + else -> childView.translationX = px + } + } + } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModel.kt new file mode 100644 index 000000000000..374a93275ff2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModel.kt @@ -0,0 +1,59 @@ +/* + * 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.keyguard.ui.viewmodel + +import com.android.app.animation.Interpolators.EMPHASIZED +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow +import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.flow.Flow + +@SysUISingleton +class DreamingToGlanceableHubTransitionViewModel +@Inject +constructor(animationFlow: KeyguardTransitionAnimationFlow) { + + private val transitionAnimation = + animationFlow.setup( + duration = TO_GLANCEABLE_HUB_DURATION, + from = KeyguardState.DREAMING, + to = KeyguardState.GLANCEABLE_HUB, + ) + + fun dreamOverlayTranslationX(translatePx: Int): Flow<Float> { + return transitionAnimation.sharedFlow( + duration = TO_GLANCEABLE_HUB_DURATION, + onStep = { it * -translatePx }, + interpolator = EMPHASIZED, + name = "DREAMING->GLANCEABLE_HUB: overlayTranslationX", + ) + } + + val dreamOverlayAlpha: Flow<Float> = + transitionAnimation.sharedFlow( + duration = 167.milliseconds, + onStep = { 1f - it }, + name = "DREAMING->GLANCEABLE_HUB: dreamOverlayAlpha", + ) + + private companion object { + val TO_GLANCEABLE_HUB_DURATION = 1.seconds + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt index 6aa2ecabae75..e5b596419efe 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt @@ -16,13 +16,22 @@ package com.android.systemui.keyguard.ui.viewmodel +import com.android.app.animation.Interpolators.EMPHASIZED +import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.keyguard.domain.interactor.FromGlanceableHubTransitionInteractor +import com.android.systemui.keyguard.domain.interactor.FromGlanceableHubTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow +import com.android.systemui.keyguard.ui.StateToValue +import com.android.systemui.res.R import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart /** * Breaks down GLANCEABLE_HUB->LOCKSCREEN transition into discrete steps for corresponding views to @@ -33,32 +42,43 @@ import kotlinx.coroutines.flow.Flow class GlanceableHubToLockscreenTransitionViewModel @Inject constructor( + configurationInteractor: ConfigurationInteractor, animationFlow: KeyguardTransitionAnimationFlow, ) { private val transitionAnimation = animationFlow.setup( - duration = FromGlanceableHubTransitionInteractor.TO_LOCKSCREEN_DURATION, + duration = TO_LOCKSCREEN_DURATION, from = KeyguardState.GLANCEABLE_HUB, to = KeyguardState.LOCKSCREEN, ) - // TODO(b/315205222): implement full animation spec instead of just a simple fade. val keyguardAlpha: Flow<Float> = - transitionAnimation.sharedFlow( - duration = FromGlanceableHubTransitionInteractor.TO_LOCKSCREEN_DURATION, - onStep = { it }, - onFinish = { 1f }, - onCancel = { 0f }, - name = "GLANCEABLE_HUB->LOCKSCREEN: keyguardAlpha", - ) + transitionAnimation + .sharedFlow( + duration = 167.milliseconds, + startTime = 167.milliseconds, + onStep = { it }, + onFinish = { 1f }, + onCancel = { 0f }, + name = "GLANCEABLE_HUB->LOCKSCREEN: keyguardAlpha", + ) + .onStart { emit(0f) } - // TODO(b/315205216): implement full animation spec instead of just a simple fade. - val notificationAlpha: Flow<Float> = - transitionAnimation.sharedFlow( - duration = FromGlanceableHubTransitionInteractor.TO_LOCKSCREEN_DURATION, - onStep = { it }, - onFinish = { 1f }, - onCancel = { 0f }, - name = "GLANCEABLE_HUB->LOCKSCREEN: notificationAlpha", - ) + val keyguardTranslationX: Flow<StateToValue> = + configurationInteractor + .dimensionPixelSize(R.dimen.hub_to_lockscreen_transition_lockscreen_translation_x) + .flatMapLatest { translatePx: Int -> + transitionAnimation.sharedFlowWithState( + duration = TO_LOCKSCREEN_DURATION, + onStep = { value -> -translatePx + value * translatePx }, + interpolator = EMPHASIZED, + onCancel = { -translatePx.toFloat() }, + name = "GLANCEABLE_HUB->LOCKSCREEN: keyguardTranslationX" + ) + } + + val notificationAlpha: Flow<Float> = keyguardAlpha + + val notificationTranslationX: Flow<Float> = + keyguardTranslationX.map { it.value }.filterNotNull() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt index f790d356620d..35119338fb32 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt @@ -32,6 +32,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.GONE import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING import com.android.systemui.keyguard.shared.model.TransitionState.STARTED +import com.android.systemui.keyguard.ui.StateToValue import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor import com.android.systemui.statusbar.phone.DozeParameters import com.android.systemui.statusbar.phone.ScreenOffAnimationController @@ -165,8 +166,12 @@ constructor( return aodBurnInViewModel.translationY(params) } - fun translationX(params: BurnInParameters): Flow<Float> { - return aodBurnInViewModel.translationX(params) + fun translationX(params: BurnInParameters): Flow<StateToValue> { + return merge( + aodBurnInViewModel.translationX(params).map { StateToValue(to = AOD, value = it) }, + lockscreenToGlanceableHubTransitionViewModel.keyguardTranslationX, + glanceableHubToLockscreenTransitionViewModel.keyguardTranslationX, + ) } fun scale(params: BurnInParameters): Flow<BurnInScaleViewModel> { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt index 3afa49e50167..978e71e2a825 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt @@ -16,13 +16,22 @@ package com.android.systemui.keyguard.ui.viewmodel +import com.android.app.animation.Interpolators.EMPHASIZED +import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow +import com.android.systemui.keyguard.ui.StateToValue +import com.android.systemui.res.R import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart /** * Breaks down LOCKSCREEN->GLANCEABLE_HUB transition into discrete steps for corresponding views to @@ -33,6 +42,7 @@ import kotlinx.coroutines.flow.Flow class LockscreenToGlanceableHubTransitionViewModel @Inject constructor( + configurationInteractor: ConfigurationInteractor, animationFlow: KeyguardTransitionAnimationFlow, ) { private val transitionAnimation = @@ -42,23 +52,35 @@ constructor( to = KeyguardState.GLANCEABLE_HUB, ) - // TODO(b/315205222): implement full animation spec instead of just a simple fade. val keyguardAlpha: Flow<Float> = - transitionAnimation.sharedFlow( - duration = FromLockscreenTransitionInteractor.TO_GLANCEABLE_HUB_DURATION, - onStep = { 1f - it }, - onFinish = { 0f }, - onCancel = { 1f }, - name = "LOCKSCREEN->GLANCEABLE_HUB: keyguardAlpha", - ) + transitionAnimation + .sharedFlow( + duration = 167.milliseconds, + onStep = { 1f - it }, + onFinish = { 0f }, + onCancel = { 1f }, + name = "LOCKSCREEN->GLANCEABLE_HUB: keyguardAlpha", + ) + .onStart { emit(1f) } - // TODO(b/315205216): implement full animation spec instead of just a simple fade. - val notificationAlpha: Flow<Float> = - transitionAnimation.sharedFlow( - duration = FromLockscreenTransitionInteractor.TO_GLANCEABLE_HUB_DURATION, - onStep = { 1f - it }, - onFinish = { 0f }, - onCancel = { 1f }, - name = "LOCKSCREEN->GLANCEABLE_HUB: notificationAlpha", - ) + val keyguardTranslationX: Flow<StateToValue> = + configurationInteractor + .dimensionPixelSize(R.dimen.lockscreen_to_hub_transition_lockscreen_translation_x) + .flatMapLatest { translatePx: Int -> + transitionAnimation.sharedFlowWithState( + duration = FromLockscreenTransitionInteractor.TO_GLANCEABLE_HUB_DURATION, + onStep = { value -> value * translatePx }, + // Move notifications back to their original position since they can be + // accessed from the shade. + onFinish = { 0f }, + onCancel = { 0f }, + interpolator = EMPHASIZED, + name = "LOCKSCREEN->GLANCEABLE_HUB: keyguardTranslationX" + ) + } + + val notificationAlpha: Flow<Float> = keyguardAlpha + + val notificationTranslationX: Flow<Float> = + keyguardTranslationX.map { it.value }.filterNotNull() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index d2ff266319d0..715505d81d71 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -1039,6 +1039,11 @@ public class NotificationStackScrollLayoutController implements Dumpable { mView.setTranslationY(translationY); } + /** Set view x-translation */ + public void setTranslationX(float translationX) { + mView.setTranslationX(translationX); + } + public int indexOfChild(View view) { return mView.indexOfChild(view); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt index daea8af8f334..77e146b98242 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt @@ -144,6 +144,8 @@ object SharedNotificationContainerBinder { .collect { y -> controller.setTranslationY(y) } } + launch { viewModel.translationX.collect { x -> controller.translationX = x } } + if (!sceneContainerFlags.isEnabled()) { launch { viewModel.expansionAlpha(viewState).collect { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt index f325157c598d..6b949a3461e7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt @@ -363,14 +363,9 @@ constructor( lockscreenToGlanceableHubRunning, glanceableHubToLockscreenRunning, merge( - lockscreenToGlanceableHubTransitionViewModel.notificationAlpha, - glanceableHubToLockscreenTransitionViewModel.notificationAlpha, - ) - .onStart { - // Transition flows don't emit a value on start, kick things off so the - // combine starts. - emit(1f) - } + lockscreenToGlanceableHubTransitionViewModel.notificationAlpha, + glanceableHubToLockscreenTransitionViewModel.notificationAlpha, + ) ) { lockscreenToGlanceableHubRunning, glanceableHubToLockscreenRunning, alpha -> if (isOnGlanceableHubWithoutShade) { // Notifications should not be visible on the glanceable hub. @@ -409,6 +404,16 @@ constructor( } /** + * The container may need to be translated in the x direction as the keyguard fades out, such as + * when swiping open the glanceable hub from the lockscreen. + */ + val translationX: Flow<Float> = + merge( + lockscreenToGlanceableHubTransitionViewModel.notificationTranslationX, + glanceableHubToLockscreenTransitionViewModel.notificationTranslationX, + ) + + /** * When on keyguard, there is limited space to display notifications so calculate how many could * be shown. Otherwise, there is no limit since the vertical space will be scrollable. * diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt index 5b93df5a0bad..a5d577dceecb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt @@ -57,7 +57,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceTimeBy -import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -148,6 +147,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { keyguardInteractor = keyguardInteractor, transitionRepository = transitionRepository, transitionInteractor = transitionInteractor, + glanceableHubTransitions = glanceableHubTransitions, ) .apply { start() } @@ -1372,6 +1372,44 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { } @Test + fun dreamingToGlanceableHub() = + testScope.runTest { + // GIVEN a prior transition has run to DREAMING + keyguardRepository.setDreaming(true) + runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.DREAMING) + runCurrent() + + // WHEN a transition to the glanceable hub starts + val currentScene = CommunalSceneKey.Blank + val targetScene = CommunalSceneKey.Communal + + val progress = MutableStateFlow(0f) + val transitionState = + MutableStateFlow<ObservableCommunalTransitionState>( + ObservableCommunalTransitionState.Transition( + fromScene = currentScene, + toScene = targetScene, + progress = progress, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + ) + ) + communalInteractor.setTransitionState(transitionState) + progress.value = .1f + runCurrent() + + assertThat(transitionRepository) + .startedTransition( + ownerName = FromDreamingTransitionInteractor::class.simpleName, + from = KeyguardState.DREAMING, + to = KeyguardState.GLANCEABLE_HUB, + animatorAssertion = { it.isNull() }, // transition should be manually animated + ) + + coroutineContext.cancelChildren() + } + + @Test fun lockscreenToOccluded() = testScope.runTest { // GIVEN a prior transition has run to LOCKSCREEN diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt index 0e9197ef8ac1..f0607f4b70e1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt @@ -22,7 +22,8 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository -import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING +import com.android.systemui.keyguard.shared.model.KeyguardState.GONE import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.kosmos.testScope @@ -51,8 +52,8 @@ class KeyguardTransitionAnimationFlowTest : SysuiTestCase() { underTest = animationFlow.setup( duration = 1000.milliseconds, - from = KeyguardState.GONE, - to = KeyguardState.DREAMING, + from = GONE, + to = DREAMING, ) } @@ -192,17 +193,65 @@ class KeyguardTransitionAnimationFlowTest : SysuiTestCase() { runCurrent() repository.sendTransitionStep(step(0f, TransitionState.STARTED)) - assertThat(animationValues).isEqualTo(StateToValue(TransitionState.STARTED, 0f)) + assertThat(animationValues) + .isEqualTo( + StateToValue( + from = GONE, + to = DREAMING, + transitionState = TransitionState.STARTED, + value = 0f + ) + ) repository.sendTransitionStep(step(0.3f, TransitionState.RUNNING)) - assertThat(animationValues).isEqualTo(StateToValue(TransitionState.RUNNING, 0.6f)) + assertThat(animationValues) + .isEqualTo( + StateToValue( + from = GONE, + to = DREAMING, + transitionState = TransitionState.RUNNING, + value = 0.6f + ) + ) repository.sendTransitionStep(step(0.6f, TransitionState.RUNNING)) - assertThat(animationValues).isEqualTo(StateToValue(TransitionState.RUNNING, 1.2f)) + assertThat(animationValues) + .isEqualTo( + StateToValue( + from = GONE, + to = DREAMING, + transitionState = TransitionState.RUNNING, + value = 1.2f + ) + ) repository.sendTransitionStep(step(0.8f, TransitionState.RUNNING)) - assertThat(animationValues).isEqualTo(StateToValue(TransitionState.RUNNING, 1.6f)) + assertThat(animationValues) + .isEqualTo( + StateToValue( + from = GONE, + to = DREAMING, + transitionState = TransitionState.RUNNING, + value = 1.6f + ) + ) repository.sendTransitionStep(step(1f, TransitionState.RUNNING)) - assertThat(animationValues).isEqualTo(StateToValue(TransitionState.RUNNING, 2f)) + assertThat(animationValues) + .isEqualTo( + StateToValue( + from = GONE, + to = DREAMING, + transitionState = TransitionState.RUNNING, + value = 2f + ) + ) repository.sendTransitionStep(step(1f, TransitionState.FINISHED)) - assertThat(animationValues).isEqualTo(StateToValue(TransitionState.FINISHED, null)) + assertThat(animationValues) + .isEqualTo( + StateToValue( + from = GONE, + to = DREAMING, + transitionState = TransitionState.FINISHED, + value = null + ) + ) } @Test @@ -251,8 +300,8 @@ class KeyguardTransitionAnimationFlowTest : SysuiTestCase() { state: TransitionState = TransitionState.RUNNING ): TransitionStep { return TransitionStep( - from = KeyguardState.GONE, - to = KeyguardState.DREAMING, + from = GONE, + to = DREAMING, value = value, transitionState = state, ownerName = "GoneToDreamingTransitionViewModelTest" diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelTest.kt index bfa84335d670..716c40d59ccf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelTest.kt @@ -59,7 +59,14 @@ class GoneToAodTransitionViewModelTest : SysuiTestCase() { // The animation should only start > .4f way through repository.sendTransitionStep(step(0f, TransitionState.STARTED)) assertThat(enterFromTopTranslationY) - .isEqualTo(StateToValue(TransitionState.STARTED, pixels)) + .isEqualTo( + StateToValue( + from = KeyguardState.GONE, + to = KeyguardState.AOD, + transitionState = TransitionState.STARTED, + value = pixels + ) + ) repository.sendTransitionStep(step(.55f)) assertThat(enterFromTopTranslationY!!.value ?: -1f).isIn(Range.closed(pixels, 0f)) @@ -70,7 +77,14 @@ class GoneToAodTransitionViewModelTest : SysuiTestCase() { // At the end, the translation should be complete and set to zero repository.sendTransitionStep(step(1f)) assertThat(enterFromTopTranslationY) - .isEqualTo(StateToValue(TransitionState.RUNNING, 0f)) + .isEqualTo( + StateToValue( + from = KeyguardState.GONE, + to = KeyguardState.AOD, + transitionState = TransitionState.RUNNING, + value = 0f + ) + ) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt index b9b872254b06..3e0082c5aed7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt @@ -52,6 +52,7 @@ import com.android.systemui.statusbar.policy.splitShadeStateController import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever +import com.google.common.collect.Range import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow @@ -274,8 +275,7 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { ) ) runCurrent() - // Expected alpha is inverse of progress as notifications are fading away - assertThat(alpha).isEqualTo(1 - progress) + assertThat(alpha).isIn(Range.closed(0f, 1f)) // Finish transition to glanceable hub keyguardTransitionRepository.sendTransitionStep( diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModel.kt new file mode 100644 index 000000000000..b37085957d7e --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModel.kt @@ -0,0 +1,27 @@ +/* + * 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.keyguard.ui.viewmodel + +import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow +import com.android.systemui.kosmos.Kosmos + +val Kosmos.dreamingToGlanceableHubTransitionViewModel by + Kosmos.Fixture { + DreamingToGlanceableHubTransitionViewModel( + animationFlow = keyguardTransitionAnimationFlow, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelKosmos.kt index 28fce77b75b7..b1c21b8fa6cf 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelKosmos.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.ui.viewmodel +import com.android.systemui.common.ui.domain.interactor.configurationInteractor import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture @@ -26,6 +27,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi @OptIn(ExperimentalCoroutinesApi::class) val Kosmos.glanceableHubToLockscreenTransitionViewModel by Fixture { GlanceableHubToLockscreenTransitionViewModel( + configurationInteractor = configurationInteractor, animationFlow = keyguardTransitionAnimationFlow, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModelKosmos.kt index 9fe4ea347f63..471381f7a13f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModelKosmos.kt @@ -18,13 +18,16 @@ package com.android.systemui.keyguard.ui.viewmodel +import com.android.systemui.common.ui.domain.interactor.configurationInteractor import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import kotlinx.coroutines.ExperimentalCoroutinesApi +@ExperimentalCoroutinesApi val Kosmos.lockscreenToGlanceableHubTransitionViewModel by Fixture { LockscreenToGlanceableHubTransitionViewModel( + configurationInteractor = configurationInteractor, animationFlow = keyguardTransitionAnimationFlow, ) } |