Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6a932544 authored by Selim Cinek's avatar Selim Cinek Committed by Automerger Merge Worker
Browse files

Merge "Transitioning media on lockscreen with a fade" into sc-dev am: 7ddf76be

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/14640413

Change-Id: I47bc1f04b2aa020df4a1230b4af6f7b1c3dfbf13
parents 3836c857 7ddf76be
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -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"
+7 −8
Original line number Diff line number Diff line
@@ -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>
+284 −43
Original line number Diff line number Diff line
@@ -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()
                }
@@ -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
@@ -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)!!
@@ -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) {
@@ -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 {
@@ -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
        }
@@ -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.
     */
@@ -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