From b6d298a4d57a62cc5d4f79b715be61ce2e4b74f6 Mon Sep 17 00:00:00 2001 From: Evan Laird Date: Mon, 7 Mar 2022 15:49:38 -0500 Subject: [PATCH 1/2] Don't transition to ongoing privacy dot if still animating If privacy events come in very fast (during the debounce time), we sometimes would transition to showing the ongoing privacy event dot before the animation finished. Test: manual Fixes: 222554701 Change-Id: I270e0180655eb2a6254a17ce6f18f0fd4a77b081 --- .../statusbar/events/SystemStatusAnimationScheduler.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt index 947f3eb2ee123..9e3782f8bfe2f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt @@ -116,7 +116,10 @@ class SystemStatusAnimationScheduler @Inject constructor( scheduledEvent?.updateFromEvent(event) if (event.forceVisible) { hasPersistentDot = true - notifyTransitionToPersistentDot() + // If we missed the chance to show the persistent dot, do it now + if (animationState == IDLE) { + notifyTransitionToPersistentDot() + } } } else { if (DEBUG) { From c1b85a8b3d3634a47e2740ba6116a85158c3b3af Mon Sep 17 00:00:00 2001 From: Evan Laird Date: Wed, 9 Mar 2022 16:34:06 -0500 Subject: [PATCH 2/2] Rework system event animations to a federated model Before this cl, all system event chip animations were structured to run via callbacks from a single ValueAnimator which ran from [0,1], and clients could implement either the system chrome callbacks or the chip callbacks. This change reworks the whole system to instead use the animation scheduler to collect animators from any relevant clients and play them all together. It allows for much more granular control over individual elements that want to participate in the animation. The tradeoff here is that there is no single place for the animation to be coordinated from. The entire experince needs to be composed by orchestrating every animation individually and testing manually to ensure that the motion is adequate. Test: manual Bug: 187881849 Change-Id: I8b2ec28b0489f6ec5870e4e14e5e9fd1bd15e076 --- .../res/layout/ongoing_privacy_chip.xml | 2 - packages/SystemUI/res/values/dimens.xml | 3 + .../SystemEventChipAnimationController.kt | 282 ++++++++++++------ .../events/SystemStatusAnimationScheduler.kt | 248 +++++++-------- .../phone/KeyguardStatusBarView.java | 24 +- .../KeyguardStatusBarViewController.java | 24 +- .../fragment/CollapsedStatusBarFragment.java | 42 +-- .../fragment/StatusBarSystemEventAnimator.kt | 84 ++++++ 8 files changed, 421 insertions(+), 288 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarSystemEventAnimator.kt diff --git a/packages/SystemUI/res/layout/ongoing_privacy_chip.xml b/packages/SystemUI/res/layout/ongoing_privacy_chip.xml index ad2bc79a5c969..5aa608084510c 100644 --- a/packages/SystemUI/res/layout/ongoing_privacy_chip.xml +++ b/packages/SystemUI/res/layout/ongoing_privacy_chip.xml @@ -37,7 +37,5 @@ android:layout_gravity="center" android:minWidth="@dimen/ongoing_appops_chip_min_width" android:maxWidth="@dimen/ongoing_appops_chip_max_width" - android:clipChildren="false" - android:clipToPadding="false" /> \ No newline at end of file diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 52ec516291167..3b79e78fd8875 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -941,6 +941,9 @@ 76dp 6dp + 10dp + 15dp + 7dp 20dp diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt index 9795dcf1fcd84..b74140da99d2e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt @@ -16,9 +16,12 @@ package com.android.systemui.statusbar.events +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.AnimatorSet import android.animation.ValueAnimator import android.content.Context -import android.graphics.Point +import android.graphics.Rect import android.view.Gravity import android.view.LayoutInflater import android.view.View @@ -30,6 +33,7 @@ import com.android.systemui.R import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider import com.android.systemui.statusbar.window.StatusBarWindowController import javax.inject.Inject +import kotlin.math.roundToInt /** * Controls the view for system event animations. @@ -38,7 +42,7 @@ class SystemEventChipAnimationController @Inject constructor( private val context: Context, private val statusBarWindowController: StatusBarWindowController, private val contentInsetsProvider: StatusBarContentInsetsProvider -) : SystemStatusChipAnimationCallback { +) : SystemStatusAnimationCallback { private lateinit var animationWindowView: FrameLayout @@ -49,90 +53,169 @@ class SystemEventChipAnimationController @Inject constructor( private var chipRight = 0 private var chipLeft = 0 private var chipWidth = 0 - private var dotCenter = Point(0, 0) + private var chipMinWidth = context.resources.getDimensionPixelSize( + R.dimen.ongoing_appops_chip_min_animation_width) private var dotSize = context.resources.getDimensionPixelSize( R.dimen.ongoing_appops_dot_diameter) - // If the chip animates away to a persistent dot, then we modify the CHIP_OUT animation - private var isAnimatingToDot = false + // Use during animation so that multiple animators can update the drawing rect + private var animRect = Rect() // TODO: move to dagger private var initialized = false - override fun onChipAnimationStart( - viewCreator: ViewCreator, - @SystemAnimationState state: Int - ) { - if (!initialized) init() + /** + * Give the chip controller a chance to inflate and configure the chip view before we start + * animating + */ + fun prepareChipAnimation(viewCreator: ViewCreator) { + if (!initialized) { + init() + } + animationDirection = if (animationWindowView.isLayoutRtl) RIGHT else LEFT - if (state == ANIMATING_IN) { - animationDirection = if (animationWindowView.isLayoutRtl) RIGHT else LEFT + // Initialize the animated view + val insets = contentInsetsProvider.getStatusBarContentInsetsForCurrentRotation() + currentAnimatedView = viewCreator(context).also { + animationWindowView.addView( + it.view, + layoutParamsDefault( + if (animationWindowView.isLayoutRtl) insets.first + else insets.second)) + it.view.alpha = 0f + // For some reason, the window view's measured width is always 0 here, so use the + // parent (status bar) + it.view.measure( + View.MeasureSpec.makeMeasureSpec( + (animationWindowView.parent as View).width, AT_MOST), + View.MeasureSpec.makeMeasureSpec(animationWindowView.height, AT_MOST)) + chipWidth = it.chipWidth + } - // Initialize the animated view - val insets = contentInsetsProvider.getStatusBarContentInsetsForCurrentRotation() - currentAnimatedView = viewCreator(context).also { - animationWindowView.addView( - it.view, - layoutParamsDefault( - if (animationWindowView.isLayoutRtl) insets.first - else insets.second)) - it.view.alpha = 0f - // For some reason, the window view's measured width is always 0 here, so use the - // parent (status bar) - it.view.measure( - View.MeasureSpec.makeMeasureSpec( - (animationWindowView.parent as View).width, AT_MOST), - View.MeasureSpec.makeMeasureSpec(animationWindowView.height, AT_MOST)) - chipWidth = it.chipWidth + // decide which direction we're animating from, and then set some screen coordinates + val contentRect = contentInsetsProvider.getStatusBarContentAreaForCurrentRotation() + when (animationDirection) { + LEFT -> { + chipRight = contentRect.right + chipLeft = contentRect.right - chipWidth } - - // decide which direction we're animating from, and then set some screen coordinates - val contentRect = contentInsetsProvider.getStatusBarContentAreaForCurrentRotation() - when (animationDirection) { - LEFT -> { - chipRight = contentRect.right - chipLeft = contentRect.right - chipWidth - } - else /* RIGHT */ -> { - chipLeft = contentRect.left - chipRight = contentRect.left + chipWidth - } - } - - currentAnimatedView?.apply { - updateAnimatedViewBoundsForAmount(0.1f, this) - } - } else { - // We are animating away - currentAnimatedView!!.view.apply { - alpha = 1f + else /* RIGHT */ -> { + chipLeft = contentRect.left + chipRight = contentRect.left + chipWidth } } } - override fun onChipAnimationEnd(@SystemAnimationState state: Int) { - if (state == ANIMATING_IN) { - // Finished animating in - currentAnimatedView?.apply { - updateAnimatedViewBoundsForAmount(1f, this) - } - } else { - // Finished animating away - currentAnimatedView!!.view.apply { - visibility = View.INVISIBLE - } - animationWindowView.removeView(currentAnimatedView!!.view) + override fun onSystemEventAnimationBegin(): Animator { + initializeAnimRect() + + val alphaIn = ValueAnimator.ofFloat(0f, 1f).apply { + startDelay = 117 + duration = 83 + interpolator = null + addUpdateListener { currentAnimatedView?.view?.alpha = animatedValue as Float } } + val moveIn = ValueAnimator.ofInt(chipMinWidth, chipWidth).apply { + startDelay = 117 + duration = 383 + interpolator = STATUS_BAR_X_MOVE_IN + addUpdateListener { + updateAnimatedViewBoundsWidth(animatedValue as Int) + } + } + val animSet = AnimatorSet() + animSet.playTogether(alphaIn, moveIn) + return animSet } - override fun onChipAnimationUpdate( - animator: ValueAnimator, - @SystemAnimationState state: Int - ) { - currentAnimatedView?.apply { - val amt = (animator.animatedValue as Float).amt() - view.alpha = (animator.animatedValue as Float) - updateAnimatedViewBoundsForAmount(amt, this) + override fun onSystemEventAnimationFinish(hasPersistentDot: Boolean): Animator { + initializeAnimRect() + val finish = if (hasPersistentDot) { + createMoveOutAnimationForDot() + } else { + createMoveOutAnimationDefault() } + + finish.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + animationWindowView.removeView(currentAnimatedView!!.view) + } + }) + + return finish + } + + private fun createMoveOutAnimationForDot(): Animator { + val width1 = ValueAnimator.ofInt(chipWidth, chipMinWidth).apply { + duration = 150 + interpolator = STATUS_CHIP_WIDTH_TO_DOT_KEYFRAME_1 + addUpdateListener { + updateAnimatedViewBoundsWidth(it.animatedValue as Int) + } + } + + val width2 = ValueAnimator.ofInt(chipMinWidth, dotSize).apply { + startDelay = 150 + duration = 333 + interpolator = STATUS_CHIP_WIDTH_TO_DOT_KEYFRAME_2 + addUpdateListener { + updateAnimatedViewBoundsWidth(it.animatedValue as Int) + } + } + + val keyFrame1Height = dotSize * 2 + val v = currentAnimatedView!!.view + val chipVerticalCenter = v.top + v.measuredHeight / 2 + val height1 = ValueAnimator.ofInt( + currentAnimatedView!!.view.measuredHeight, keyFrame1Height).apply { + startDelay = 133 + duration = 100 + interpolator = STATUS_CHIP_HEIGHT_TO_DOT_KEYFRAME_1 + addUpdateListener { + updateAnimatedViewBoundsHeight(it.animatedValue as Int, chipVerticalCenter) + } + } + + val height2 = ValueAnimator.ofInt(keyFrame1Height, dotSize).apply { + startDelay = 233 + duration = 250 + interpolator = STATUS_CHIP_HEIGHT_TO_DOT_KEYFRAME_2 + addUpdateListener { + updateAnimatedViewBoundsHeight(it.animatedValue as Int, chipVerticalCenter) + } + } + + // Move the chip view to overlap exactly with the privacy dot. The chip displays by default + // exactly adjacent to the dot, so we can just move over by the diameter of the dot itself + val moveOut = ValueAnimator.ofInt(0, dotSize).apply { + startDelay = 50 + duration = 183 + interpolator = STATUS_CHIP_MOVE_TO_DOT + addUpdateListener { + // If RTL, we can just invert the move + val amt = if (animationDirection == LEFT) { + animatedValue as Int + } else { + -(animatedValue as Int) + } + updateAnimatedBoundsX(amt) + } + } + + val animSet = AnimatorSet() + animSet.playTogether(width1, width2, height1, height2, moveOut) + return animSet + } + + private fun createMoveOutAnimationDefault(): Animator { + val moveOut = ValueAnimator.ofInt(chipWidth, chipMinWidth).apply { + duration = 383 + addUpdateListener { + currentAnimatedView?.apply { + updateAnimatedViewBoundsWidth(it.animatedValue as Int) + } + } + } + return moveOut } private fun init() { @@ -144,7 +227,6 @@ class SystemEventChipAnimationController @Inject constructor( statusBarWindowController.addViewToWindow(animationWindowView, lp) animationWindowView.clipToPadding = false animationWindowView.clipChildren = false - animationWindowView.measureAllChildren = true } private fun layoutParamsDefault(marginEnd: Int): FrameLayout.LayoutParams = @@ -153,29 +235,55 @@ class SystemEventChipAnimationController @Inject constructor( it.marginEnd = marginEnd } - private fun updateAnimatedViewBoundsForAmount(amt: Float, chip: BackgroundAnimatableView) { + private fun initializeAnimRect() = animRect.set( + chipLeft, + currentAnimatedView!!.view.top, + chipRight, + currentAnimatedView!!.view.bottom) + + /** + * To be called during an animation, sets the width and updates the current animated chip view + */ + private fun updateAnimatedViewBoundsWidth(width: Int) { when (animationDirection) { LEFT -> { - chip.setBoundsForAnimation( - (chipRight - (chipWidth * amt)).toInt(), - chip.view.top, - chipRight, - chip.view.bottom) - } - else /* RIGHT */ -> { - chip.setBoundsForAnimation( - chipLeft, - chip.view.top, - (chipLeft + (chipWidth * amt)).toInt(), - chip.view.bottom) + animRect.set((chipRight - width), animRect.top, chipRight, animRect.bottom) + } else /* RIGHT */ -> { + animRect.set(chipLeft, animRect.top, (chipLeft + width), animRect.bottom) } } + + updateCurrentAnimatedView() } - private fun start() = if (animationWindowView.isLayoutRtl) right() else left() - private fun right() = contentInsetsProvider.getStatusBarContentAreaForCurrentRotation().right - private fun left() = contentInsetsProvider.getStatusBarContentAreaForCurrentRotation().left - private fun Float.amt() = 0.01f.coerceAtLeast(this) + /** + * To be called during an animation, updates the animation rect and sends the update to the chip + */ + private fun updateAnimatedViewBoundsHeight(height: Int, verticalCenter: Int) { + animRect.set( + animRect.left, + verticalCenter - (height.toFloat() / 2).roundToInt(), + animRect.right, + verticalCenter + (height.toFloat() / 2).roundToInt()) + + updateCurrentAnimatedView() + } + + /** + * To be called during an animation, updates the animation rect offset and updates the chip + */ + private fun updateAnimatedBoundsX(translation: Int) { + currentAnimatedView?.view?.translationX = translation.toFloat() + } + + /** + * To be called during an animation. Sets the chip rect to animRect + */ + private fun updateCurrentAnimatedView() { + currentAnimatedView?.setBoundsForAnimation( + animRect.left, animRect.top, animRect.right, animRect.bottom + ) + } } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt index 9e3782f8bfe2f..36233e4e3eab0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt @@ -19,14 +19,12 @@ package com.android.systemui.statusbar.events import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.AnimatorSet -import android.animation.ValueAnimator import android.annotation.IntDef import android.os.Process import android.provider.DeviceConfig import android.util.Log +import android.view.animation.PathInterpolator import com.android.systemui.Dumpable -import com.android.systemui.animation.Interpolators.STANDARD_ACCELERATE -import com.android.systemui.animation.Interpolators.STANDARD_DECELERATE import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main @@ -38,6 +36,7 @@ import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.time.SystemClock import java.io.FileDescriptor import java.io.PrintWriter +import java.lang.IllegalStateException import javax.inject.Inject @@ -165,68 +164,90 @@ class SystemStatusAnimationScheduler @Inject constructor( return } - // Schedule the animation to start after a debounce period - cancelExecutionRunnable = executor.executeDelayed({ - cancelExecutionRunnable = null - animationState = ANIMATING_IN - statusBarWindowController.setForceStatusBarVisible(true) + chipAnimationController.prepareChipAnimation(scheduledEvent!!.viewCreator) + animationState = ANIMATION_QUEUED + executor.executeDelayed({ + runChipAnimation() + }, DEBOUNCE_DELAY) + } - val entranceAnimator = ValueAnimator.ofFloat(1f, 0f) - entranceAnimator.duration = ENTRANCE_ANIM_LENGTH - entranceAnimator.addListener(systemAnimatorAdapter) - entranceAnimator.addUpdateListener(systemUpdateListener) - entranceAnimator.interpolator = STANDARD_ACCELERATE + /** + * 1. Define a total budget for the chip animation (1500ms) + * 2. Send out callbacks to listeners so that they can generate animations locally + * 3. Update the scheduler state so that clients know where we are + * 4. Maybe: provide scaffolding such as: dot location, margins, etc + * 5. Maybe: define a maximum animation length and enforce it. Probably only doable if we + * collect all of the animators and run them together. + */ + private fun runChipAnimation() { + statusBarWindowController.setForceStatusBarVisible(true) + animationState = ANIMATING_IN - val chipAnimator = ValueAnimator.ofFloat(0f, 1f) - chipAnimator.duration = CHIP_ANIM_LENGTH - chipAnimator.addListener( - ChipAnimatorAdapter(RUNNING_CHIP_ANIM, scheduledEvent!!.viewCreator)) - chipAnimator.addUpdateListener(chipUpdateListener) - chipAnimator.interpolator = STANDARD_DECELERATE + val animSet = collectStartAnimations() + if (animSet.totalDuration > 500) { + throw IllegalStateException("System animation total length exceeds budget. " + + "Expected: 500, actual: ${animSet.totalDuration}") + } + animSet.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + animationState = RUNNING_CHIP_ANIM + } + }) + animSet.start() - val aSet2 = AnimatorSet() - aSet2.playSequentially(entranceAnimator, chipAnimator) - aSet2.start() - - executor.executeDelayed({ - animationState = ANIMATING_OUT - - val systemAnimator = ValueAnimator.ofFloat(0f, 1f) - systemAnimator.duration = ENTRANCE_ANIM_LENGTH - systemAnimator.addListener(systemAnimatorAdapter) - systemAnimator.addUpdateListener(systemUpdateListener) - systemAnimator.interpolator = STANDARD_DECELERATE - systemAnimator.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { - statusBarWindowController.setForceStatusBarVisible(false) + executor.executeDelayed({ + val animSet2 = collectFinishAnimations() + animationState = ANIMATING_OUT + animSet2.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + animationState = if (hasPersistentDot) { + SHOWING_PERSISTENT_DOT + } else { + IDLE } - }) - val chipAnimator = ValueAnimator.ofFloat(1f, 0f) - chipAnimator.duration = CHIP_ANIM_LENGTH - val endState = if (hasPersistentDot) { - SHOWING_PERSISTENT_DOT - } else { - IDLE + statusBarWindowController.setForceStatusBarVisible(false) } - chipAnimator.addListener( - ChipAnimatorAdapter(endState, scheduledEvent!!.viewCreator)) - chipAnimator.addUpdateListener(chipUpdateListener) - chipAnimator.interpolator = STANDARD_ACCELERATE + }) + animSet2.start() + scheduledEvent = null + }, DISPLAY_LENGTH) + } - val aSet2 = AnimatorSet() + private fun collectStartAnimations(): AnimatorSet { + val animators = mutableListOf() + listeners.forEach { listener -> + listener.onSystemEventAnimationBegin()?.let { anim -> + animators.add(anim) + } + } + animators.add(chipAnimationController.onSystemEventAnimationBegin()) + val animSet = AnimatorSet().also { + it.playTogether(animators) + } - aSet2.play(chipAnimator).before(systemAnimator) - if (hasPersistentDot) { - val dotAnim = notifyTransitionToPersistentDot() - if (dotAnim != null) aSet2.playTogether(systemAnimator, dotAnim) - } + return animSet + } - aSet2.start() + private fun collectFinishAnimations(): AnimatorSet { + val animators = mutableListOf() + listeners.forEach { listener -> + listener.onSystemEventAnimationFinish(hasPersistentDot)?.let { anim -> + animators.add(anim) + } + } + animators.add(chipAnimationController.onSystemEventAnimationFinish(hasPersistentDot)) + if (hasPersistentDot) { + val dotAnim = notifyTransitionToPersistentDot() + if (dotAnim != null) { + animators.add(dotAnim) + } + } + val animSet = AnimatorSet().also { + it.playTogether(animators) + } - scheduledEvent = null - }, DISPLAY_LENGTH) - }, DELAY) + return animSet } private fun notifyTransitionToPersistentDot(): Animator? { @@ -260,18 +281,6 @@ class SystemStatusAnimationScheduler @Inject constructor( return null } - private fun notifySystemStart() { - listeners.forEach { it.onSystemChromeAnimationStart() } - } - - private fun notifySystemFinish() { - listeners.forEach { it.onSystemChromeAnimationEnd() } - } - - private fun notifySystemAnimationUpdate(anim: ValueAnimator) { - listeners.forEach { it.onSystemChromeAnimationUpdate(anim) } - } - override fun addCallback(listener: SystemStatusAnimationCallback) { Assert.isMainThread() @@ -290,24 +299,6 @@ class SystemStatusAnimationScheduler @Inject constructor( } } - private val systemUpdateListener = ValueAnimator.AnimatorUpdateListener { - anim -> notifySystemAnimationUpdate(anim) - } - - private val systemAnimatorAdapter = object : AnimatorListenerAdapter() { - override fun onAnimationEnd(p0: Animator?) { - notifySystemFinish() - } - - override fun onAnimationStart(p0: Animator?) { - notifySystemStart() - } - } - - private val chipUpdateListener = ValueAnimator.AnimatorUpdateListener { - anim -> chipAnimationController.onChipAnimationUpdate(anim, animationState) - } - override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array) { pw.println("Scheduled event: $scheduledEvent") pw.println("Has persistent privacy dot: $hasPersistentDot") @@ -321,24 +312,6 @@ class SystemStatusAnimationScheduler @Inject constructor( } } } - - inner class ChipAnimatorAdapter( - @SystemAnimationState val endState: Int, - val viewCreator: ViewCreator - ) : AnimatorListenerAdapter() { - override fun onAnimationEnd(p0: Animator?) { - chipAnimationController.onChipAnimationEnd(animationState) - animationState = if (endState == SHOWING_PERSISTENT_DOT && !hasPersistentDot) { - IDLE - } else { - endState - } - } - - override fun onAnimationStart(p0: Animator?) { - chipAnimationController.onChipAnimationStart(viewCreator, animationState) - } - } } /** @@ -347,16 +320,14 @@ class SystemStatusAnimationScheduler @Inject constructor( * create space for the chip animation to display. This means hiding the system elements in the * status bar and keyguard. * - * TODO: the chip animation really only has one client, we can probably remove it from this - * interface - * * The value animators themselves are simple animators from 0.0 to 1.0. Listeners can apply any * interpolation they choose but realistically these are most likely to be simple alpha transitions */ interface SystemStatusAnimationCallback { - @JvmDefault fun onSystemChromeAnimationUpdate(animator: ValueAnimator) {} - @JvmDefault fun onSystemChromeAnimationStart() {} - @JvmDefault fun onSystemChromeAnimationEnd() {} + /** Implement this method to return an [Animator] or [AnimatorSet] that presents the chip */ + fun onSystemEventAnimationBegin(): Animator? { return null } + /** Implement this method to return an [Animator] or [AnimatorSet] that hides the chip */ + fun onSystemEventAnimationFinish(hasPersistentDot: Boolean): Animator? { return null } // Best method name, change my mind @JvmDefault @@ -366,50 +337,61 @@ interface SystemStatusAnimationCallback { @JvmDefault fun onHidePersistentDot(): Animator? { return null } } -interface SystemStatusChipAnimationCallback { - fun onChipAnimationUpdate(animator: ValueAnimator, @SystemAnimationState state: Int) {} - - fun onChipAnimationStart( - viewCreator: ViewCreator, - @SystemAnimationState state: Int - ) {} - - fun onChipAnimationEnd(@SystemAnimationState state: Int) {} -} - /** + * Animation state IntDef */ @Retention(AnnotationRetention.SOURCE) @IntDef( value = [ - IDLE, ANIMATING_IN, RUNNING_CHIP_ANIM, ANIMATING_OUT, SHOWING_PERSISTENT_DOT + IDLE, + ANIMATION_QUEUED, + ANIMATING_IN, + RUNNING_CHIP_ANIM, + ANIMATING_OUT, + SHOWING_PERSISTENT_DOT ] ) annotation class SystemAnimationState /** No animation is in progress */ const val IDLE = 0 +/** An animation is queued, and awaiting the debounce period */ +const val ANIMATION_QUEUED = 1 /** System is animating out, and chip is animating in */ -const val ANIMATING_IN = 1 +const val ANIMATING_IN = 2 /** Chip has animated in and is awaiting exit animation, and optionally playing its own animation */ -const val RUNNING_CHIP_ANIM = 2 +const val RUNNING_CHIP_ANIM = 3 /** Chip is animating away and system is animating back */ -const val ANIMATING_OUT = 3 +const val ANIMATING_OUT = 4 /** Chip has animated away, and the persistent dot is showing */ -const val SHOWING_PERSISTENT_DOT = 4 +const val SHOWING_PERSISTENT_DOT = 5 + +/** Commonly-needed interpolators can go here */ +@JvmField val STATUS_BAR_X_MOVE_OUT = PathInterpolator(0.33f, 0f, 0f, 1f) +@JvmField val STATUS_BAR_X_MOVE_IN = PathInterpolator(0f, 0f, 0f, 1f) +/** + * Status chip animation to dot have multiple stages of motion, the _1 and _2 interpolators should + * be used in succession + */ +val STATUS_CHIP_WIDTH_TO_DOT_KEYFRAME_1 = PathInterpolator(0.44f, 0f, 0.25f, 1f) +val STATUS_CHIP_WIDTH_TO_DOT_KEYFRAME_2 = PathInterpolator(0.3f, 0f, 0.26f, 1f) +val STATUS_CHIP_HEIGHT_TO_DOT_KEYFRAME_1 = PathInterpolator(0.4f, 0f, 0.17f, 1f) +val STATUS_CHIP_HEIGHT_TO_DOT_KEYFRAME_2 = PathInterpolator(0.3f, 0f, 0f, 1f) +val STATUS_CHIP_MOVE_TO_DOT = PathInterpolator(0f, 0f, 0.05f, 1f) private const val TAG = "SystemStatusAnimationScheduler" -private const val DELAY = 0L +private const val DEBOUNCE_DELAY = 100L /** - * The total time spent animation should be 1500ms. The entrance animation is how much time - * we give to the system to animate system elements out of the way. Total chip animation length - * will be equivalent to 2*chip_anim_length + display_length + * The total time spent on the chip animation is 1500ms, broken up into 3 sections: + * - 500ms to animate the chip in (including animating system icons away) + * - 500ms holding the chip on screen + * - 500ms to animate the chip away (and system icons back) + * + * So DISPLAY_LENGTH should be the sum of the first 2 phases, while the final 500ms accounts for + * the actual animation */ -private const val ENTRANCE_ANIM_LENGTH = 250L -private const val CHIP_ANIM_LENGTH = 250L -// 1s + entrance time + chip anim_length -private const val DISPLAY_LENGTH = 1500L +private const val DISPLAY_LENGTH = 1000L private const val MIN_UPTIME: Long = 5 * 1000 diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java index 65173a230871b..cb332bdf59b16 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java @@ -174,7 +174,7 @@ public class KeyguardStatusBarView extends RelativeLayout { } private void updateKeyguardStatusBarHeight() { - MarginLayoutParams lp = (MarginLayoutParams) getLayoutParams(); + MarginLayoutParams lp = (MarginLayoutParams) getLayoutParams(); lp.height = getStatusBarHeaderHeightKeyguard(mContext); setLayoutParams(lp); } @@ -510,28 +510,6 @@ public class KeyguardStatusBarView extends RelativeLayout { } } - void onSystemChromeAnimationStart(boolean isAnimatingOut) { - if (isAnimatingOut) { - mSystemIconsContainer.setVisibility(View.VISIBLE); - mSystemIconsContainer.setAlpha(0f); - } - } - - void onSystemChromeAnimationEnd(boolean isAnimatingIn) { - // Make sure the system icons are out of the way - if (isAnimatingIn) { - mSystemIconsContainer.setVisibility(View.INVISIBLE); - mSystemIconsContainer.setAlpha(0f); - } else { - mSystemIconsContainer.setAlpha(1f); - mSystemIconsContainer.setVisibility(View.VISIBLE); - } - } - - void onSystemChromeAnimationUpdate(float animatedValue) { - mSystemIconsContainer.setAlpha(animatedValue); - } - @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java index 1df1aff385939..af4fb8e2cf493 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java @@ -17,8 +17,6 @@ package com.android.systemui.statusbar.phone; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; -import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.ANIMATING_IN; -import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.ANIMATING_OUT; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -47,6 +45,7 @@ import com.android.systemui.statusbar.notification.AnimatableProperty; import com.android.systemui.statusbar.notification.PropertyAnimator; import com.android.systemui.statusbar.notification.stack.AnimationProperties; import com.android.systemui.statusbar.notification.stack.StackStateAnimator; +import com.android.systemui.statusbar.phone.fragment.StatusBarSystemEventAnimator; import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserInfoTracker; import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherController; import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherFeatureController; @@ -107,6 +106,8 @@ public class KeyguardStatusBarViewController extends ViewController mView.setKeyguardUserAvatarEnabled(!enabled)); + mSystemEventAnimator = new StatusBarSystemEventAnimator(mView, r); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java index 2c84219dbfd06..33bc401197451 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java @@ -20,12 +20,10 @@ import static android.app.StatusBarManager.DISABLE_NOTIFICATION_ICONS; import static android.app.StatusBarManager.DISABLE_ONGOING_CALL_CHIP; import static android.app.StatusBarManager.DISABLE_SYSTEM_INFO; -import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.ANIMATING_IN; -import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.ANIMATING_OUT; import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.IDLE; import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.SHOWING_PERSISTENT_DOT; -import android.animation.ValueAnimator; +import android.animation.Animator; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; @@ -136,6 +134,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue } }; private OperatorNameViewController mOperatorNameViewController; + private StatusBarSystemEventAnimator mSystemEventAnimator; @SuppressLint("ValidFragment") public CollapsedStatusBarFragment( @@ -210,7 +209,8 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue initEmergencyCryptkeeperText(); initOperatorName(); initNotificationIconArea(); - mAnimationScheduler.addCallback(this); + mSystemEventAnimator = + new StatusBarSystemEventAnimator(mSystemIconArea, getResources()); } @VisibleForTesting @@ -245,6 +245,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue mCommandQueue.addCallback(this); mStatusBarStateController.addCallback(this); initOngoingCallChip(); + mAnimationScheduler.addCallback(this); mSecureSettings.registerContentObserver( Settings.Secure.getUriFor(Settings.Secure.STATUS_BAR_SHOW_VIBRATE_ICON), @@ -258,6 +259,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue mCommandQueue.removeCallback(this); mStatusBarStateController.removeCallback(this); mOngoingCallController.removeCallback(mOngoingCallListener); + mAnimationScheduler.removeCallback(this); mSecureSettings.unregisterContentObserver(mVolumeSettingObserver); } @@ -265,7 +267,6 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue public void onDestroyView() { super.onDestroyView(); mStatusBarIconController.removeIconGroup(mDarkIconManager); - mAnimationScheduler.removeCallback(this); if (mNetworkController.hasEmergencyCryptKeeperText()) { mNetworkController.removeCallback(mSignalCallback); } @@ -576,35 +577,16 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue disable(getContext().getDisplayId(), mDisabled1, mDisabled2, false /* animate */); } + @Nullable @Override - public void onSystemChromeAnimationStart() { - if (mAnimationScheduler.getAnimationState() == ANIMATING_OUT - && !isSystemIconAreaDisabled()) { - mSystemIconArea.setVisibility(View.VISIBLE); - mSystemIconArea.setAlpha(0f); - } + public Animator onSystemEventAnimationBegin() { + return mSystemEventAnimator.onSystemEventAnimationBegin(); } + @Nullable @Override - public void onSystemChromeAnimationEnd() { - // Make sure the system icons are out of the way - if (mAnimationScheduler.getAnimationState() == ANIMATING_IN) { - mSystemIconArea.setVisibility(View.INVISIBLE); - mSystemIconArea.setAlpha(0f); - } else { - if (isSystemIconAreaDisabled()) { - // don't unhide - return; - } - - mSystemIconArea.setAlpha(1f); - mSystemIconArea.setVisibility(View.VISIBLE); - } - } - - @Override - public void onSystemChromeAnimationUpdate(@NonNull ValueAnimator animator) { - mSystemIconArea.setAlpha((float) animator.getAnimatedValue()); + public Animator onSystemEventAnimationFinish(boolean hasPersistentDot) { + return mSystemEventAnimator.onSystemEventAnimationFinish(hasPersistentDot); } private boolean isSystemIconAreaDisabled() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarSystemEventAnimator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarSystemEventAnimator.kt new file mode 100644 index 0000000000000..f530ec83aec54 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarSystemEventAnimator.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2022 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.statusbar.phone.fragment + +import android.animation.Animator +import android.animation.AnimatorSet +import android.animation.ValueAnimator +import android.content.res.Resources +import android.view.View +import com.android.systemui.R +import com.android.systemui.statusbar.events.STATUS_BAR_X_MOVE_IN +import com.android.systemui.statusbar.events.STATUS_BAR_X_MOVE_OUT +import com.android.systemui.statusbar.events.SystemStatusAnimationCallback + +/** + * Tied directly to [SystemStatusAnimationScheduler]. Any StatusBar-like thing (keyguard, collapsed + * status bar fragment), can just feed this an animatable view to get the default system status + * animation. + * + * This animator relies on resources, and should be recreated whenever resources are updated. While + * this class could be used directly as the animation callback, it's probably best to forward calls + * to it so that it can be recreated at any moment without needing to remove/add callback. + */ +class StatusBarSystemEventAnimator( + val animatedView: View, + resources: Resources +) : SystemStatusAnimationCallback { + private val translationXIn: Int = resources.getDimensionPixelSize( + R.dimen.ongoing_appops_chip_animation_in_status_bar_translation_x) + private val translationXOut: Int = resources.getDimensionPixelSize( + R.dimen.ongoing_appops_chip_animation_out_status_bar_translation_x) + + override fun onSystemEventAnimationBegin(): Animator { + val moveOut = ValueAnimator.ofFloat(0f, 1f).setDuration(383) + moveOut.interpolator = STATUS_BAR_X_MOVE_OUT + moveOut.addUpdateListener { animation: ValueAnimator -> + animatedView.translationX = -(translationXIn * animation.animatedValue as Float) + } + val alphaOut = ValueAnimator.ofFloat(1f, 0f).setDuration(133) + alphaOut.interpolator = null + alphaOut.addUpdateListener { animation: ValueAnimator -> + animatedView.alpha = animation.animatedValue as Float + } + + val animSet = AnimatorSet() + animSet.playTogether(moveOut, alphaOut) + return animSet + } + + override fun onSystemEventAnimationFinish(hasPersistentDot: Boolean): Animator { + animatedView.translationX = translationXOut.toFloat() + val moveIn = ValueAnimator.ofFloat(1f, 0f).setDuration(467) + moveIn.startDelay = 33 + moveIn.interpolator = STATUS_BAR_X_MOVE_IN + moveIn.addUpdateListener { animation: ValueAnimator -> + animatedView.translationX = translationXOut * animation.animatedValue as Float + } + val alphaIn = ValueAnimator.ofFloat(0f, 1f).setDuration(167) + alphaIn.startDelay = 67 + alphaIn.interpolator = null + alphaIn.addUpdateListener { animation: ValueAnimator -> + animatedView.alpha = animation.animatedValue as Float + } + + val animatorSet = AnimatorSet() + animatorSet.playTogether(moveIn, alphaIn) + + return animatorSet + } +} \ No newline at end of file