Loading packages/SystemUI/res/layout/media_carousel.xml +1 −0 Original line number Diff line number Diff line Loading @@ -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" Loading packages/SystemUI/res/values/dimens.xml +7 −8 Original line number Diff line number Diff line Loading @@ -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> Loading @@ -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> Loading packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt +284 −43 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading @@ -112,6 +152,7 @@ class MediaHierarchyManager @Inject constructor( } override fun onAnimationEnd(animation: Animator?) { isCrossFadeAnimatorRunning = false if (!cancelled) { applyTargetStateIfNotAnimating() } Loading Loading @@ -191,11 +232,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 Loading @@ -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 Loading @@ -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 } /** Loading Loading @@ -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 { Loading Loading @@ -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) } /** Loading Loading @@ -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) } } Loading @@ -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) { Loading @@ -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 { Loading Loading @@ -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 } Loading @@ -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() } Loading @@ -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) } } Loading @@ -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)!! Loading Loading @@ -605,13 +745,34 @@ class MediaHierarchyManager @Inject constructor( return getTransformationProgress() >= 0 } /** * 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) { Loading Loading @@ -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) { Loading @@ -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 Loading @@ -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. Loading @@ -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 { Loading @@ -714,7 +917,7 @@ class MediaHierarchyManager @Inject constructor( 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 } Loading @@ -739,6 +942,26 @@ class MediaHierarchyManager @Inject constructor( return location } /** * 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. */ Loading Loading @@ -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) Loading Loading
packages/SystemUI/res/layout/media_carousel.xml +1 −0 Original line number Diff line number Diff line Loading @@ -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" Loading
packages/SystemUI/res/values/dimens.xml +7 −8 Original line number Diff line number Diff line Loading @@ -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> Loading @@ -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> Loading
packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt +284 −43 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading @@ -112,6 +152,7 @@ class MediaHierarchyManager @Inject constructor( } override fun onAnimationEnd(animation: Animator?) { isCrossFadeAnimatorRunning = false if (!cancelled) { applyTargetStateIfNotAnimating() } Loading Loading @@ -191,11 +232,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 Loading @@ -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 Loading @@ -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 } /** Loading Loading @@ -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 { Loading Loading @@ -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) } /** Loading Loading @@ -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) } } Loading @@ -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) { Loading @@ -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 { Loading Loading @@ -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 } Loading @@ -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() } Loading @@ -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) } } Loading @@ -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)!! Loading Loading @@ -605,13 +745,34 @@ class MediaHierarchyManager @Inject constructor( return getTransformationProgress() >= 0 } /** * 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) { Loading Loading @@ -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) { Loading @@ -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 Loading @@ -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. Loading @@ -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 { Loading @@ -714,7 +917,7 @@ class MediaHierarchyManager @Inject constructor( 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 } Loading @@ -739,6 +942,26 @@ class MediaHierarchyManager @Inject constructor( return location } /** * 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. */ Loading Loading @@ -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) Loading