diff options
| author | 2021-05-19 14:59:29 +0200 | |
|---|---|---|
| committer | 2021-05-26 16:23:26 +0200 | |
| commit | 67563f5ce01fb0ad6a409caed0180e14d6d4620b (patch) | |
| tree | fa9bb63ed9c6036466ac01d7dce18f6be6b52b1f | |
| parent | 48f518618e3c75ac793703da678e3c4ba9b05b64 (diff) | |
Transitioning media on lockscreen with a fade
UX wise a fade was much preferred, so we built in the capability
for media transitions to fade from location to location.
We're now also fading the media when transitioning
between QS and Lockscreen.
This also polishes the animation further.
Bug: 184946919
Test: atest SystemUITests
Change-Id: Id9fc58469bebe69ad7a0189e4c4acd36523cdeed
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 915b62791f58..dc278fb0c58e 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1409,10 +1409,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> @@ -1421,13 +1417,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 a804ae6a5cd8..f5daa867394d 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 @@ -5147,8 +5147,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 0d42428dd11d..0ce452e565ed 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); } @@ -1415,15 +1424,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 9d8a9bfafe49..4d35e9a59c41 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java @@ -507,6 +507,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! @@ -528,7 +535,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 @@ -811,7 +818,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); }); @@ -872,14 +879,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, @@ -2363,7 +2372,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) { @@ -2371,29 +2379,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); @@ -2401,8 +2408,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() |