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
This commit is contained in:
Evan Laird
2022-03-09 16:34:06 -05:00
parent b6d298a4d5
commit c1b85a8b3d
8 changed files with 421 additions and 288 deletions

View File

@@ -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"
/>
</com.android.systemui.privacy.OngoingPrivacyChip>

View File

@@ -941,6 +941,9 @@
<!-- Three privacy items. This value must not be exceeded -->
<dimen name="ongoing_appops_chip_max_width">76dp</dimen>
<dimen name="ongoing_appops_dot_diameter">6dp</dimen>
<dimen name="ongoing_appops_chip_min_animation_width">10dp</dimen>
<dimen name="ongoing_appops_chip_animation_in_status_bar_translation_x">15dp</dimen>
<dimen name="ongoing_appops_chip_animation_out_status_bar_translation_x">7dp</dimen>
<!-- Total minimum padding to enforce to ensure that the dot can always show -->
<dimen name="ongoing_appops_dot_min_padding">20dp</dimen>

View File

@@ -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
)
}
}
/**

View File

@@ -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<Animator>()
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<Animator>()
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<out String>) {
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

View File

@@ -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);

View File

@@ -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<KeyguardStat
@Override
public void onDensityOrFontScaleChanged() {
mView.loadDimens();
// The animator is dependent on resources for offsets
mSystemEventAnimator = new StatusBarSystemEventAnimator(mView, getResources());
}
@Override
@@ -123,21 +124,16 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
private final SystemStatusAnimationCallback mAnimationCallback =
new SystemStatusAnimationCallback() {
@NonNull
@Override
public void onSystemChromeAnimationStart() {
mView.onSystemChromeAnimationStart(
mAnimationScheduler.getAnimationState() == ANIMATING_OUT);
public Animator onSystemEventAnimationFinish(boolean hasPersistentDot) {
return mSystemEventAnimator.onSystemEventAnimationFinish(hasPersistentDot);
}
@NonNull
@Override
public void onSystemChromeAnimationEnd() {
mView.onSystemChromeAnimationEnd(
mAnimationScheduler.getAnimationState() == ANIMATING_IN);
}
@Override
public void onSystemChromeAnimationUpdate(@NonNull ValueAnimator anim) {
mView.onSystemChromeAnimationUpdate((float) anim.getAnimatedValue());
public Animator onSystemEventAnimationBegin() {
return mSystemEventAnimator.onSystemEventAnimationBegin();
}
};
@@ -232,6 +228,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
private int mStatusBarState;
private boolean mDozing;
private boolean mShowingKeyguardHeadsUp;
private StatusBarSystemEventAnimator mSystemEventAnimator;
@Inject
public KeyguardStatusBarViewController(
@@ -302,6 +299,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
mView.setKeyguardUserAvatarEnabled(
!mFeatureController.isStatusBarUserSwitcherFeatureEnabled());
mFeatureController.addCallback(enabled -> mView.setKeyguardUserAvatarEnabled(!enabled));
mSystemEventAnimator = new StatusBarSystemEventAnimator(mView, r);
}
@Override

View File

@@ -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() {

View File

@@ -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
}
}