diff options
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 |