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

Commit e5d6da0c authored by Miranda Kephart's avatar Miranda Kephart
Browse files

Update shelf screenshot UI entrance animation

Bug: 332410356
Test: manual (visual animation change)
Flag: ACONFIG com.android.systemui.screenshot_shelf_ui DEVELOPMENT

Change-Id: If525186a81d896b8b3420f478045912e2a3771aa
parent bf5ead7c
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -147,4 +147,11 @@
        <include layout="@layout/screenshot_work_profile_first_run" />
        <include layout="@layout/screenshot_detection_notice" />
    </FrameLayout>
    <ImageView
        android:id="@+id/screenshot_flash"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="gone"
        android:elevation="12dp"
        android:src="@android:color/white"/>
</com.android.systemui.screenshot.ui.ScreenshotShelfView>
+5 −1
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import android.view.WindowInsets
import android.view.WindowManager
import android.window.OnBackInvokedCallback
import android.window.OnBackInvokedDispatcher
import androidx.core.animation.doOnEnd
import com.android.internal.logging.UiEventLogger
import com.android.systemui.log.DebugLogger.debugLog
import com.android.systemui.res.R
@@ -109,7 +110,10 @@ constructor(
    override fun updateOrientation(insets: WindowInsets) {}

    override fun createScreenshotDropInAnimation(screenRect: Rect, showFlash: Boolean): Animator {
        return animationController.getEntranceAnimation()
        val entrance = animationController.getEntranceAnimation(screenRect, showFlash)
        // reset the timeout when animation finishes
        entrance.doOnEnd { callbacks?.onUserInteraction() }
        return entrance
    }

    override fun addQuickShareChip(quickShareAction: Notification.Action) {}
+97 −14
Original line number Diff line number Diff line
@@ -17,9 +17,16 @@
package com.android.systemui.screenshot.ui

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.graphics.PointF
import android.graphics.Rect
import android.util.MathUtils
import android.view.View
import android.view.animation.AnimationUtils
import androidx.core.animation.doOnEnd
import androidx.core.animation.doOnStart
import com.android.systemui.res.R
import kotlin.math.abs
import kotlin.math.max
@@ -27,23 +34,57 @@ import kotlin.math.sign

class ScreenshotAnimationController(private val view: ScreenshotShelfView) {
    private var animator: Animator? = null
    private val screenshotPreview = view.requireViewById<View>(R.id.screenshot_preview)
    private val flashView = view.requireViewById<View>(R.id.screenshot_flash)
    private val actionContainer = view.requireViewById<View>(R.id.actions_container_background)
    private val fastOutSlowIn =
        AnimationUtils.loadInterpolator(view.context, android.R.interpolator.fast_out_slow_in)
    private val staticUI =
        listOf<View>(
            view.requireViewById(R.id.screenshot_preview_border),
            view.requireViewById(R.id.actions_container_background),
            view.requireViewById(R.id.screenshot_badge),
            view.requireViewById(R.id.screenshot_dismiss_button)
        )

    fun getEntranceAnimation(bounds: Rect, showFlash: Boolean): Animator {
        val entranceAnimation = AnimatorSet()

        val previewAnimator = getPreviewAnimator(bounds)

    fun getEntranceAnimation(): Animator {
        val animator = ValueAnimator.ofFloat(0f, 1f)
        animator.addUpdateListener { view.alpha = it.animatedFraction }
        animator.addListener(
            object : AnimatorListenerAdapter() {
                override fun onAnimationStart(animator: Animator) {
                    view.alpha = 0f
        if (showFlash) {
            val flashInAnimator =
                ObjectAnimator.ofFloat(flashView, "alpha", 0f, 1f).apply {
                    duration = FLASH_IN_DURATION_MS
                    interpolator = fastOutSlowIn
                }
                override fun onAnimationEnd(animator: Animator) {
                    view.alpha = 1f
            val flashOutAnimator =
                ObjectAnimator.ofFloat(flashView, "alpha", 1f, 0f).apply {
                    duration = FLASH_OUT_DURATION_MS
                    interpolator = fastOutSlowIn
                }
            flashInAnimator.doOnStart { flashView.visibility = View.VISIBLE }
            flashOutAnimator.doOnEnd { flashView.visibility = View.GONE }
            entranceAnimation.play(flashOutAnimator).after(flashInAnimator)
            entranceAnimation.play(previewAnimator).with(flashOutAnimator)
            entranceAnimation.doOnStart { screenshotPreview.visibility = View.INVISIBLE }
        }

        val fadeInAnimator = ValueAnimator.ofFloat(0f, 1f)
        fadeInAnimator.addUpdateListener {
            for (child in staticUI) {
                child.alpha = it.animatedValue as Float
            }
        }
        entranceAnimation.play(fadeInAnimator).after(previewAnimator)
        entranceAnimation.doOnStart {
            for (child in staticUI) {
                child.alpha = 0f
            }
        )
        this.animator = animator
        return animator
        }

        this.animator = entranceAnimation
        return entranceAnimation
    }

    fun getSwipeReturnAnimation(): Animator {
@@ -81,11 +122,49 @@ class ScreenshotAnimationController(private val view: ScreenshotShelfView) {
        animator?.cancel()
    }

    private fun getPreviewAnimator(bounds: Rect): Animator {
        val targetPosition = Rect()
        screenshotPreview.getHitRect(targetPosition)
        val startXScale = bounds.width() / targetPosition.width().toFloat()
        val startYScale = bounds.height() / targetPosition.height().toFloat()
        val startPos = PointF(bounds.exactCenterX(), bounds.exactCenterY())
        val endPos = PointF(targetPosition.exactCenterX(), targetPosition.exactCenterY())

        val previewYAnimator =
            ValueAnimator.ofFloat(startPos.y, endPos.y).apply {
                duration = PREVIEW_Y_ANIMATION_DURATION_MS
                interpolator = fastOutSlowIn
            }
        previewYAnimator.addUpdateListener {
            val progress = it.animatedValue as Float
            screenshotPreview.y = progress - screenshotPreview.height / 2f
        }
        // scale animation starts/finishes at the same time as x placement
        val previewXAndScaleAnimator =
            ValueAnimator.ofFloat(0f, 1f).apply {
                duration = PREVIEW_X_ANIMATION_DURATION_MS
                interpolator = fastOutSlowIn
            }
        previewXAndScaleAnimator.addUpdateListener {
            val t = it.animatedFraction
            screenshotPreview.scaleX = MathUtils.lerp(startXScale, 1f, t)
            screenshotPreview.scaleY = MathUtils.lerp(startYScale, 1f, t)
            screenshotPreview.x =
                MathUtils.lerp(startPos.x, endPos.x, t) - screenshotPreview.width / 2f
        }

        val previewAnimator = AnimatorSet()
        previewAnimator.play(previewXAndScaleAnimator).with(previewYAnimator)

        previewAnimator.doOnStart { screenshotPreview.visibility = View.VISIBLE }
        return previewAnimator
    }

    private fun getAdjustedVelocity(requestedVelocity: Float?): Float {
        return if (requestedVelocity == null) {
            val isLTR = view.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR
            // dismiss to the left in LTR locales, to the right in RTL
            if (isLTR) -1 * MINIMUM_VELOCITY else MINIMUM_VELOCITY
            if (isLTR) -MINIMUM_VELOCITY else MINIMUM_VELOCITY
        } else {
            sign(requestedVelocity) * max(MINIMUM_VELOCITY, abs(requestedVelocity))
        }
@@ -93,5 +172,9 @@ class ScreenshotAnimationController(private val view: ScreenshotShelfView) {

    companion object {
        private const val MINIMUM_VELOCITY = 1.5f // pixels per second
        private const val FLASH_IN_DURATION_MS: Long = 133
        private const val FLASH_OUT_DURATION_MS: Long = 217
        private const val PREVIEW_X_ANIMATION_DURATION_MS: Long = 234
        private const val PREVIEW_Y_ANIMATION_DURATION_MS: Long = 500
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -24,7 +24,7 @@ import kotlin.math.abs

class SwipeGestureListener(
    private val view: View,
    private val onDismiss: (Float) -> Unit,
    private val onDismiss: (Float?) -> Unit,
    private val onCancel: () -> Unit
) {
    private val velocityTracker = VelocityTracker.obtain()
+6 −4
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ import com.android.systemui.screenshot.ui.ScreenshotShelfView
import com.android.systemui.screenshot.ui.SwipeGestureListener
import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel
import com.android.systemui.util.children
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

object ScreenshotShelfViewBinder {
@@ -60,7 +61,8 @@ object ScreenshotShelfViewBinder {
            onDismissalRequested(ScreenshotEvent.SCREENSHOT_EXPLICIT_DISMISSAL, null)
        }

        view.repeatWhenAttached {
        // use immediate dispatcher to ensure screenshot bitmap is set before animation
        view.repeatWhenAttached(Dispatchers.Main.immediate) {
            lifecycleScope.launch {
                repeatOnLifecycle(Lifecycle.State.STARTED) {
                    launch {
@@ -96,9 +98,9 @@ object ScreenshotShelfViewBinder {
                            // ID is unique.
                            val newIds = visibleActions.map { it.id }

                            for (view in actionsContainer.children.toList()) {
                                if (view.tag !in newIds) {
                                    actionsContainer.removeView(view)
                            for (child in actionsContainer.children.toList()) {
                                if (child.tag !in newIds) {
                                    actionsContainer.removeView(child)
                                }
                            }