diff options
| author | 2025-03-13 10:40:15 -0700 | |
|---|---|---|
| committer | 2025-03-13 17:09:42 -0400 | |
| commit | 1695035a09adeec10a1a08cf816c79d6348488fd (patch) | |
| tree | 4bedeb2cc197d9664829f4d366578ae604d70a39 | |
| parent | 3f7eacaaf860a21ea15b6203abcdc0e60f9bd269 (diff) | |
[Flexiglass] Fix NotificationScrimNestedScrollConnection rememeberSession keys
We were passing in keys to rememberSession that were references to objects using remember, and thus the equality check was breaking once those references changed upon recomposition.
Bug: 403285138
Test: verified through logging that the relevant rememberSession equality checks now pass
Flag: com.android.systemui.scene_container
Change-Id: I846a4357b9359d02288cd707f5c2ebae5fbf0e36
3 files changed, 91 insertions, 15 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimFlingBehavior.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimFlingBehavior.kt new file mode 100644 index 000000000000..bc38ef0abb25 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimFlingBehavior.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.notifications.ui.composable + +import androidx.compose.animation.core.AnimationState +import androidx.compose.animation.core.DecayAnimationSpec +import androidx.compose.animation.core.animateDecay +import androidx.compose.foundation.gestures.FlingBehavior +import androidx.compose.foundation.gestures.ScrollScope +import androidx.compose.ui.MotionDurationScale +import com.android.systemui.scene.session.ui.composable.rememberSession +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.withContext +import kotlin.math.abs + +/** + * Fork of [androidx.compose.foundation.gestures.DefaultFlingBehavior] to allow us to use it with + * [rememberSession]. + */ +internal class NotificationScrimFlingBehavior( + private var flingDecay: DecayAnimationSpec<Float>, + private val motionDurationScale: MotionDurationScale = NotificationScrimMotionDurationScale +) : FlingBehavior { + override suspend fun ScrollScope.performFling(initialVelocity: Float): Float { + // come up with the better threshold, but we need it since spline curve gives us NaNs + return withContext(motionDurationScale) { + if (abs(initialVelocity) > 1f) { + var velocityLeft = initialVelocity + var lastValue = 0f + val animationState = + AnimationState( + initialValue = 0f, + initialVelocity = initialVelocity, + ) + try { + animationState.animateDecay(flingDecay) { + val delta = value - lastValue + val consumed = scrollBy(delta) + lastValue = value + velocityLeft = this.velocity + // avoid rounding errors and stop if anything is unconsumed + if (abs(delta - consumed) > 0.5f) this.cancelAnimation() + } + } catch (exception: CancellationException) { + velocityLeft = animationState.velocity + } + velocityLeft + } else { + initialVelocity + } + } + } +} + +internal val NotificationScrimMotionDurationScale = + object : MotionDurationScale { + override val scaleFactor: Float + get() = 1f + }
\ No newline at end of file diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt index 09b8d178cc8e..800501a920fd 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt @@ -20,12 +20,13 @@ package com.android.systemui.notifications.ui.composable import android.util.Log import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationVector1D +import androidx.compose.animation.core.DecayAnimationSpec import androidx.compose.animation.core.tween +import androidx.compose.animation.splineBasedDecay import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.ScrollableDefaults import androidx.compose.foundation.gestures.animateScrollBy import androidx.compose.foundation.gestures.rememberScrollableState import androidx.compose.foundation.gestures.scrollBy @@ -308,8 +309,6 @@ fun ContentScope.NotificationScrollingStack( ScrollState(initial = 0) } val syntheticScroll = viewModel.syntheticScroll.collectAsStateWithLifecycle(0f) - val isCurrentGestureOverscroll = - viewModel.isCurrentGestureOverscroll.collectAsStateWithLifecycle(false) val expansionFraction by viewModel.expandFraction.collectAsStateWithLifecycle(0f) val shadeToQsFraction by viewModel.shadeToQsFraction.collectAsStateWithLifecycle(0f) @@ -454,15 +453,15 @@ fun ContentScope.NotificationScrollingStack( } } - val flingBehavior = ScrollableDefaults.flingBehavior() val scrimNestedScrollConnection = shadeSession.rememberSession( scrimOffset, - maxScrimTop, minScrimTop, - isCurrentGestureOverscroll, - flingBehavior, + viewModel.isCurrentGestureOverscroll, + density, ) { + val flingSpec: DecayAnimationSpec<Float> = splineBasedDecay(density) + val flingBehavior = NotificationScrimFlingBehavior(flingSpec) NotificationScrimNestedScrollConnection( scrimOffset = { scrimOffset.value }, snapScrimOffset = { value -> coroutineScope.launch { scrimOffset.snapTo(value) } }, @@ -473,7 +472,7 @@ fun ContentScope.NotificationScrollingStack( maxScrimOffset = 0f, contentHeight = { stackHeight.intValue.toFloat() }, minVisibleScrimHeight = minVisibleScrimHeight, - isCurrentGestureOverscroll = { isCurrentGestureOverscroll.value }, + isCurrentGestureOverscroll = { viewModel.isCurrentGestureOverscroll }, flingBehavior = flingBehavior, ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt index 000b3f643e9a..12b48eba7a96 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt @@ -89,6 +89,17 @@ constructor( source = shadeModeInteractor.shadeMode.map { getQuickSettingsShadeContentKey(it) }, ) + /** + * Whether the current touch gesture is overscroll. If true, it means the NSSL has already + * consumed part of the gesture. + */ + val isCurrentGestureOverscroll: Boolean by + hydrator.hydratedStateOf( + traceName = "isCurrentGestureOverscroll", + initialValue = false, + source = interactor.isCurrentGestureOverscroll + ) + /** DEBUG: whether the placeholder should be made slightly visible for positional debugging. */ val isVisualDebuggingEnabled: Boolean = featureFlags.isEnabled(Flags.NSSL_DEBUG_LINES) @@ -157,13 +168,6 @@ constructor( val syntheticScroll: Flow<Float> = interactor.syntheticScroll.dumpWhileCollecting("syntheticScroll") - /** - * Whether the current touch gesture is overscroll. If true, it means the NSSL has already - * consumed part of the gesture. - */ - val isCurrentGestureOverscroll: Flow<Boolean> = - interactor.isCurrentGestureOverscroll.dumpWhileCollecting("isCurrentGestureOverScroll") - /** Whether remote input is currently active for any notification. */ val isRemoteInputActive = remoteInputInteractor.isRemoteInputActive |