Swipe to unlock - Fix alpha continuity
Many transitions should start from the last value used in the UI
layer, such as alpha, translationX, translationY. In the case of swipe
to unlock, as the user is swiping up, elements are fading out until a
certain point is reached which then triggers a LOCKSCREEN->GONE
transition. This transition should begin from the current alpha value
rather than some other arbitrary point.
Fixes: 322198222
Test: atest KeyguardRootViewModelTest
SharedNotificationContainerViewModelTest
LockscreenToGoneTransitionViewModelTest
Test: manual - slowly swipe to unlock
Test: manual - fling swipe to unlock
Flag: ACONFIG com.android.systemui.keyguard_shade_migration_nssl
DEVELOPMENT
Flag: ACONFIG com.android.systemui.keyguard_bottom_area_refactor
STAGING
Change-Id: Ida64adfbde26654548f80426cb4050b4aedfa52e
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
index c23ec22..bf1d76f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
@@ -74,6 +74,8 @@
private val dozeParameters = kosmos.dozeParameters
private val underTest by lazy { kosmos.keyguardRootViewModel }
+ private val viewState = ViewStateAccessor()
+
@Before
fun setUp() {
mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
@@ -251,7 +253,7 @@
@Test
fun alpha_idleOnHub_isZero() =
testScope.runTest {
- val alpha by collectLastValue(underTest.alpha)
+ val alpha by collectLastValue(underTest.alpha(viewState))
// Hub transition state is idle with hub open.
communalRepository.setTransitionState(
@@ -269,7 +271,7 @@
@Test
fun alpha_transitionToHub_isZero() =
testScope.runTest {
- val alpha by collectLastValue(underTest.alpha)
+ val alpha by collectLastValue(underTest.alpha(viewState))
keyguardTransitionRepository.sendTransitionSteps(
from = KeyguardState.LOCKSCREEN,
@@ -283,7 +285,7 @@
@Test
fun alpha_transitionFromHubToLockscreen_isOne() =
testScope.runTest {
- val alpha by collectLastValue(underTest.alpha)
+ val alpha by collectLastValue(underTest.alpha(viewState))
// Transition to the glanceable hub and back.
keyguardTransitionRepository.sendTransitionSteps(
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 9e7c70d..1b7a507 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
@@ -48,6 +48,7 @@
import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel
+import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.plugins.clocks.ClockController
@@ -112,6 +113,10 @@
}
val burnInParams = MutableStateFlow(BurnInParameters())
+ val viewState =
+ ViewStateAccessor(
+ alpha = { view.alpha },
+ )
val disposableHandle =
view.repeatWhenAttached {
@@ -134,7 +139,7 @@
if (keyguardBottomAreaRefactor()) {
launch {
- viewModel.alpha.collect { alpha ->
+ viewModel.alpha(viewState).collect { alpha ->
view.alpha = alpha
childViews[statusViewId]?.alpha = alpha
}
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 ec13228..83be651 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
@@ -60,18 +60,21 @@
private val deviceEntryInteractor: DeviceEntryInteractor,
private val dozeParameters: DozeParameters,
private val keyguardInteractor: KeyguardInteractor,
- communalInteractor: CommunalInteractor,
+ private val communalInteractor: CommunalInteractor,
keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val notificationsKeyguardInteractor: NotificationsKeyguardInteractor,
- aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
- lockscreenToGoneTransitionViewModel: LockscreenToGoneTransitionViewModel,
- alternateBouncerToGoneTransitionViewModel: AlternateBouncerToGoneTransitionViewModel,
- primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel,
- lockscreenToGlanceableHubTransitionViewModel: LockscreenToGlanceableHubTransitionViewModel,
- glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel,
- screenOffAnimationController: ScreenOffAnimationController,
+ private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
+ private val lockscreenToGoneTransitionViewModel: LockscreenToGoneTransitionViewModel,
+ private val alternateBouncerToGoneTransitionViewModel:
+ AlternateBouncerToGoneTransitionViewModel,
+ private val primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel,
+ private val lockscreenToGlanceableHubTransitionViewModel:
+ LockscreenToGlanceableHubTransitionViewModel,
+ private val glanceableHubToLockscreenTransitionViewModel:
+ GlanceableHubToLockscreenTransitionViewModel,
+ private val screenOffAnimationController: ScreenOffAnimationController,
private val aodBurnInViewModel: AodBurnInViewModel,
- aodAlphaViewModel: AodAlphaViewModel,
+ private val aodAlphaViewModel: AodAlphaViewModel,
) {
val burnInLayerVisibility: Flow<Int> =
@@ -101,8 +104,8 @@
val topClippingBounds: Flow<Int?> = keyguardInteractor.topClippingBounds
/** An observable for the alpha level for the entire keyguard root view. */
- val alpha: Flow<Float> =
- combine(
+ fun alpha(viewState: ViewStateAccessor): Flow<Float> {
+ return combine(
communalInteractor.isIdleOnCommunal,
// The transitions are mutually exclusive, so they are safe to merge to get the last
// value emitted by any of them. Do not add flows that cannot make this guarantee.
@@ -110,7 +113,7 @@
aodAlphaViewModel.alpha,
lockscreenToGlanceableHubTransitionViewModel.keyguardAlpha,
glanceableHubToLockscreenTransitionViewModel.keyguardAlpha,
- lockscreenToGoneTransitionViewModel.lockscreenAlpha,
+ lockscreenToGoneTransitionViewModel.lockscreenAlpha(viewState),
primaryBouncerToGoneTransitionViewModel.lockscreenAlpha,
alternateBouncerToGoneTransitionViewModel.lockscreenAlpha,
)
@@ -125,6 +128,7 @@
}
}
.distinctUntilChanged()
+ }
/** Specific alpha value for elements visible during [KeyguardState.LOCKSCREEN] */
val lockscreenStateAlpha: Flow<Float> = aodToLockscreenTransitionViewModel.lockscreenAlpha
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt
index d981650..15459f4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt
@@ -16,10 +16,12 @@
package com.android.systemui.keyguard.ui.viewmodel
+import android.util.MathUtils
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.KeyguardTransitionAnimationFlow.FlowBuilder
import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
@@ -37,7 +39,7 @@
animationFlow: KeyguardTransitionAnimationFlow,
) : DeviceEntryIconTransition {
- private val transitionAnimation =
+ private val transitionAnimation: FlowBuilder =
animationFlow.setup(
duration = FromLockscreenTransitionInteractor.TO_GONE_DURATION,
from = KeyguardState.LOCKSCREEN,
@@ -52,7 +54,26 @@
onCancel = { 1f },
)
- val lockscreenAlpha: Flow<Float> = shortcutsAlpha
+ fun lockscreenAlpha(viewState: ViewStateAccessor): Flow<Float> {
+ var startAlpha: Float? = null
+ return transitionAnimation.sharedFlow(
+ duration = 200.milliseconds,
+ onStep = {
+ if (startAlpha == null) {
+ startAlpha = viewState.alpha()
+ }
+ MathUtils.lerp(startAlpha!!, 0f, it)
+ },
+ onFinish = {
+ startAlpha = null
+ 0f
+ },
+ onCancel = {
+ startAlpha = null
+ 1f
+ },
+ )
+ }
override val deviceEntryParentViewAlpha: Flow<Float> =
transitionAnimation.immediatelyTransitionTo(0f)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/ViewStateAccessor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/ViewStateAccessor.kt
new file mode 100644
index 0000000..cb5db86
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/ViewStateAccessor.kt
@@ -0,0 +1,24 @@
+/*
+ * 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
+
+/** View-level state information to be shared between ui and viewmodel. */
+data class ViewStateAccessor(
+ val alpha: () -> Float = { 0f },
+ val translationY: () -> Int = { 0 },
+ val translationX: () -> Int = { 0 },
+)
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 47daf49..830b8c1 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
@@ -1335,6 +1335,10 @@
}
}
+ public float getAlpha() {
+ return mView.getAlpha();
+ }
+
public void setSuppressChildrenMeasureAndLayout(boolean suppressLayout) {
mView.suppressChildrenMeasureAndLayout(suppressLayout);
}
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 97db9b6..daea8af 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
@@ -24,6 +24,7 @@
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
+import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.scene.shared.flag.SceneContainerFlags
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
@@ -75,6 +76,10 @@
}
val burnInParams = MutableStateFlow(BurnInParameters())
+ val viewState =
+ ViewStateAccessor(
+ alpha = { controller.getAlpha() },
+ )
/*
* For animation sensitive coroutines, immediately run just like applicationScope does
@@ -141,7 +146,7 @@
if (!sceneContainerFlags.isEnabled()) {
launch {
- viewModel.expansionAlpha.collect {
+ viewModel.expansionAlpha(viewState).collect {
controller.setMaxAlphaForExpansion(it)
}
}
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 e0c2c3b..ff00cb3 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
@@ -50,6 +50,7 @@
import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine
@@ -67,7 +68,6 @@
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onStart
@@ -100,21 +100,35 @@
setOf(AOD, LOCKSCREEN, DOZING, ALTERNATE_BOUNCER, PRIMARY_BOUNCER)
private val edgeToAlphaViewModel =
- mapOf<Edge?, Flow<Float>>(
+ mapOf<Edge?, (ViewStateAccessor) -> Flow<Float>>(
Edge(from = LOCKSCREEN, to = DREAMING) to
- lockscreenToDreamingTransitionViewModel.lockscreenAlpha,
+ { _: ViewStateAccessor ->
+ lockscreenToDreamingTransitionViewModel.lockscreenAlpha
+ },
Edge(from = LOCKSCREEN, to = GONE) to
- lockscreenToGoneTransitionViewModel.lockscreenAlpha,
+ { viewState: ViewStateAccessor ->
+ lockscreenToGoneTransitionViewModel.lockscreenAlpha(viewState)
+ },
Edge(from = ALTERNATE_BOUNCER, to = GONE) to
- alternateBouncerToGoneTransitionViewModel.lockscreenAlpha,
+ { _: ViewStateAccessor ->
+ alternateBouncerToGoneTransitionViewModel.lockscreenAlpha
+ },
Edge(from = PRIMARY_BOUNCER, to = GONE) to
- primaryBouncerToGoneTransitionViewModel.lockscreenAlpha,
+ { _: ViewStateAccessor ->
+ primaryBouncerToGoneTransitionViewModel.lockscreenAlpha
+ },
Edge(from = DREAMING, to = LOCKSCREEN) to
- dreamingToLockscreenTransitionViewModel.lockscreenAlpha,
+ { _: ViewStateAccessor ->
+ dreamingToLockscreenTransitionViewModel.lockscreenAlpha
+ },
Edge(from = LOCKSCREEN, to = OCCLUDED) to
- lockscreenToOccludedTransitionViewModel.lockscreenAlpha,
+ { _: ViewStateAccessor ->
+ lockscreenToOccludedTransitionViewModel.lockscreenAlpha
+ },
Edge(from = OCCLUDED, to = LOCKSCREEN) to
- occludedToLockscreenTransitionViewModel.lockscreenAlpha,
+ { _: ViewStateAccessor ->
+ occludedToLockscreenTransitionViewModel.lockscreenAlpha
+ },
)
private val lockscreenTransitionInProgress: Flow<Edge?> =
@@ -279,46 +293,63 @@
initialValue = NotificationContainerBounds(),
)
- /** As QS is expanding, fade out notifications unless in splitshade */
- private val alphaForQsExpansion: Flow<Float> =
- interactor.configurationBasedDimensions.flatMapLatest {
- if (it.useSplitShade) {
- flowOf(1f)
- } else {
- shadeInteractor.qsExpansion.map { 1f - it }
+ /**
+ * Ensure view is visible when the shade/qs are expanded. Also, as QS is expanding, fade out
+ * notifications unless in splitshade.
+ */
+ private val alphaForShadeAndQsExpansion: Flow<Float> =
+ interactor.configurationBasedDimensions
+ .flatMapLatest { configurationBasedDimensions ->
+ combine(
+ shadeInteractor.shadeExpansion,
+ shadeInteractor.qsExpansion,
+ ) { shadeExpansion, qsExpansion ->
+ if (shadeExpansion > 0f || qsExpansion > 0f) {
+ if (configurationBasedDimensions.useSplitShade) {
+ 1f
+ } else {
+ // Fade as QS shade expands
+ 1f - qsExpansion
+ }
+ } else {
+ // Not visible unless the shade/qs is visible
+ 0f
+ }
+ }
}
- }
+ .distinctUntilChanged()
- val expansionAlpha: Flow<Float> =
+ fun expansionAlpha(viewState: ViewStateAccessor): Flow<Float> {
// Due to issues with the legacy shade, some shade expansion events are sent incorrectly,
// such as when the shade resets. This can happen while the transition to/from LOCKSCREEN
// is running. Therefore use a series of flatmaps to prevent unwanted interruptions while
// those transitions are in progress. Without this, the alpha value will produce a visible
// flicker.
- lockscreenTransitionInProgress
+ return lockscreenTransitionInProgress
.flatMapLatest { edge ->
- edgeToAlphaViewModel.getOrElse(
+ edgeToAlphaViewModel.getOrDefault(
edge,
- {
+ { _: ViewStateAccessor ->
isOnLockscreenWithoutShade.flatMapLatest { isOnLockscreenWithoutShade ->
combineTransform(
keyguardInteractor.keyguardAlpha,
shadeCollpaseFadeIn,
- alphaForQsExpansion,
- ) { alpha, shadeCollpaseFadeIn, alphaForQsExpansion ->
+ alphaForShadeAndQsExpansion,
+ ) { alpha, shadeCollpaseFadeIn, alphaForShadeAndQsExpansion ->
if (isOnLockscreenWithoutShade) {
if (!shadeCollpaseFadeIn) {
emit(alpha)
}
} else {
- emit(alphaForQsExpansion)
+ emit(alphaForShadeAndQsExpansion)
}
}
}
}
- )
+ )(viewState)
}
.distinctUntilChanged()
+ }
/**
* Returns a flow of the expected alpha while running a LOCKSCREEN<->GLANCEABLE_HUB transition
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.kt
index 8b05a54..e7aaddd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.kt
@@ -19,6 +19,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
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
@@ -56,6 +57,41 @@
deviceEntryParentViewAlpha.forEach { assertThat(it).isEqualTo(0f) }
}
+ @Test
+ fun lockscreenAlphaStartsFromViewStateAccessorAlpha() =
+ testScope.runTest {
+ val viewState = ViewStateAccessor(alpha = { 0.5f })
+ val alpha by collectLastValue(underTest.lockscreenAlpha(viewState))
+
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+
+ repository.sendTransitionStep(step(0f))
+ assertThat(alpha).isEqualTo(0.5f)
+
+ repository.sendTransitionStep(step(0.25f))
+ assertThat(alpha).isEqualTo(0.25f)
+
+ repository.sendTransitionStep(step(.5f))
+ assertThat(alpha).isEqualTo(0f)
+ }
+
+ @Test
+ fun lockscreenAlphaWithNoViewStateAccessorValue() =
+ testScope.runTest {
+ val alpha by collectLastValue(underTest.lockscreenAlpha(ViewStateAccessor()))
+
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+
+ repository.sendTransitionStep(step(0f))
+ assertThat(alpha).isEqualTo(0f)
+
+ repository.sendTransitionStep(step(0.25f))
+ assertThat(alpha).isEqualTo(0f)
+
+ repository.sendTransitionStep(step(0.5f))
+ assertThat(alpha).isEqualTo(0f)
+ }
+
private fun step(
value: Float,
state: TransitionState = TransitionState.RUNNING