From e5d6da0ccfa2b39082225de4c561a6ccfa2913f7 Mon Sep 17 00:00:00 2001 From: Miranda Kephart Date: Thu, 18 Apr 2024 15:01:08 -0400 Subject: 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 --- packages/SystemUI/res/layout/screenshot_shelf.xml | 7 ++ .../screenshot/ScreenshotShelfViewProxy.kt | 6 +- .../screenshot/ui/ScreenshotAnimationController.kt | 111 ++++++++++++++++++--- .../systemui/screenshot/ui/SwipeGestureListener.kt | 2 +- .../ui/binder/ScreenshotShelfViewBinder.kt | 10 +- 5 files changed, 116 insertions(+), 20 deletions(-) diff --git a/packages/SystemUI/res/layout/screenshot_shelf.xml b/packages/SystemUI/res/layout/screenshot_shelf.xml index 6a5b999f5444..26d3f4325a78 100644 --- a/packages/SystemUI/res/layout/screenshot_shelf.xml +++ b/packages/SystemUI/res/layout/screenshot_shelf.xml @@ -147,4 +147,11 @@ + diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt index 303eb78d73d8..12a3daa57517 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt @@ -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) {} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt index 9cf8ca5f602d..3f4f74b5398f 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt @@ -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(R.id.screenshot_preview) + private val flashView = view.requireViewById(R.id.screenshot_flash) private val actionContainer = view.requireViewById(R.id.actions_container_background) + private val fastOutSlowIn = + AnimationUtils.loadInterpolator(view.context, android.R.interpolator.fast_out_slow_in) + private val staticUI = + listOf( + 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 } - ) - this.animator = animator - return animator + } + entranceAnimation.play(fadeInAnimator).after(previewAnimator) + entranceAnimation.doOnStart { + for (child in staticUI) { + child.alpha = 0f + } + } + + 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 } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/SwipeGestureListener.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/SwipeGestureListener.kt index 727475787f3e..61d448960f98 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ui/SwipeGestureListener.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/SwipeGestureListener.kt @@ -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() diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt index 3e352951f44c..3376b8c84826 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt @@ -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) } } -- cgit v1.2.3-59-g8ed1b