From bde7292c88a5b37e9f77348a74a3369baca4e3c1 Mon Sep 17 00:00:00 2001 From: Michel Comin Escude Date: Thu, 23 Apr 2020 11:39:36 -0700 Subject: [PATCH] Updated blur interpolation for notification shade The animation has been updated so 60% of the amount of blur is controlled by a physics-based animation that it's triggered once the user starts pulling the notification shade. The other 40% is controlled by the shade expansion amount. Bug: 152988192 Test: visual Change-Id: I3895bff685be60714f19ec2a29d125050c9c2da7 --- .../android/systemui/statusbar/BlurUtils.kt | 4 +- .../NotificationShadeDepthController.kt | 149 +++++++++++++++++- .../NotificationShadeDepthControllerTest.kt | 55 +++++++ 3 files changed, 198 insertions(+), 10 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt b/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt index 866980458cada..c70d3847bd6f6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt @@ -38,8 +38,8 @@ open class BlurUtils @Inject constructor( @Main private val resources: Resources, dumpManager: DumpManager ) : Dumpable { - private val minBlurRadius = resources.getDimensionPixelSize(R.dimen.min_window_blur_radius) - private val maxBlurRadius = resources.getDimensionPixelSize(R.dimen.max_window_blur_radius) + val minBlurRadius = resources.getDimensionPixelSize(R.dimen.min_window_blur_radius) + val maxBlurRadius = resources.getDimensionPixelSize(R.dimen.max_window_blur_radius) private val blurSupportedSysProp = SystemProperties .getBoolean("ro.surface_flinger.supports_background_blur", false) private val blurDisabledSysProp = SystemProperties diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt index 25f1a974bc36b..7aae724ae80a3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt @@ -20,7 +20,9 @@ import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.ValueAnimator import android.app.WallpaperManager +import android.os.SystemClock import android.util.Log +import android.util.MathUtils import android.view.Choreographer import android.view.View import androidx.annotation.VisibleForTesting @@ -44,6 +46,7 @@ import java.io.PrintWriter import javax.inject.Inject import javax.inject.Singleton import kotlin.math.max +import kotlin.math.sign /** * Controller responsible for statusbar window blur. @@ -61,6 +64,11 @@ class NotificationShadeDepthController @Inject constructor( ) : PanelExpansionListener, Dumpable { companion object { private const val WAKE_UP_ANIMATION_ENABLED = true + private const val VELOCITY_SCALE = 100f + private const val MAX_VELOCITY = 3000f + private const val MIN_VELOCITY = -MAX_VELOCITY + private const val INTERACTION_BLUR_FRACTION = 0.4f + private const val ANIMATION_BLUR_FRACTION = 1f - INTERACTION_BLUR_FRACTION private const val TAG = "DepthController" } @@ -71,8 +79,19 @@ class NotificationShadeDepthController @Inject constructor( private var updateScheduled: Boolean = false private var shadeExpansion = 0f private var ignoreShadeBlurUntilHidden: Boolean = false + private var isClosed: Boolean = true + private var isOpen: Boolean = false + private var isBlurred: Boolean = false + + private var prevTracking: Boolean = false + private var prevTimestamp: Long = -1 + private var prevShadeDirection = 0 + private var prevShadeVelocity = 0f + @VisibleForTesting var shadeSpring = DepthAnimation() + var shadeAnimation = DepthAnimation() + @VisibleForTesting var globalActionsSpring = DepthAnimation() var showingHomeControls: Boolean = false @@ -98,12 +117,15 @@ class NotificationShadeDepthController @Inject constructor( return } - if (shadeSpring.radius == 0) { + if (shadeSpring.radius == 0 && shadeAnimation.radius == 0) { return } ignoreShadeBlurUntilHidden = true shadeSpring.animateTo(0) shadeSpring.finishIfRunning() + + shadeAnimation.animateTo(0) + shadeAnimation.finishIfRunning() } /** @@ -132,8 +154,11 @@ class NotificationShadeDepthController @Inject constructor( @VisibleForTesting val updateBlurCallback = Choreographer.FrameCallback { updateScheduled = false - - var shadeRadius = max(shadeSpring.radius, wakeAndUnlockBlurRadius).toFloat() + val normalizedBlurRadius = MathUtils.constrain(shadeAnimation.radius, + blurUtils.minBlurRadius, blurUtils.maxBlurRadius) + val combinedBlur = (shadeSpring.radius * INTERACTION_BLUR_FRACTION + + normalizedBlurRadius * ANIMATION_BLUR_FRACTION).toInt() + var shadeRadius = max(combinedBlur, wakeAndUnlockBlurRadius).toFloat() shadeRadius *= 1f - brightnessMirrorSpring.ratio val launchProgress = notificationLaunchAnimationParams?.linearProgress ?: 0f shadeRadius *= (1f - launchProgress) * (1f - launchProgress) @@ -208,12 +233,15 @@ class NotificationShadeDepthController @Inject constructor( private val statusBarStateCallback = object : StatusBarStateController.StateListener { override fun onStateChanged(newState: Int) { + updateShadeAnimationBlur( + shadeExpansion, prevTracking, prevShadeVelocity, prevShadeDirection) updateShadeBlur() } override fun onDozingChanged(isDozing: Boolean) { if (isDozing) { shadeSpring.finishIfRunning() + shadeAnimation.finishIfRunning() globalActionsSpring.finishIfRunning() brightnessMirrorSpring.finishIfRunning() } @@ -234,24 +262,110 @@ class NotificationShadeDepthController @Inject constructor( // Stop blur effect when scrims is opaque to avoid unnecessary GPU composition. visibility -> scrimsVisible = visibility == ScrimController.OPAQUE } + shadeAnimation.setStiffness(SpringForce.STIFFNESS_LOW) + shadeAnimation.setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY) } /** * Update blurs when pulling down the shade */ override fun onPanelExpansionChanged(expansion: Float, tracking: Boolean) { - if (expansion == shadeExpansion) { + val timestamp = SystemClock.elapsedRealtimeNanos() + + if (shadeExpansion == expansion && prevTracking == tracking) { + prevTimestamp = timestamp return } + + var deltaTime = 1f + if (prevTimestamp < 0) { + prevTimestamp = timestamp + } else { + deltaTime = MathUtils.constrain( + ((timestamp - prevTimestamp) / 1E9).toFloat(), 0.00001f, 1f) + } + + val diff = expansion - shadeExpansion + val shadeDirection = sign(diff).toInt() + val shadeVelocity = MathUtils.constrain( + VELOCITY_SCALE * diff / deltaTime, MIN_VELOCITY, MAX_VELOCITY) + updateShadeAnimationBlur(expansion, tracking, shadeVelocity, shadeDirection) + + prevShadeDirection = shadeDirection + prevShadeVelocity = shadeVelocity shadeExpansion = expansion + prevTracking = tracking + prevTimestamp = timestamp + updateShadeBlur() } + private fun updateShadeAnimationBlur( + expansion: Float, + tracking: Boolean, + velocity: Float, + direction: Int + ) { + if (isOnKeyguardNotDismissing()) { + if (expansion > 0f) { + // Blur view if user starts animating in the shade. + if (isClosed) { + animateBlur(true, velocity) + isClosed = false + } + + // If we were blurring out and the user stopped the animation, blur view. + if (tracking && !isBlurred) { + animateBlur(true, 0f) + } + + // If shade is being closed and the user isn't interacting with it, un-blur. + if (!tracking && direction < 0 && isBlurred) { + animateBlur(false, velocity) + } + + if (expansion == 1f) { + if (!isOpen) { + isOpen = true + // If shade is open and view is not blurred, blur. + if (!isBlurred) { + animateBlur(true, velocity) + } + } + } else { + isOpen = false + } + // Automatic animation when the user closes the shade. + } else if (!isClosed) { + isClosed = true + // If shade is closed and view is not blurred, blur. + if (isBlurred) { + animateBlur(false, velocity) + } + } + } else { + animateBlur(false, 0f) + isClosed = true + isOpen = false + } + } + + private fun animateBlur(blur: Boolean, velocity: Float) { + isBlurred = blur + + val targetBlurNormalized = if (blur && isOnKeyguardNotDismissing()) { + 1f + } else { + 0f + } + + shadeAnimation.setStartVelocity(velocity) + shadeAnimation.animateTo(blurUtils.blurRadiusOfRatio(targetBlurNormalized)) + } + private fun updateShadeBlur() { var newBlur = 0 - val state = statusBarStateController.state - if ((state == StatusBarState.SHADE || state == StatusBarState.SHADE_LOCKED) && - !keyguardStateController.isKeyguardFadingAway) { + if (isOnKeyguardNotDismissing()) { newBlur = blurUtils.blurRadiusOfRatio(shadeExpansion) } shadeSpring.animateTo(newBlur) @@ -266,6 +380,12 @@ class NotificationShadeDepthController @Inject constructor( choreographer.postFrameCallback(updateBlurCallback) } + private fun isOnKeyguardNotDismissing(): Boolean { + val state = statusBarStateController.state + return (state == StatusBarState.SHADE || state == StatusBarState.SHADE_LOCKED) && + !keyguardStateController.isKeyguardFadingAway + } + fun updateGlobalDialogVisibility(visibility: Float, dialogView: View?) { globalActionsSpring.animateTo(blurUtils.blurRadiusOfRatio(visibility), dialogView) } @@ -275,6 +395,7 @@ class NotificationShadeDepthController @Inject constructor( it.println("StatusBarWindowBlurController:") it.increaseIndent() it.println("shadeRadius: ${shadeSpring.radius}") + it.println("shadeAnimation: ${shadeAnimation.radius}") it.println("globalActionsRadius: ${globalActionsSpring.radius}") it.println("brightnessMirrorRadius: ${brightnessMirrorSpring.radius}") it.println("wakeAndUnlockBlur: $wakeAndUnlockBlurRadius") @@ -311,7 +432,7 @@ class NotificationShadeDepthController @Inject constructor( private var view: View? = null private var springAnimation = SpringAnimation(this, object : - FloatPropertyCompat("blurRadius") { + FloatPropertyCompat("blurRadius") { override fun setValue(rect: DepthAnimation?, value: Float) { radius = value.toInt() scheduleUpdate(view) @@ -343,5 +464,17 @@ class NotificationShadeDepthController @Inject constructor( springAnimation.skipToEnd() } } + + fun setStiffness(stiffness: Float) { + springAnimation.spring.stiffness = stiffness + } + + fun setDampingRatio(dampingRation: Float) { + springAnimation.spring.dampingRatio = dampingRation + } + + fun setStartVelocity(velocity: Float) { + springAnimation.setStartVelocity(velocity) + } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt index c874915e91240..c46ac47e660c0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt @@ -64,6 +64,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { @Mock private lateinit var root: View @Mock private lateinit var viewRootImpl: ViewRootImpl @Mock private lateinit var shadeSpring: NotificationShadeDepthController.DepthAnimation + @Mock private lateinit var shadeAnimation: NotificationShadeDepthController.DepthAnimation @Mock private lateinit var globalActionsSpring: NotificationShadeDepthController.DepthAnimation @Mock private lateinit var brightnessSpring: NotificationShadeDepthController.DepthAnimation @JvmField @Rule val mockitoRule = MockitoJUnit.rule() @@ -80,11 +81,15 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { `when`(blurUtils.blurRadiusOfRatio(anyFloat())).then { answer -> (answer.arguments[0] as Float * maxBlur).toInt() } + `when`(blurUtils.minBlurRadius).thenReturn(0) + `when`(blurUtils.maxBlurRadius).thenReturn(maxBlur) + notificationShadeDepthController = NotificationShadeDepthController( statusBarStateController, blurUtils, biometricUnlockController, keyguardStateController, choreographer, wallpaperManager, notificationShadeWindowController, dumpManager) notificationShadeDepthController.shadeSpring = shadeSpring + notificationShadeDepthController.shadeAnimation = shadeAnimation notificationShadeDepthController.brightnessMirrorSpring = brightnessSpring notificationShadeDepthController.globalActionsSpring = globalActionsSpring notificationShadeDepthController.root = root @@ -104,16 +109,61 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { notificationShadeDepthController.onPanelExpansionChanged(1f /* expansion */, false /* tracking */) verify(shadeSpring).animateTo(eq(maxBlur), any()) + verify(shadeAnimation).animateTo(eq(maxBlur), any()) + } + + @Test + fun onPanelExpansionChanged_animatesBlurIn_ifShade() { + notificationShadeDepthController.onPanelExpansionChanged(0.01f /* expansion */, + false /* tracking */) + verify(shadeAnimation).animateTo(eq(maxBlur), any()) + } + + @Test + fun onPanelExpansionChanged_animatesBlurOut_ifShade() { + onPanelExpansionChanged_animatesBlurIn_ifShade() + clearInvocations(shadeAnimation) + notificationShadeDepthController.onPanelExpansionChanged(0f /* expansion */, + false /* tracking */) + verify(shadeAnimation).animateTo(eq(0), any()) + } + + @Test + fun onPanelExpansionChanged_animatesBlurOut_ifFlick() { + onPanelExpansionChanged_apliesBlur_ifShade() + clearInvocations(shadeAnimation) + notificationShadeDepthController.onPanelExpansionChanged(1f /* expansion */, + true /* tracking */) + verify(shadeAnimation, never()).animateTo(anyInt(), any()) + + notificationShadeDepthController.onPanelExpansionChanged(0.9f /* expansion */, + true /* tracking */) + verify(shadeAnimation, never()).animateTo(anyInt(), any()) + + notificationShadeDepthController.onPanelExpansionChanged(0.8f /* expansion */, + false /* tracking */) + verify(shadeAnimation).animateTo(eq(0), any()) + } + + @Test + fun onPanelExpansionChanged_animatesBlurIn_ifFlickCancelled() { + onPanelExpansionChanged_animatesBlurOut_ifFlick() + clearInvocations(shadeAnimation) + notificationShadeDepthController.onPanelExpansionChanged(0.6f /* expansion */, + true /* tracking */) + verify(shadeAnimation).animateTo(eq(maxBlur), any()) } @Test fun onStateChanged_reevalutesBlurs_ifSameRadiusAndNewState() { onPanelExpansionChanged_apliesBlur_ifShade() clearInvocations(shadeSpring) + clearInvocations(shadeAnimation) statusBarState = StatusBarState.KEYGUARD statusBarStateListener.onStateChanged(statusBarState) verify(shadeSpring).animateTo(eq(0), any()) + verify(shadeAnimation).animateTo(eq(0), any()) } @Test @@ -147,6 +197,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { @Test fun updateBlurCallback_setsBlur_whenExpanded() { `when`(shadeSpring.radius).thenReturn(maxBlur) + `when`(shadeAnimation.radius).thenReturn(maxBlur) notificationShadeDepthController.updateBlurCallback.doFrame(0) verify(blurUtils).applyBlur(any(), eq(maxBlur)) } @@ -154,6 +205,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { @Test fun updateBlurCallback_appLaunchAnimation_overridesZoom() { `when`(shadeSpring.radius).thenReturn(maxBlur) + `when`(shadeAnimation.radius).thenReturn(maxBlur) val animProgress = ActivityLaunchAnimator.ExpandAnimationParameters() animProgress.linearProgress = 1f notificationShadeDepthController.notificationLaunchAnimationParams = animProgress @@ -187,6 +239,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { `when`(brightnessSpring.ratio).thenReturn(1f) // And shade is blurred `when`(shadeSpring.radius).thenReturn(maxBlur) + `when`(shadeAnimation.radius).thenReturn(maxBlur) notificationShadeDepthController.updateBlurCallback.doFrame(0) verify(notificationShadeWindowController).setBackgroundBlurRadius(0) @@ -207,8 +260,10 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { val animProgress = ActivityLaunchAnimator.ExpandAnimationParameters() animProgress.linearProgress = 0.5f `when`(shadeSpring.radius).thenReturn(0) + `when`(shadeAnimation.radius).thenReturn(0) notificationShadeDepthController.notificationLaunchAnimationParams = animProgress verify(shadeSpring, never()).animateTo(anyInt(), any()) + verify(shadeAnimation, never()).animateTo(anyInt(), any()) } private fun safeEq(value: T): T {