diff options
| author | 2021-05-26 16:10:07 +0000 | |
|---|---|---|
| committer | 2021-05-26 16:10:07 +0000 | |
| commit | 7ddf76bedc8c5b7bac0a9d68b779590382875a6f (patch) | |
| tree | c64c170a6f1422acc84c60645dccfec729c39ad3 | |
| parent | f7e815dd30292b9370f44201253abbd064f59332 (diff) | |
| parent | 67563f5ce01fb0ad6a409caed0180e14d6d4620b (diff) | |
Merge "Transitioning media on lockscreen with a fade" into sc-dev
9 files changed, 426 insertions, 98 deletions
diff --git a/packages/SystemUI/res/layout/media_carousel.xml b/packages/SystemUI/res/layout/media_carousel.xml index 87acfd088939..52132e881c43 100644 --- a/packages/SystemUI/res/layout/media_carousel.xml +++ b/packages/SystemUI/res/layout/media_carousel.xml @@ -22,6 +22,7 @@ android:layout_height="wrap_content" android:clipChildren="false" android:clipToPadding="false" + android:forceHasOverlappingRendering="false" android:theme="@style/MediaPlayer"> <com.android.systemui.media.MediaScrollView android:id="@+id/media_carousel_scroller" diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index b2ab5f782d71..5c1e9355b650 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1418,10 +1418,6 @@ <dimen name="media_output_dialog_icon_corner_radius">16dp</dimen> <dimen name="media_output_dialog_title_anim_y_delta">12.5dp</dimen> - <!-- Delay after which the media will start transitioning to the full shade on - the lockscreen --> - <dimen name="lockscreen_shade_media_transition_start_delay">40dp</dimen> - <!-- Distance that the full shade transition takes in order for qs to fully transition to the shade --> <dimen name="lockscreen_shade_qs_transition_distance">200dp</dimen> @@ -1430,13 +1426,16 @@ the shade (in alpha) --> <dimen name="lockscreen_shade_scrim_transition_distance">80dp</dimen> - <!-- Extra inset for the notifications when accounting for media during the lockscreen to - shade transition to compensate for the disappearing media --> - <dimen name="lockscreen_shade_transition_extra_media_inset">-48dp</dimen> + <!-- Distance that the full shade transition takes in order for media to fully transition to + the shade --> + <dimen name="lockscreen_shade_media_transition_distance">140dp</dimen> <!-- Maximum overshoot for the topPadding of notifications when transitioning to the full shade --> - <dimen name="lockscreen_shade_max_top_overshoot">32dp</dimen> + <dimen name="lockscreen_shade_notification_movement">24dp</dimen> + + <!-- Maximum overshoot for the pulse expansion --> + <dimen name="pulse_expansion_max_top_overshoot">16dp</dimen> <dimen name="people_space_widget_radius">28dp</dimen> <dimen name="people_space_image_radius">20dp</dimen> diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt index 73dfe5e68d9a..075bc700cfa0 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt @@ -26,21 +26,23 @@ import android.util.MathUtils import android.view.View import android.view.ViewGroup import android.view.ViewGroupOverlay +import androidx.annotation.VisibleForTesting import com.android.systemui.R import com.android.systemui.animation.Interpolators import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.CrossFadeHelper import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.notification.stack.StackStateAnimator import com.android.systemui.statusbar.phone.KeyguardBypassController +import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.animation.UniqueObjectHostView import javax.inject.Inject -import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager /** * Similarly to isShown but also excludes views that have 0 alpha @@ -80,6 +82,7 @@ class MediaHierarchyManager @Inject constructor( wakefulnessLifecycle: WakefulnessLifecycle, private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager ) { + /** * The root overlay of the hierarchy. This is where the media notification is attached to * whenever the view is transitioning from one host to another. It also make sure that the @@ -90,6 +93,30 @@ class MediaHierarchyManager @Inject constructor( private var rootView: View? = null private var currentBounds = Rect() private var animationStartBounds: Rect = Rect() + + /** + * The cross fade progress at the start of the animation. 0.5f means it's just switching between + * the start and the end location and the content is fully faded, while 0.75f means that we're + * halfway faded in again in the target state. + */ + private var animationStartCrossFadeProgress = 0.0f + + /** + * The starting alpha of the animation + */ + private var animationStartAlpha = 0.0f + + /** + * The starting location of the cross fade if an animation is running right now. + */ + @MediaLocation + private var crossFadeAnimationStartLocation = -1 + + /** + * The end location of the cross fade if an animation is running right now. + */ + @MediaLocation + private var crossFadeAnimationEndLocation = -1 private var targetBounds: Rect = Rect() private val mediaFrame get() = mediaCarouselController.mediaFrame @@ -98,9 +125,22 @@ class MediaHierarchyManager @Inject constructor( interpolator = Interpolators.FAST_OUT_SLOW_IN addUpdateListener { updateTargetState() - interpolateBounds(animationStartBounds, targetBounds, animatedFraction, + val currentAlpha: Float + var boundsProgress = animatedFraction + if (isCrossFadeAnimatorRunning) { + animationCrossFadeProgress = MathUtils.lerp(animationStartCrossFadeProgress, 1.0f, + animatedFraction) + // When crossfading, let's keep the bounds at the right location during fading + boundsProgress = if (animationCrossFadeProgress < 0.5f) 0.0f else 1.0f + currentAlpha = calculateAlphaFromCrossFade(animationCrossFadeProgress, + instantlyShowAtEnd = false) + } else { + // If we're not crossfading, let's interpolate from the start alpha to 1.0f + currentAlpha = MathUtils.lerp(animationStartAlpha, 1.0f, animatedFraction) + } + interpolateBounds(animationStartBounds, targetBounds, boundsProgress, result = currentBounds) - applyState(currentBounds) + applyState(currentBounds, currentAlpha) } addListener(object : AnimatorListenerAdapter() { private var cancelled: Boolean = false @@ -112,6 +152,7 @@ class MediaHierarchyManager @Inject constructor( } override fun onAnimationEnd(animation: Animator?) { + isCrossFadeAnimatorRunning = false if (!cancelled) { applyTargetStateIfNotAnimating() } @@ -192,11 +233,6 @@ class MediaHierarchyManager @Inject constructor( private var distanceForFullShadeTransition = 0 /** - * Delay after which the media will start transitioning to the full shade on the lockscreen. - */ - private var fullShadeTransitionDelay = 0 - - /** * The amount of progress we are currently in if we're transitioning to the full shade. * 0.0f means we're not transitioning yet, while 1 means we're all the way in the full * shade. @@ -207,18 +243,33 @@ class MediaHierarchyManager @Inject constructor( return } field = value - if (bypassController.bypassEnabled) { + if (bypassController.bypassEnabled || statusbarState != StatusBarState.KEYGUARD) { + // No need to do all the calculations / updates below if we're not on the lockscreen + // or if we're bypassing. return } - updateDesiredLocation() + updateDesiredLocation(forceNoAnimation = isCurrentlyFading()) if (value >= 0) { updateTargetState() + // Setting the alpha directly, as the below call will use it to update the alpha + carouselAlpha = calculateAlphaFromCrossFade(field, instantlyShowAtEnd = true) applyTargetStateIfNotAnimating() } } + /** + * Is there currently a cross-fade animation running driven by an animator? + */ + private var isCrossFadeAnimatorRunning = false + + /** + * Are we currently transitionioning from the lockscreen to the full shade + * [StatusBarState.SHADE_LOCKED] or [StatusBarState.SHADE]. Once the user has dragged down and + * the transition starts, this will no longer return true. + */ private val isTransitioningToFullShade: Boolean - get() = fullShadeTransitionProgress != 0f && !bypassController.bypassEnabled + get() = fullShadeTransitionProgress != 0f && !bypassController.bypassEnabled && + statusbarState == StatusBarState.KEYGUARD /** * Set the amount of pixels we have currently dragged down if we're transitioning to the full @@ -227,14 +278,8 @@ class MediaHierarchyManager @Inject constructor( fun setTransitionToFullShadeAmount(value: Float) { // If we're transitioning starting on the shade_locked, we don't want any delay and rather // have it aligned with the rest of the animation - val delay = if (statusbarState == StatusBarState.KEYGUARD) { - fullShadeTransitionDelay - } else { - 0 - } - val progress = MathUtils.saturate((value - delay) / - (distanceForFullShadeTransition - delay)) - fullShadeTransitionProgress = Interpolators.FAST_OUT_SLOW_IN.getInterpolation(progress) + val progress = MathUtils.saturate(value / distanceForFullShadeTransition) + fullShadeTransitionProgress = progress } /** @@ -296,6 +341,49 @@ class MediaHierarchyManager @Inject constructor( } } + /** + * The current cross fade progress. 0.5f means it's just switching + * between the start and the end location and the content is fully faded, while 0.75f means + * that we're halfway faded in again in the target state. + * This is only valid while [isCrossFadeAnimatorRunning] is true. + */ + private var animationCrossFadeProgress = 1.0f + + /** + * The current carousel Alpha. + */ + private var carouselAlpha: Float = 1.0f + set(value) { + if (field == value) { + return + } + field = value + CrossFadeHelper.fadeIn(mediaFrame, value) + } + + /** + * Calculate the alpha of the view when given a cross-fade progress. + * + * @param crossFadeProgress The current cross fade progress. 0.5f means it's just switching + * between the start and the end location and the content is fully faded, while 0.75f means + * that we're halfway faded in again in the target state. + * + * @param instantlyShowAtEnd should the view be instantly shown at the end. This is needed + * to avoid fadinging in when the target was hidden anyway. + */ + private fun calculateAlphaFromCrossFade( + crossFadeProgress: Float, + instantlyShowAtEnd: Boolean + ): Float { + if (crossFadeProgress <= 0.5f) { + return 1.0f - crossFadeProgress / 0.5f + } else if (instantlyShowAtEnd) { + return 1.0f + } else { + return (crossFadeProgress - 0.5f) / 0.5f + } + } + init { updateConfiguration() configurationController.addCallback(object : ConfigurationController.ConfigurationListener { @@ -375,9 +463,7 @@ class MediaHierarchyManager @Inject constructor( private fun updateConfiguration() { distanceForFullShadeTransition = context.resources.getDimensionPixelSize( - R.dimen.lockscreen_shade_qs_transition_distance) - fullShadeTransitionDelay = context.resources.getDimensionPixelSize( - R.dimen.lockscreen_shade_media_transition_start_delay) + R.dimen.lockscreen_shade_media_transition_distance) } /** @@ -449,8 +535,13 @@ class MediaHierarchyManager @Inject constructor( shouldAnimateTransition(desiredLocation, previousLocation) val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation) val host = getHost(desiredLocation) - mediaCarouselController.onDesiredLocationChanged(desiredLocation, host, animate, - animDuration, delay) + val willFade = calculateTransformationType() == TRANSFORMATION_TYPE_FADE + if (!willFade || isCurrentlyInGuidedTransformation() || !animate) { + // if we're fading, we want the desired location / measurement only to change + // once fully faded. This is happening in the host attachment + mediaCarouselController.onDesiredLocationChanged(desiredLocation, host, + animate, animDuration, delay) + } performTransitionToNewLocation(isNewView, animate) } } @@ -470,6 +561,8 @@ class MediaHierarchyManager @Inject constructor( if (isCurrentlyInGuidedTransformation()) { applyTargetStateIfNotAnimating() } else if (animate) { + val wasCrossFading = isCrossFadeAnimatorRunning + val previewsCrossFadeProgress = animationCrossFadeProgress animator.cancel() if (currentAttachmentLocation != previousLocation || !previousHost.hostView.isAttachedToWindow) { @@ -482,6 +575,42 @@ class MediaHierarchyManager @Inject constructor( // be outdated animationStartBounds.set(previousHost.currentBounds) } + val transformationType = calculateTransformationType() + var needsCrossFade = transformationType == TRANSFORMATION_TYPE_FADE + var crossFadeStartProgress = 0.0f + // The alpha is only relevant when not cross fading + var newCrossFadeStartLocation = previousLocation + if (wasCrossFading) { + if (currentAttachmentLocation == crossFadeAnimationEndLocation) { + if (needsCrossFade) { + // We were previously crossFading and we've already reached + // the end view, Let's start crossfading from the same position there + crossFadeStartProgress = 1.0f - previewsCrossFadeProgress + } + // Otherwise let's fade in from the current alpha, but not cross fade + } else { + // We haven't reached the previous location yet, let's still cross fade from + // where we were. + newCrossFadeStartLocation = crossFadeAnimationStartLocation + if (newCrossFadeStartLocation == desiredLocation) { + // we're crossFading back to where we were, let's start at the end position + crossFadeStartProgress = 1.0f - previewsCrossFadeProgress + } else { + // Let's start from where we are right now + crossFadeStartProgress = previewsCrossFadeProgress + // We need to force cross fading as we haven't reached the end location yet + needsCrossFade = true + } + } + } else if (needsCrossFade) { + // let's not flicker and start with the same alpha + crossFadeStartProgress = (1.0f - carouselAlpha) / 2.0f + } + isCrossFadeAnimatorRunning = needsCrossFade + crossFadeAnimationStartLocation = newCrossFadeStartLocation + crossFadeAnimationEndLocation = desiredLocation + animationStartAlpha = carouselAlpha + animationStartCrossFadeProgress = crossFadeStartProgress adjustAnimatorForTransition(desiredLocation, previousLocation) if (!animationPending) { rootView?.let { @@ -518,6 +647,17 @@ class MediaHierarchyManager @Inject constructor( // non-trivial reattaching logic happening that will make the view not-shown earlier return true } + + if (statusbarState == StatusBarState.KEYGUARD) { + if (currentLocation == LOCATION_LOCKSCREEN && + previousLocation == LOCATION_QS || + (currentLocation == LOCATION_QS && + previousLocation == LOCATION_LOCKSCREEN)) { + // We're always fading from lockscreen to keyguard in situations where the player + // is already fully hidden + return false + } + } return mediaFrame.isShownNotFaded || animator.isRunning || animationPending } @@ -538,7 +678,7 @@ class MediaHierarchyManager @Inject constructor( keyguardStateController.isKeyguardFadingAway) { delay = keyguardStateController.keyguardFadingAwayDelay } - animDuration = StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE.toLong() + animDuration = (StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE / 2f).toLong() } else if (previousLocation == LOCATION_QQS && desiredLocation == LOCATION_LOCKSCREEN) { animDuration = StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR.toLong() } @@ -550,7 +690,7 @@ class MediaHierarchyManager @Inject constructor( // Let's immediately apply the target state (which is interpolated) if there is // no animation running. Otherwise the animation update will already update // the location - applyState(targetBounds) + applyState(targetBounds, carouselAlpha) } } @@ -558,7 +698,7 @@ class MediaHierarchyManager @Inject constructor( * Updates the bounds that the view wants to be in at the end of the animation. */ private fun updateTargetState() { - if (isCurrentlyInGuidedTransformation()) { + if (isCurrentlyInGuidedTransformation() && !isCurrentlyFading()) { val progress = getTransformationProgress() var endHost = getHost(desiredLocation)!! var starthost = getHost(previousLocation)!! @@ -606,12 +746,33 @@ class MediaHierarchyManager @Inject constructor( } /** + * Calculate the transformation type for the current animation + */ + @VisibleForTesting + @TransformationType + fun calculateTransformationType(): Int { + if (isTransitioningToFullShade) { + return TRANSFORMATION_TYPE_FADE + } + if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QS || + previousLocation == LOCATION_QS && desiredLocation == LOCATION_LOCKSCREEN) { + // animating between ls and qs should fade, as QS is clipped. + return TRANSFORMATION_TYPE_FADE + } + if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QQS) { + // animating between ls and qqs should fade when dragging down via e.g. expand button + return TRANSFORMATION_TYPE_FADE + } + return TRANSFORMATION_TYPE_TRANSITION + } + + /** * @return the current transformation progress if we're in a guided transformation and -1 * otherwise */ private fun getTransformationProgress(): Float { val progress = getQSTransformationProgress() - if (progress >= 0) { + if (statusbarState != StatusBarState.KEYGUARD && progress >= 0) { return progress } if (isTransitioningToFullShade) { @@ -643,19 +804,20 @@ class MediaHierarchyManager @Inject constructor( private fun cancelAnimationAndApplyDesiredState() { animator.cancel() getHost(desiredLocation)?.let { - applyState(it.currentBounds, immediately = true) + applyState(it.currentBounds, alpha = 1.0f, immediately = true) } } /** * Apply the current state to the view, updating it's bounds and desired state */ - private fun applyState(bounds: Rect, immediately: Boolean = false) { + private fun applyState(bounds: Rect, alpha: Float, immediately: Boolean = false) { currentBounds.set(bounds) - val currentlyInGuidedTransformation = isCurrentlyInGuidedTransformation() - val startLocation = if (currentlyInGuidedTransformation) previousLocation else -1 - val progress = if (currentlyInGuidedTransformation) getTransformationProgress() else 1.0f - val endLocation = desiredLocation + carouselAlpha = if (isCurrentlyFading()) alpha else 1.0f + val onlyUseEndState = !isCurrentlyInGuidedTransformation() || isCurrentlyFading() + val startLocation = if (onlyUseEndState) -1 else previousLocation + val progress = if (onlyUseEndState) 1.0f else getTransformationProgress() + val endLocation = resolveLocationForFading() mediaCarouselController.setCurrentState(startLocation, endLocation, progress, immediately) updateHostAttachment() if (currentAttachmentLocation == IN_OVERLAY) { @@ -668,8 +830,19 @@ class MediaHierarchyManager @Inject constructor( } private fun updateHostAttachment() { - val inOverlay = isTransitionRunning() && rootOverlay != null - val newLocation = if (inOverlay) IN_OVERLAY else desiredLocation + var newLocation = resolveLocationForFading() + var canUseOverlay = !isCurrentlyFading() + if (isCrossFadeAnimatorRunning) { + if (getHost(newLocation)?.visible == true && + getHost(newLocation)?.hostView?.isShown == false && + newLocation != desiredLocation) { + // We're crossfading but the view is already hidden. Let's move to the overlay + // instead. This happens when animating to the full shade using a button click. + canUseOverlay = true + } + } + val inOverlay = isTransitionRunning() && rootOverlay != null && canUseOverlay + newLocation = if (inOverlay) IN_OVERLAY else newLocation if (currentAttachmentLocation != newLocation) { currentAttachmentLocation = newLocation @@ -677,10 +850,10 @@ class MediaHierarchyManager @Inject constructor( (mediaFrame.parent as ViewGroup?)?.removeView(mediaFrame) // Add it to the new one - val targetHost = getHost(desiredLocation)!!.hostView if (inOverlay) { rootOverlay!!.add(mediaFrame) } else { + val targetHost = getHost(newLocation)!!.hostView // When adding back to the host, let's make sure to reset the bounds. // Usually adding the view will trigger a layout that does this automatically, // but we sometimes suppress this. @@ -693,7 +866,37 @@ class MediaHierarchyManager @Inject constructor( left + currentBounds.width(), top + currentBounds.height()) } + if (isCrossFadeAnimatorRunning) { + // When cross-fading with an animation, we only notify the media carousel of the + // location change, once the view is reattached to the new place and not immediately + // when the desired location changes. This callback will update the measurement + // of the carousel, only once we've faded out at the old location and then reattach + // to fade it in at the new location. + mediaCarouselController.onDesiredLocationChanged( + newLocation, + getHost(newLocation), + animate = false + ) + } + } + } + + /** + * Calculate the location when cross fading between locations. While fading out, + * the content should remain in the previous location, while after the switch it should + * be at the desired location. + */ + private fun resolveLocationForFading(): Int { + if (isCrossFadeAnimatorRunning) { + // When animating between two hosts with a fade, let's keep ourselves in the old + // location for the first half, and then switch over to the end location + if (animationCrossFadeProgress > 0.5 || previousLocation == -1) { + return crossFadeAnimationEndLocation + } else { + return crossFadeAnimationStartLocation + } } + return desiredLocation } private fun isTransitionRunning(): Boolean { @@ -708,29 +911,29 @@ class MediaHierarchyManager @Inject constructor( return desiredLocation } val onLockscreen = (!bypassController.bypassEnabled && - (statusbarState == StatusBarState.KEYGUARD || - statusbarState == StatusBarState.FULLSCREEN_USER_SWITCHER)) + (statusbarState == StatusBarState.KEYGUARD || + statusbarState == StatusBarState.FULLSCREEN_USER_SWITCHER)) val allowedOnLockscreen = notifLockscreenUserManager.shouldShowLockscreenNotifications() val location = when { qsExpansion > 0.0f && !onLockscreen -> LOCATION_QS qsExpansion > 0.4f && onLockscreen -> LOCATION_QS - onLockscreen && isTransitioningToFullShade -> LOCATION_QQS + onLockscreen && isTransformingToFullShadeAndInQQS() -> LOCATION_QQS onLockscreen && allowedOnLockscreen -> LOCATION_LOCKSCREEN else -> LOCATION_QQS } // When we're on lock screen and the player is not active, we should keep it in QS. // Otherwise it will try to animate a transition that doesn't make sense. if (location == LOCATION_LOCKSCREEN && getHost(location)?.visible != true && - !statusBarStateController.isDozing) { + !statusBarStateController.isDozing) { return LOCATION_QS } if (location == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QS && - collapsingShadeFromQS) { + collapsingShadeFromQS) { // When collapsing on the lockscreen, we want to remain in QS return LOCATION_QS } if (location != LOCATION_LOCKSCREEN && desiredLocation == LOCATION_LOCKSCREEN && - !fullyAwake) { + !fullyAwake) { // When unlocking from dozing / while waking up, the media shouldn't be transitioning // in an animated way. Let's keep it in the lockscreen until we're fully awake and // reattach it without an animation @@ -740,6 +943,26 @@ class MediaHierarchyManager @Inject constructor( } /** + * Are we currently transforming to the full shade and already in QQS + */ + private fun isTransformingToFullShadeAndInQQS(): Boolean { + if (!isTransitioningToFullShade) { + return false + } + return fullShadeTransitionProgress > 0.5f + } + + /** + * Is the current transformationType fading + */ + private fun isCurrentlyFading(): Boolean { + if (isTransitioningToFullShade) { + return true + } + return isCrossFadeAnimatorRunning + } + + /** * Returns true when the media card could be visible to the user if existed. */ private fun isVisibleToUser(): Boolean { @@ -789,9 +1012,27 @@ class MediaHierarchyManager @Inject constructor( * Attached at the root of the hierarchy in an overlay */ const val IN_OVERLAY = -1000 + + /** + * The default transformation type where the hosts transform into each other using a direct + * transition + */ + const val TRANSFORMATION_TYPE_TRANSITION = 0 + + /** + * A transformation type where content fades from one place to another instead of + * transitioning + */ + const val TRANSFORMATION_TYPE_FADE = 1 } } +@IntDef(prefix = ["TRANSFORMATION_TYPE_"], value = [ + MediaHierarchyManager.TRANSFORMATION_TYPE_TRANSITION, + MediaHierarchyManager.TRANSFORMATION_TYPE_FADE]) +@Retention(AnnotationRetention.SOURCE) +private annotation class TransformationType + @IntDef(prefix = ["LOCATION_"], value = [MediaHierarchyManager.LOCATION_QS, MediaHierarchyManager.LOCATION_QQS, MediaHierarchyManager.LOCATION_LOCKSCREEN]) @Retention(AnnotationRetention.SOURCE) diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java index 34c654c9135d..1c5fa439d0ee 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java @@ -95,7 +95,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca private boolean mLastKeyguardAndExpanded; /** * The last received state from the controller. This should not be used directly to check if - * we're on keyguard but use {@link #isKeyguardShowing()} instead since that is more accurate + * we're on keyguard but use {@link #isKeyguardState()} instead since that is more accurate * during state transitions which often call into us. */ private int mState; @@ -326,7 +326,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca || mHeaderAnimating; mQSPanelController.setExpanded(mQsExpanded); mQSDetail.setExpanded(mQsExpanded); - boolean keyguardShowing = isKeyguardShowing(); + boolean keyguardShowing = isKeyguardState(); mHeader.setVisibility((mQsExpanded || !keyguardShowing || mHeaderAnimating || mShowCollapsedOnKeyguard) ? View.VISIBLE @@ -344,7 +344,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca !mQsDisabled && expandVisually ? View.VISIBLE : View.INVISIBLE); } - private boolean isKeyguardShowing() { + private boolean isKeyguardState() { // We want the freshest state here since otherwise we'll have some weirdness if earlier // listeners trigger updates return mStatusBarStateController.getState() == StatusBarState.KEYGUARD; @@ -366,7 +366,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca if (mQSAnimator != null) { mQSAnimator.setShowCollapsedOnKeyguard(showCollapsed); } - if (!showCollapsed && isKeyguardShowing()) { + if (!showCollapsed && isKeyguardState()) { setQsExpansion(mLastQSExpansion, 0); } } @@ -457,7 +457,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca mContainer.setExpansion(expansion); final float translationScaleY = (mTranslateWhileExpanding ? 1 : QSAnimator.SHORT_PARALLAX_AMOUNT) * (expansion - 1); - boolean onKeyguardAndExpanded = isKeyguardShowing() && !mShowCollapsedOnKeyguard; + boolean onKeyguardAndExpanded = isKeyguardState() && !mShowCollapsedOnKeyguard; if (!mHeaderAnimating && !headerWillBeAnimating()) { getView().setTranslationY( onKeyguardAndExpanded @@ -531,7 +531,6 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca // The Media can be scrolled off screen by default, let's offset it float expandedMediaPosition = absoluteBottomPosition - mQSPanelScrollView.getScrollY() + mQSPanelScrollView.getScrollRange(); - // The expanded media host should never move below the laid out position pinToBottom(expandedMediaPosition, mQsMediaHost, true /* expanded */); // The expanded media host should never move above the laid out position pinToBottom(absoluteBottomPosition, mQqsMediaHost, false /* expanded */); @@ -540,7 +539,8 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca private void pinToBottom(float absoluteBottomPosition, MediaHost mediaHost, boolean expanded) { View hostView = mediaHost.getHostView(); - if (mLastQSExpansion > 0) { + // on keyguard we cross-fade to expanded, so no need to pin it. + if (mLastQSExpansion > 0 && !isKeyguardState()) { float targetPosition = absoluteBottomPosition - getTotalBottomMargin(hostView) - hostView.getHeight(); float currentPosition = mediaHost.getCurrentBounds().top @@ -573,7 +573,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca private boolean headerWillBeAnimating() { return mState == StatusBarState.KEYGUARD && mShowCollapsedOnKeyguard - && !isKeyguardShowing(); + && !isKeyguardState(); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 21d8164ba491..94edbd092a4d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -5145,8 +5145,8 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } /** - * Sets the extra top inset for the full shade transition. This is needed to compensate for - * media transitioning to quick settings + * Sets the extra top inset for the full shade transition. This moves notifications down + * during the drag down. */ public void setExtraTopInsetForFullShadeTransition(float inset) { mExtraTopInsetForFullShadeTransition = inset; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index d23a309ad1e9..4432f5463802 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -185,8 +185,17 @@ public class NotificationStackScrollLayoutController { private ColorExtractor.OnColorsChangedListener mOnColorsChangedListener; + /** + * The total distance in pixels that the full shade transition takes to transition entirely to + * the full shade. + */ private int mTotalDistanceForFullShadeTransition; - private int mTotalExtraMediaInsetFullShadeTransition; + + /** + * The amount of movement the notifications do when transitioning to the full shade before + * reaching the overstrech + */ + private int mNotificationDragDownMovement; @VisibleForTesting final View.OnAttachStateChangeListener mOnAttachStateChangeListener = @@ -255,8 +264,8 @@ public class NotificationStackScrollLayoutController { }; private void updateResources() { - mTotalExtraMediaInsetFullShadeTransition = mResources.getDimensionPixelSize( - R.dimen.lockscreen_shade_transition_extra_media_inset); + mNotificationDragDownMovement = mResources.getDimensionPixelSize( + R.dimen.lockscreen_shade_notification_movement); mTotalDistanceForFullShadeTransition = mResources.getDimensionPixelSize( R.dimen.lockscreen_shade_qs_transition_distance); } @@ -1410,15 +1419,13 @@ public class NotificationStackScrollLayoutController { * shade. 0.0f means we're not transitioning yet. */ public void setTransitionToFullShadeAmount(float amount) { - float extraTopInset; - MediaHeaderView view = mKeyguardMediaController.getSinglePaneContainer(); - if (view == null || view.getHeight() == 0 - || mStatusBarStateController.getState() != KEYGUARD) { - extraTopInset = 0; - } else { - extraTopInset = MathUtils.saturate(amount / mTotalDistanceForFullShadeTransition); - extraTopInset = Interpolators.FAST_OUT_SLOW_IN.getInterpolation(extraTopInset); - extraTopInset = extraTopInset * mTotalExtraMediaInsetFullShadeTransition; + float extraTopInset = 0.0f; + if (mStatusBarStateController.getState() == KEYGUARD) { + float overallProgress = MathUtils.saturate(amount / mView.getHeight()); + float transitionProgress = Interpolators.getOvershootInterpolation(overallProgress, + 0.6f, + (float) mTotalDistanceForFullShadeTransition / (float) mView.getHeight()); + extraTopInset = transitionProgress * mNotificationDragDownMovement; } mView.setExtraTopInsetForFullShadeTransition(extraTopInset); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java index f4710f49524d..7c2723d724ce 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java @@ -325,7 +325,9 @@ public class KeyguardClockPositionAlgorithm { */ private float getClockAlpha(int y) { float alphaKeyguard = Math.max(0, y / Math.max(1f, getClockY(1f, mDarkAmount))); - alphaKeyguard *= (1f - mQsExpansion); + float qsAlphaFactor = MathUtils.saturate(mQsExpansion / 0.3f); + qsAlphaFactor = 1f - qsAlphaFactor; + alphaKeyguard *= qsAlphaFactor; alphaKeyguard = Interpolators.ACCELERATE.getInterpolation(alphaKeyguard); return MathUtils.lerp(alphaKeyguard, 1f, mDarkAmount); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java index def9092e4171..74071439b0d7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java @@ -510,6 +510,13 @@ public class NotificationPanelViewController extends PanelViewController { private float mSectionPadding; /** + * The padding between the start of notifications and the qs boundary on the lockscreen. + * On lockscreen, notifications aren't inset this extra amount, but we still want the + * qs boundary to be padded. + */ + private int mLockscreenNotificationQSPadding; + + /** * The amount of progress we are currently in if we're transitioning to the full shade. * 0.0f means we're not transitioning yet, while 1 means we're all the way in the full * shade. This value can also go beyond 1.1 when we're overshooting! @@ -531,7 +538,7 @@ public class NotificationPanelViewController extends PanelViewController { /** * The maximum overshoot allowed for the top padding for the full shade transition */ - private int mMaxOverscrollAmountForDragDown; + private int mMaxOverscrollAmountForPulse; /** * Should we animate the next bounds update @@ -823,7 +830,7 @@ public class NotificationPanelViewController extends PanelViewController { amount -> { float progress = amount / mView.getHeight(); float overstretch = Interpolators.getOvershootInterpolation(progress, - (float) mMaxOverscrollAmountForDragDown / mView.getHeight(), + (float) mMaxOverscrollAmountForPulse / mView.getHeight(), 0.2f); setOverStrechAmount(overstretch); }); @@ -886,14 +893,16 @@ public class NotificationPanelViewController extends PanelViewController { R.dimen.heads_up_status_bar_padding); mDistanceForQSFullShadeTransition = mResources.getDimensionPixelSize( R.dimen.lockscreen_shade_qs_transition_distance); - mMaxOverscrollAmountForDragDown = mResources.getDimensionPixelSize( - R.dimen.lockscreen_shade_max_top_overshoot); + mMaxOverscrollAmountForPulse = mResources.getDimensionPixelSize( + R.dimen.pulse_expansion_max_top_overshoot); mScrimCornerRadius = mResources.getDimensionPixelSize( R.dimen.notification_scrim_corner_radius); mScreenCornerRadius = mResources.getDimensionPixelSize( com.android.internal.R.dimen.rounded_corner_radius); mNotificationScrimPadding = mResources.getDimensionPixelSize( R.dimen.notification_side_paddings); + mLockscreenNotificationQSPadding = mResources.getDimensionPixelSize( + R.dimen.notification_side_paddings); } private void updateViewControllers(KeyguardStatusView keyguardStatusView, @@ -2380,7 +2389,6 @@ public class NotificationPanelViewController extends PanelViewController { public void setTransitionToFullShadeAmount(float pxAmount, boolean animate, long delay) { mAnimateNextNotificationBounds = animate && !mShouldUseSplitNotificationShade; mNotificationBoundsAnimationDelay = delay; - float progress = MathUtils.saturate(pxAmount / mView.getHeight()); float endPosition = 0; if (pxAmount > 0.0f) { @@ -2388,29 +2396,28 @@ public class NotificationPanelViewController extends PanelViewController { && !mMediaDataManager.hasActiveMedia()) { // No notifications are visible, let's animate to the height of qs instead if (mQs != null) { - // Let's interpolate to the header height - endPosition = mQs.getHeader().getHeight(); + // Let's interpolate to the header height instead of the top padding, + // because the toppadding is way too low because of the large clock. + // we still want to take into account the edgePosition though as that nicely + // overshoots in the stackscroller + endPosition = getQSEdgePosition() + - mNotificationStackScrollLayoutController.getTopPadding() + + mQs.getHeader().getHeight(); } } else { // Interpolating to the new bottom edge position! - endPosition = getQSEdgePosition() - mOverStretchAmount; - - // If we have media, we need to put the boundary below it, as the media header - // still uses the space during the transition. - endPosition += - mNotificationStackScrollLayoutController.getFullShadeTransitionInset(); + endPosition = getQSEdgePosition() + + mNotificationStackScrollLayoutController.getFullShadeTransitionInset(); + if (isOnKeyguard()) { + endPosition -= mLockscreenNotificationQSPadding; + } } } // Calculate the overshoot amount such that we're reaching the target after our desired // distance, but only reach it fully once we drag a full shade length. - float transitionProgress = 0; - if (endPosition != 0 && progress != 0) { - transitionProgress = Interpolators.getOvershootInterpolation(progress, - mMaxOverscrollAmountForDragDown / endPosition, - (float) mDistanceForQSFullShadeTransition / (float) mView.getHeight()); - } - mTransitioningToFullShadeProgress = transitionProgress; + mTransitioningToFullShadeProgress = Interpolators.FAST_OUT_SLOW_IN.getInterpolation( + MathUtils.saturate(pxAmount / mDistanceForQSFullShadeTransition)); int position = (int) MathUtils.lerp((float) 0, endPosition, mTransitioningToFullShadeProgress); @@ -2418,8 +2425,6 @@ public class NotificationPanelViewController extends PanelViewController { // we want at least 1 pixel otherwise the panel won't be clipped position = Math.max(1, position); } - float overStretchAmount = Math.max(position - endPosition, 0.0f); - setOverStrechAmount(overStretchAmount); mTransitionToFullShadeQSPosition = position; updateQsExpansion(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt index c6aef4a18373..bf87a4a59c49 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt @@ -20,6 +20,7 @@ import android.graphics.Rect import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.ViewGroup +import android.widget.FrameLayout import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.controls.controller.ControlsControllerImplTest.Companion.eq @@ -33,6 +34,7 @@ import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.animation.UniqueObjectHostView +import junit.framework.Assert import org.junit.Assert.assertNotNull import org.junit.Before import org.junit.Rule @@ -65,8 +67,6 @@ class MediaHierarchyManagerTest : SysuiTestCase() { @Mock private lateinit var bypassController: KeyguardBypassController @Mock - private lateinit var mediaFrame: ViewGroup - @Mock private lateinit var keyguardStateController: KeyguardStateController @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController @@ -90,9 +90,11 @@ class MediaHierarchyManagerTest : SysuiTestCase() { @Rule val mockito = MockitoJUnit.rule() private lateinit var mediaHiearchyManager: MediaHierarchyManager + private lateinit var mediaFrame: ViewGroup @Before fun setup() { + mediaFrame = FrameLayout(context) `when`(mediaCarouselController.mediaFrame).thenReturn(mediaFrame) mediaHiearchyManager = MediaHierarchyManager( context, @@ -112,6 +114,9 @@ class MediaHierarchyManagerTest : SysuiTestCase() { `when`(statusBarStateController.state).thenReturn(StatusBarState.SHADE) `when`(mediaCarouselController.mediaCarouselScrollHandler) .thenReturn(mediaCarouselScrollHandler) + val observer = wakefullnessObserver.value + assertNotNull("lifecycle observer wasn't registered", observer) + observer.onFinishedWakingUp() // We'll use the viewmanager to verify a few calls below, let's reset this. clearInvocations(mediaCarouselController) } @@ -120,6 +125,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { `when`(host.location).thenReturn(location) `when`(host.currentBounds).thenReturn(Rect()) `when`(host.hostView).thenReturn(UniqueObjectHostView(context)) + `when`(host.visible).thenReturn(true) mediaHiearchyManager.register(host) } @@ -160,6 +166,73 @@ class MediaHierarchyManagerTest : SysuiTestCase() { } @Test + fun testGoingToFullShade() { + // Let's set it onto Lock screen + `when`(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) + `when`(notificationLockscreenUserManager.shouldShowLockscreenNotifications()).thenReturn( + true) + statusBarCallback.value.onStatePreChange(StatusBarState.SHADE, StatusBarState.KEYGUARD) + clearInvocations(mediaCarouselController) + + // Let's transition all the way to full shade + mediaHiearchyManager.setTransitionToFullShadeAmount(100000f) + verify(mediaCarouselController).onDesiredLocationChanged( + eq(MediaHierarchyManager.LOCATION_QQS), + any(MediaHostState::class.java), + eq(false), + anyLong(), + anyLong()) + clearInvocations(mediaCarouselController) + + // Let's go back to the lock screen + mediaHiearchyManager.setTransitionToFullShadeAmount(0.0f) + verify(mediaCarouselController).onDesiredLocationChanged( + eq(MediaHierarchyManager.LOCATION_LOCKSCREEN), + any(MediaHostState::class.java), + eq(false), + anyLong(), + anyLong()) + + // Let's make sure alpha is set + mediaHiearchyManager.setTransitionToFullShadeAmount(2.0f) + Assert.assertTrue("alpha should not be 1.0f when cross fading", mediaFrame.alpha != 1.0f) + } + + @Test + fun testTransformationOnLockScreenIsFading() { + // Let's set it onto Lock screen + `when`(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) + `when`(notificationLockscreenUserManager.shouldShowLockscreenNotifications()).thenReturn( + true) + statusBarCallback.value.onStatePreChange(StatusBarState.SHADE, StatusBarState.KEYGUARD) + clearInvocations(mediaCarouselController) + + // Let's transition from lockscreen to qs + mediaHiearchyManager.qsExpansion = 1.0f + val transformType = mediaHiearchyManager.calculateTransformationType() + Assert.assertTrue("media isn't transforming to qs with a fade", + transformType == MediaHierarchyManager.TRANSFORMATION_TYPE_FADE) + } + + @Test + fun testTransformationOnLockScreenToQQSisFading() { + // Let's set it onto Lock screen + `when`(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) + `when`(notificationLockscreenUserManager.shouldShowLockscreenNotifications()).thenReturn( + true) + statusBarCallback.value.onStatePreChange(StatusBarState.SHADE, StatusBarState.KEYGUARD) + clearInvocations(mediaCarouselController) + + // Let's transition from lockscreen to qs + `when`(statusBarStateController.state).thenReturn(StatusBarState.SHADE_LOCKED) + statusBarCallback.value.onStatePreChange(StatusBarState.KEYGUARD, + StatusBarState.SHADE_LOCKED) + val transformType = mediaHiearchyManager.calculateTransformationType() + Assert.assertTrue("media isn't transforming to qqswith a fade", + transformType == MediaHierarchyManager.TRANSFORMATION_TYPE_FADE) + } + + @Test fun testCloseGutsRelayToCarousel() { mediaHiearchyManager.closeGuts() |