Split ActivityLaunchAnimator (1/2)
This CL splits ActivityLaunchAnimator by extracting common logic into LaunchAnimator. The goal of this CL is to be able to reuse the LaunchAnimator logic for dialog launches. This CL also improves GhostedViewLaunchAnimator such that it also works with: * launch containers that are not located at (0, 0). * ghosted views that move. Test: atest ActivityLaunchAnimatorTest Bug: 193634619 Change-Id: Ic05603600d4a7d4e27182c79bef54286087d00d0
This commit is contained in:
@@ -165,6 +165,10 @@ public final class MathUtils {
|
||||
return start + (stop - start) * amount;
|
||||
}
|
||||
|
||||
public static float lerp(int start, int stop, float amount) {
|
||||
return lerp((float) start, (float) stop, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the interpolation scalar (s) that satisfies the equation: {@code value = }{@link
|
||||
* #lerp}{@code (a, b, s)}
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.systemui.animation
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.animation.ValueAnimator
|
||||
import android.app.ActivityManager
|
||||
import android.app.ActivityTaskManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.TaskInfo
|
||||
import android.content.Context
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffXfermode
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Looper
|
||||
import android.os.RemoteException
|
||||
import android.util.Log
|
||||
import android.util.MathUtils
|
||||
import android.view.IRemoteAnimationFinishedCallback
|
||||
import android.view.IRemoteAnimationRunner
|
||||
import android.view.RemoteAnimationAdapter
|
||||
@@ -26,7 +34,6 @@ import android.view.SyncRtSurfaceTransactionApplier
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.view.animation.AnimationUtils
|
||||
import android.view.animation.PathInterpolator
|
||||
import com.android.internal.annotations.VisibleForTesting
|
||||
import com.android.internal.policy.ScreenDecorationsUtils
|
||||
@@ -38,52 +45,23 @@ private const val TAG = "ActivityLaunchAnimator"
|
||||
* A class that allows activities to be started in a seamless way from a view that is transforming
|
||||
* nicely into the starting window.
|
||||
*/
|
||||
class ActivityLaunchAnimator(
|
||||
private val callback: Callback,
|
||||
context: Context
|
||||
) {
|
||||
class ActivityLaunchAnimator(private val launchAnimator: LaunchAnimator) {
|
||||
companion object {
|
||||
private const val DEBUG = false
|
||||
const val ANIMATION_DURATION = 500L
|
||||
private const val ANIMATION_DURATION_FADE_OUT_CONTENT = 150L
|
||||
private const val ANIMATION_DURATION_FADE_IN_WINDOW = 183L
|
||||
private const val ANIMATION_DELAY_FADE_IN_WINDOW = ANIMATION_DURATION_FADE_OUT_CONTENT
|
||||
private const val ANIMATION_DURATION_NAV_FADE_IN = 266L
|
||||
private const val ANIMATION_DURATION_NAV_FADE_OUT = 133L
|
||||
private const val ANIMATION_DELAY_NAV_FADE_IN =
|
||||
ANIMATION_DURATION - ANIMATION_DURATION_NAV_FADE_IN
|
||||
LaunchAnimator.ANIMATION_DURATION - ANIMATION_DURATION_NAV_FADE_IN
|
||||
private const val LAUNCH_TIMEOUT = 1000L
|
||||
|
||||
@JvmField val CONTENT_FADE_OUT_INTERPOLATOR = PathInterpolator(0f, 0f, 0.2f, 1f)
|
||||
private val WINDOW_FADE_IN_INTERPOLATOR = PathInterpolator(0f, 0f, 0.6f, 1f)
|
||||
private val NAV_FADE_IN_INTERPOLATOR = PathInterpolator(0f, 0f, 0f, 1f)
|
||||
private val NAV_FADE_OUT_INTERPOLATOR = PathInterpolator(0.2f, 0f, 1f, 1f)
|
||||
|
||||
private val SRC_MODE = PorterDuffXfermode(PorterDuff.Mode.SRC)
|
||||
|
||||
/**
|
||||
* Given the [linearProgress] of a launch animation, return the linear progress of the
|
||||
* sub-animation starting [delay] ms after the launch animation and that lasts [duration].
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getProgress(linearProgress: Float, delay: Long, duration: Long): Float {
|
||||
return MathUtils.constrain(
|
||||
(linearProgress * ANIMATION_DURATION - delay) / duration,
|
||||
0.0f,
|
||||
1.0f
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** The interpolator used for the width, height, Y position and corner radius. */
|
||||
private val animationInterpolator = AnimationUtils.loadInterpolator(context,
|
||||
R.interpolator.launch_animation_interpolator_y)
|
||||
|
||||
/** The interpolator used for the X position. */
|
||||
private val animationInterpolatorX = AnimationUtils.loadInterpolator(context,
|
||||
R.interpolator.launch_animation_interpolator_x)
|
||||
|
||||
private val cornerRadii = FloatArray(8)
|
||||
/**
|
||||
* The callback of this animator. This should be set before any call to
|
||||
* [start(Pending)IntentWithAnimation].
|
||||
*/
|
||||
var callback: Callback? = null
|
||||
|
||||
/**
|
||||
* Start an intent and animate the opening window. The intent will be started by running
|
||||
@@ -119,6 +97,8 @@ class ActivityLaunchAnimator(
|
||||
return
|
||||
}
|
||||
|
||||
val callback = this.callback ?: throw IllegalStateException(
|
||||
"ActivityLaunchAnimator.callback must be set before using this animator")
|
||||
val runner = Runner(controller)
|
||||
val hideKeyguardWithAnimation = callback.isOnKeyguard() && !showOverLockscreen
|
||||
|
||||
@@ -126,9 +106,9 @@ class ActivityLaunchAnimator(
|
||||
// keyguard with the animation
|
||||
val animationAdapter = if (!hideKeyguardWithAnimation) {
|
||||
RemoteAnimationAdapter(
|
||||
runner,
|
||||
ANIMATION_DURATION,
|
||||
ANIMATION_DURATION - 150 /* statusBarTransitionDelay */
|
||||
runner,
|
||||
LaunchAnimator.ANIMATION_DURATION,
|
||||
LaunchAnimator.ANIMATION_DURATION - 150 /* statusBarTransitionDelay */
|
||||
)
|
||||
} else {
|
||||
null
|
||||
@@ -150,10 +130,10 @@ class ActivityLaunchAnimator(
|
||||
// Only animate if the app is not already on top and will be opened, unless we are on the
|
||||
// keyguard.
|
||||
val willAnimate =
|
||||
launchResult == ActivityManager.START_TASK_TO_FRONT ||
|
||||
launchResult == ActivityManager.START_SUCCESS ||
|
||||
(launchResult == ActivityManager.START_DELIVERED_TO_TOP &&
|
||||
hideKeyguardWithAnimation)
|
||||
launchResult == ActivityManager.START_TASK_TO_FRONT ||
|
||||
launchResult == ActivityManager.START_SUCCESS ||
|
||||
(launchResult == ActivityManager.START_DELIVERED_TO_TOP &&
|
||||
hideKeyguardWithAnimation)
|
||||
|
||||
Log.i(TAG, "launchResult=$launchResult willAnimate=$willAnimate " +
|
||||
"hideKeyguardWithAnimation=$hideKeyguardWithAnimation")
|
||||
@@ -234,7 +214,7 @@ class ActivityLaunchAnimator(
|
||||
*
|
||||
* Note that all callbacks (onXXX methods) are all called on the main thread.
|
||||
*/
|
||||
interface Controller {
|
||||
interface Controller : LaunchAnimator.Controller {
|
||||
companion object {
|
||||
/**
|
||||
* Return a [Controller] that will animate and expand [view] into the opening window.
|
||||
@@ -258,53 +238,12 @@ class ActivityLaunchAnimator(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The container in which the view that started the intent will be animating together with
|
||||
* the opening window.
|
||||
*
|
||||
* This will be used to:
|
||||
* - Get the associated [Context].
|
||||
* - Compute whether we are expanding fully above the current window.
|
||||
* - Apply surface transactions in sync with RenderThread.
|
||||
*
|
||||
* This container can be changed to force this [Controller] to animate the expanding view
|
||||
* inside a different location, for instance to ensure correct layering during the
|
||||
* animation.
|
||||
*/
|
||||
var launchContainer: ViewGroup
|
||||
|
||||
/**
|
||||
* Return the [State] of the view that will be animated. We will animate from this state to
|
||||
* the final window state.
|
||||
*
|
||||
* Note: This state will be mutated and passed to [onLaunchAnimationProgress] during the
|
||||
* animation.
|
||||
*/
|
||||
fun createAnimatorState(): State
|
||||
|
||||
/**
|
||||
* The intent was started. If [willAnimate] is false, nothing else will happen and the
|
||||
* animation will not be started.
|
||||
*/
|
||||
fun onIntentStarted(willAnimate: Boolean) {}
|
||||
|
||||
/**
|
||||
* The animation started. This is typically used to initialize any additional resource
|
||||
* needed for the animation. [isExpandingFullyAbove] will be true if the window is expanding
|
||||
* fully above the [root view][getRootView].
|
||||
*/
|
||||
fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {}
|
||||
|
||||
/** The animation made progress and the expandable view [state] should be updated. */
|
||||
fun onLaunchAnimationProgress(state: State, progress: Float, linearProgress: Float) {}
|
||||
|
||||
/**
|
||||
* The animation ended. This will be called *if and only if* [onLaunchAnimationStart] was
|
||||
* called previously. This is typically used to clean up the resources initialized when the
|
||||
* animation was started.
|
||||
*/
|
||||
fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {}
|
||||
|
||||
/**
|
||||
* The animation was cancelled. Note that [onLaunchAnimationEnd] will still be called after
|
||||
* this if the animation was already started, i.e. if [onLaunchAnimationStart] was called
|
||||
@@ -313,66 +252,11 @@ class ActivityLaunchAnimator(
|
||||
fun onLaunchAnimationCancelled() {}
|
||||
}
|
||||
|
||||
/** The state of an expandable view during an [ActivityLaunchAnimator] animation. */
|
||||
open class State(
|
||||
/** The position of the view in screen space coordinates. */
|
||||
var top: Int,
|
||||
var bottom: Int,
|
||||
var left: Int,
|
||||
var right: Int,
|
||||
|
||||
var topCornerRadius: Float = 0f,
|
||||
var bottomCornerRadius: Float = 0f
|
||||
) {
|
||||
private val startTop = top
|
||||
private val startBottom = bottom
|
||||
private val startLeft = left
|
||||
private val startRight = right
|
||||
private val startWidth = width
|
||||
private val startHeight = height
|
||||
val startCenterX = centerX
|
||||
val startCenterY = centerY
|
||||
|
||||
val width: Int
|
||||
get() = right - left
|
||||
|
||||
val height: Int
|
||||
get() = bottom - top
|
||||
|
||||
open val topChange: Int
|
||||
get() = top - startTop
|
||||
|
||||
open val bottomChange: Int
|
||||
get() = bottom - startBottom
|
||||
|
||||
val leftChange: Int
|
||||
get() = left - startLeft
|
||||
|
||||
val rightChange: Int
|
||||
get() = right - startRight
|
||||
|
||||
val widthRatio: Float
|
||||
get() = width.toFloat() / startWidth
|
||||
|
||||
val heightRatio: Float
|
||||
get() = height.toFloat() / startHeight
|
||||
|
||||
val centerX: Float
|
||||
get() = left + width / 2f
|
||||
|
||||
val centerY: Float
|
||||
get() = top + height / 2f
|
||||
|
||||
/** Whether the expanded view should be visible or hidden. */
|
||||
var visible: Boolean = true
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
inner class Runner(private val controller: Controller) : IRemoteAnimationRunner.Stub() {
|
||||
private val launchContainer = controller.launchContainer
|
||||
private val context = launchContainer.context
|
||||
private val transactionApplier = SyncRtSurfaceTransactionApplier(launchContainer)
|
||||
private var animator: ValueAnimator? = null
|
||||
|
||||
private val matrix = Matrix()
|
||||
private val invertMatrix = Matrix()
|
||||
@@ -380,6 +264,7 @@ class ActivityLaunchAnimator(
|
||||
private var windowCropF = RectF()
|
||||
private var timedOut = false
|
||||
private var cancelled = false
|
||||
private var animation: LaunchAnimator.Animation? = null
|
||||
|
||||
// A timeout to cancel the remote animation if it is not started within X milliseconds after
|
||||
// the intent was started.
|
||||
@@ -429,7 +314,7 @@ class ActivityLaunchAnimator(
|
||||
nonApps: Array<out RemoteAnimationTarget>?,
|
||||
iCallback: IRemoteAnimationFinishedCallback?
|
||||
) {
|
||||
if (DEBUG) {
|
||||
if (LaunchAnimator.DEBUG) {
|
||||
Log.d(TAG, "Remote animation started")
|
||||
}
|
||||
|
||||
@@ -449,36 +334,20 @@ class ActivityLaunchAnimator(
|
||||
it.windowType == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR
|
||||
}
|
||||
|
||||
// Start state.
|
||||
val state = controller.createAnimatorState()
|
||||
|
||||
val startTop = state.top
|
||||
val startBottom = state.bottom
|
||||
val startLeft = state.left
|
||||
val startRight = state.right
|
||||
val startXCenter = (startLeft + startRight) / 2f
|
||||
val startWidth = startRight - startLeft
|
||||
|
||||
val startTopCornerRadius = state.topCornerRadius
|
||||
val startBottomCornerRadius = state.bottomCornerRadius
|
||||
|
||||
// End state.
|
||||
val windowBounds = window.screenSpaceBounds
|
||||
val endTop = windowBounds.top
|
||||
val endBottom = windowBounds.bottom
|
||||
val endLeft = windowBounds.left
|
||||
val endRight = windowBounds.right
|
||||
val endXCenter = (endLeft + endRight) / 2f
|
||||
val endWidth = endRight - endLeft
|
||||
val endState = LaunchAnimator.State(
|
||||
top = windowBounds.top,
|
||||
bottom = windowBounds.bottom,
|
||||
left = windowBounds.left,
|
||||
right = windowBounds.right
|
||||
)
|
||||
val callback = this@ActivityLaunchAnimator.callback!!
|
||||
val windowBackgroundColor = callback.getBackgroundColor(window.taskInfo)
|
||||
|
||||
// TODO(b/184121838): Ensure that we are launching on the same screen.
|
||||
val rootViewLocation = launchContainer.locationOnScreen
|
||||
val isExpandingFullyAbove = endTop <= rootViewLocation[1] &&
|
||||
endBottom >= rootViewLocation[1] + launchContainer.height &&
|
||||
endLeft <= rootViewLocation[0] &&
|
||||
endRight >= rootViewLocation[0] + launchContainer.width
|
||||
|
||||
// TODO(b/184121838): We should somehow get the top and bottom radius of the window.
|
||||
// TODO(b/184121838): We should somehow get the top and bottom radius of the window
|
||||
// instead of recomputing isExpandingFullyAbove here.
|
||||
val isExpandingFullyAbove =
|
||||
launchAnimator.isExpandingFullyAbove(controller.launchContainer, endState)
|
||||
val endRadius = if (isExpandingFullyAbove) {
|
||||
// Most of the time, expanding fully above the root view means expanding in full
|
||||
// screen.
|
||||
@@ -488,97 +357,40 @@ class ActivityLaunchAnimator(
|
||||
// a radius of 0.
|
||||
0f
|
||||
}
|
||||
endState.topCornerRadius = endRadius
|
||||
endState.bottomCornerRadius = endRadius
|
||||
|
||||
// We add an extra layer with the same color as the app splash screen background color,
|
||||
// which is usually the same color of the app background. We first fade in this layer
|
||||
// to hide the expanding view, then we fade it out with SRC mode to draw a hole in the
|
||||
// launch container and reveal the opening window.
|
||||
val windowBackgroundColor = callback.getBackgroundColor(window.taskInfo)
|
||||
val windowBackgroundLayer = GradientDrawable().apply {
|
||||
setColor(windowBackgroundColor)
|
||||
alpha = 0
|
||||
}
|
||||
|
||||
// Update state.
|
||||
val animator = ValueAnimator.ofFloat(0f, 1f)
|
||||
this.animator = animator
|
||||
animator.duration = ANIMATION_DURATION
|
||||
animator.interpolator = Interpolators.LINEAR
|
||||
|
||||
val launchContainerOverlay = launchContainer.overlay
|
||||
animator.addListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationStart(animation: Animator?, isReverse: Boolean) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Animation started")
|
||||
}
|
||||
|
||||
// We animate the opening window and delegate the view expansion to [this.controller].
|
||||
val delegate = this.controller
|
||||
val controller = object : LaunchAnimator.Controller by delegate {
|
||||
override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
|
||||
callback.setBlursDisabledForAppLaunch(true)
|
||||
controller.onLaunchAnimationStart(isExpandingFullyAbove)
|
||||
|
||||
// Add the drawable to the launch container overlay. Overlays always draw
|
||||
// drawables after views, so we know that it will be drawn above any view added
|
||||
// by the controller.
|
||||
launchContainerOverlay.add(windowBackgroundLayer)
|
||||
delegate.onLaunchAnimationStart(isExpandingFullyAbove)
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Animation ended")
|
||||
}
|
||||
|
||||
override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
|
||||
callback.setBlursDisabledForAppLaunch(false)
|
||||
iCallback?.invoke()
|
||||
controller.onLaunchAnimationEnd(isExpandingFullyAbove)
|
||||
launchContainerOverlay.remove(windowBackgroundLayer)
|
||||
}
|
||||
})
|
||||
|
||||
animator.addUpdateListener { animation ->
|
||||
if (cancelled) {
|
||||
return@addUpdateListener
|
||||
delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
|
||||
}
|
||||
|
||||
val linearProgress = animation.animatedFraction
|
||||
val progress = animationInterpolator.getInterpolation(linearProgress)
|
||||
val xProgress = animationInterpolatorX.getInterpolation(linearProgress)
|
||||
val xCenter = MathUtils.lerp(startXCenter, endXCenter, xProgress)
|
||||
val halfWidth = lerp(startWidth, endWidth, progress) / 2
|
||||
|
||||
state.top = lerp(startTop, endTop, progress).roundToInt()
|
||||
state.bottom = lerp(startBottom, endBottom, progress).roundToInt()
|
||||
state.left = (xCenter - halfWidth).roundToInt()
|
||||
state.right = (xCenter + halfWidth).roundToInt()
|
||||
|
||||
state.topCornerRadius = MathUtils.lerp(startTopCornerRadius, endRadius, progress)
|
||||
state.bottomCornerRadius =
|
||||
MathUtils.lerp(startBottomCornerRadius, endRadius, progress)
|
||||
|
||||
// The expanding view can/should be hidden once it is completely coverred by the
|
||||
// windowBackgroundLayer.
|
||||
state.visible =
|
||||
getProgress(linearProgress, 0, ANIMATION_DURATION_FADE_OUT_CONTENT) < 1
|
||||
|
||||
applyStateToWindow(window, state)
|
||||
applyStateToWindowBackgroundLayer(windowBackgroundLayer, state, linearProgress)
|
||||
navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) }
|
||||
|
||||
// If we started expanding the view, we make it 1 pixel smaller on all sides to
|
||||
// avoid artefacts on the corners caused by anti-aliasing of the view background and
|
||||
// the window background layer.
|
||||
if (state.top != startTop && state.left != startLeft &&
|
||||
state.bottom != startBottom && state.right != startRight) {
|
||||
state.top += 1
|
||||
state.left += 1
|
||||
state.right -= 1
|
||||
state.bottom -= 1
|
||||
override fun onLaunchAnimationProgress(
|
||||
state: LaunchAnimator.State,
|
||||
progress: Float,
|
||||
linearProgress: Float
|
||||
) {
|
||||
applyStateToWindow(window, state)
|
||||
navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) }
|
||||
delegate.onLaunchAnimationProgress(state, progress, linearProgress)
|
||||
}
|
||||
controller.onLaunchAnimationProgress(state, progress, linearProgress)
|
||||
}
|
||||
|
||||
animator.start()
|
||||
// We draw a hole when the additional layer is fading out to reveal the opening window.
|
||||
animation = launchAnimator.startAnimation(
|
||||
controller, endState, windowBackgroundColor, drawHole = true)
|
||||
}
|
||||
|
||||
private fun applyStateToWindow(window: RemoteAnimationTarget, state: State) {
|
||||
private fun applyStateToWindow(window: RemoteAnimationTarget, state: LaunchAnimator.State) {
|
||||
val screenBounds = window.screenSpaceBounds
|
||||
val centerX = (screenBounds.left + screenBounds.right) / 2f
|
||||
val centerY = (screenBounds.top + screenBounds.bottom) / 2f
|
||||
@@ -632,48 +444,13 @@ class ActivityLaunchAnimator(
|
||||
transactionApplier.scheduleApply(params)
|
||||
}
|
||||
|
||||
private fun applyStateToWindowBackgroundLayer(
|
||||
drawable: GradientDrawable,
|
||||
state: State,
|
||||
linearProgress: Float
|
||||
) {
|
||||
// Update position.
|
||||
drawable.setBounds(state.left, state.top, state.right, state.bottom)
|
||||
|
||||
// Update radius.
|
||||
cornerRadii[0] = state.topCornerRadius
|
||||
cornerRadii[1] = state.topCornerRadius
|
||||
cornerRadii[2] = state.topCornerRadius
|
||||
cornerRadii[3] = state.topCornerRadius
|
||||
cornerRadii[4] = state.bottomCornerRadius
|
||||
cornerRadii[5] = state.bottomCornerRadius
|
||||
cornerRadii[6] = state.bottomCornerRadius
|
||||
cornerRadii[7] = state.bottomCornerRadius
|
||||
drawable.cornerRadii = cornerRadii
|
||||
|
||||
// We first fade in the background layer to hide the expanding view, then fade it out
|
||||
// with SRC mode to draw a hole punch in the status bar and reveal the opening window.
|
||||
val fadeInProgress = getProgress(linearProgress, 0, ANIMATION_DURATION_FADE_OUT_CONTENT)
|
||||
if (fadeInProgress < 1) {
|
||||
val alpha = CONTENT_FADE_OUT_INTERPOLATOR.getInterpolation(fadeInProgress)
|
||||
drawable.alpha = (alpha * 0xFF).roundToInt()
|
||||
drawable.setXfermode(null)
|
||||
} else {
|
||||
val fadeOutProgress = getProgress(linearProgress,
|
||||
ANIMATION_DELAY_FADE_IN_WINDOW, ANIMATION_DURATION_FADE_IN_WINDOW)
|
||||
val alpha = 1 - WINDOW_FADE_IN_INTERPOLATOR.getInterpolation(fadeOutProgress)
|
||||
drawable.alpha = (alpha * 0xFF).roundToInt()
|
||||
drawable.setXfermode(SRC_MODE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyStateToNavigationBar(
|
||||
navigationBar: RemoteAnimationTarget,
|
||||
state: State,
|
||||
state: LaunchAnimator.State,
|
||||
linearProgress: Float
|
||||
) {
|
||||
val fadeInProgress = getProgress(linearProgress, ANIMATION_DELAY_NAV_FADE_IN,
|
||||
ANIMATION_DURATION_NAV_FADE_OUT)
|
||||
val fadeInProgress = LaunchAnimator.getProgress(linearProgress,
|
||||
ANIMATION_DELAY_NAV_FADE_IN, ANIMATION_DURATION_NAV_FADE_OUT)
|
||||
|
||||
val params = SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(navigationBar.leash)
|
||||
if (fadeInProgress > 0) {
|
||||
@@ -682,13 +459,13 @@ class ActivityLaunchAnimator(
|
||||
0f, (state.top - navigationBar.sourceContainerBounds.top).toFloat())
|
||||
windowCrop.set(state.left, 0, state.right, state.height)
|
||||
params
|
||||
.withAlpha(NAV_FADE_IN_INTERPOLATOR.getInterpolation(fadeInProgress))
|
||||
.withMatrix(matrix)
|
||||
.withWindowCrop(windowCrop)
|
||||
.withVisibility(true)
|
||||
.withAlpha(NAV_FADE_IN_INTERPOLATOR.getInterpolation(fadeInProgress))
|
||||
.withMatrix(matrix)
|
||||
.withWindowCrop(windowCrop)
|
||||
.withVisibility(true)
|
||||
} else {
|
||||
val fadeOutProgress = getProgress(linearProgress, 0,
|
||||
ANIMATION_DURATION_NAV_FADE_OUT)
|
||||
val fadeOutProgress = LaunchAnimator.getProgress(linearProgress, 0,
|
||||
ANIMATION_DURATION_NAV_FADE_OUT)
|
||||
params.withAlpha(1f - NAV_FADE_OUT_INTERPOLATOR.getInterpolation(fadeOutProgress))
|
||||
}
|
||||
|
||||
@@ -714,7 +491,7 @@ class ActivityLaunchAnimator(
|
||||
cancelled = true
|
||||
removeTimeout()
|
||||
context.mainExecutor.execute {
|
||||
animator?.cancel()
|
||||
animation?.cancel()
|
||||
controller.onLaunchAnimationCancelled()
|
||||
}
|
||||
}
|
||||
@@ -726,9 +503,5 @@ class ActivityLaunchAnimator(
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun lerp(start: Int, stop: Int, amount: Float): Float {
|
||||
return MathUtils.lerp(start.toFloat(), stop.toFloat(), amount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.systemui.animation
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,24 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.systemui.animation
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.ColorFilter
|
||||
import android.graphics.Insets
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.Rect
|
||||
@@ -42,6 +59,7 @@ open class GhostedViewLaunchAnimatorController(
|
||||
override var launchContainer = ghostedView.rootView as ViewGroup
|
||||
private val launchContainerOverlay: ViewGroupOverlay
|
||||
get() = launchContainer.overlay
|
||||
private val launchContainerLocation = IntArray(2)
|
||||
|
||||
/** The ghost view that is drawn and animated instead of the ghosted view. */
|
||||
private var ghostView: GhostView? = null
|
||||
@@ -59,8 +77,12 @@ open class GhostedViewLaunchAnimatorController(
|
||||
* [backgroundView].
|
||||
*/
|
||||
private var backgroundDrawable: WrappedDrawable? = null
|
||||
private val backgroundInsets by lazy { getBackground()?.opticalInsets ?: Insets.NONE }
|
||||
private var startBackgroundAlpha: Int = 0xFF
|
||||
|
||||
private val ghostedViewLocation = IntArray(2)
|
||||
private val ghostedViewState = LaunchAnimator.State()
|
||||
|
||||
/**
|
||||
* Return the background of the [ghostedView]. This background will be used to draw the
|
||||
* background of the background view that is expanding up to the final animation position. This
|
||||
@@ -103,16 +125,24 @@ open class GhostedViewLaunchAnimatorController(
|
||||
return gradient.cornerRadii?.get(CORNER_RADIUS_BOTTOM_INDEX) ?: gradient.cornerRadius
|
||||
}
|
||||
|
||||
override fun createAnimatorState(): ActivityLaunchAnimator.State {
|
||||
val location = ghostedView.locationOnScreen
|
||||
return ActivityLaunchAnimator.State(
|
||||
top = location[1],
|
||||
bottom = location[1] + ghostedView.height,
|
||||
left = location[0],
|
||||
right = location[0] + ghostedView.width,
|
||||
override fun createAnimatorState(): LaunchAnimator.State {
|
||||
val state = LaunchAnimator.State(
|
||||
topCornerRadius = getCurrentTopCornerRadius(),
|
||||
bottomCornerRadius = getCurrentBottomCornerRadius()
|
||||
)
|
||||
fillGhostedViewState(state)
|
||||
return state
|
||||
}
|
||||
|
||||
fun fillGhostedViewState(state: LaunchAnimator.State) {
|
||||
// For the animation we are interested in the area that has a non transparent background,
|
||||
// so we have to take the optical insets into account.
|
||||
ghostedView.getLocationOnScreen(ghostedViewLocation)
|
||||
val insets = backgroundInsets
|
||||
state.top = ghostedViewLocation[1] + insets.top
|
||||
state.bottom = ghostedViewLocation[1] + ghostedView.height - insets.bottom
|
||||
state.left = ghostedViewLocation[0] + insets.left
|
||||
state.right = ghostedViewLocation[0] + ghostedView.width - insets.right
|
||||
}
|
||||
|
||||
override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
|
||||
@@ -144,7 +174,7 @@ open class GhostedViewLaunchAnimatorController(
|
||||
}
|
||||
|
||||
override fun onLaunchAnimationProgress(
|
||||
state: ActivityLaunchAnimator.State,
|
||||
state: LaunchAnimator.State,
|
||||
progress: Float,
|
||||
linearProgress: Float
|
||||
) {
|
||||
@@ -162,19 +192,47 @@ open class GhostedViewLaunchAnimatorController(
|
||||
return
|
||||
}
|
||||
|
||||
val scale = min(state.widthRatio, state.heightRatio)
|
||||
ghostViewMatrix.setValues(initialGhostViewMatrixValues)
|
||||
ghostViewMatrix.postScale(scale, scale, state.startCenterX, state.startCenterY)
|
||||
// The ghost and backgrounds views were made invisible earlier. That can for instance happen
|
||||
// when animating a dialog into a view.
|
||||
if (ghostView.visibility == View.INVISIBLE) {
|
||||
ghostView.visibility = View.VISIBLE
|
||||
backgroundView.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
fillGhostedViewState(ghostedViewState)
|
||||
val leftChange = state.left - ghostedViewState.left
|
||||
val rightChange = state.right - ghostedViewState.right
|
||||
val topChange = state.top - ghostedViewState.top
|
||||
val bottomChange = state.bottom - ghostedViewState.bottom
|
||||
|
||||
val widthRatio = state.width.toFloat() / ghostedViewState.width
|
||||
val heightRatio = state.height.toFloat() / ghostedViewState.height
|
||||
val scale = min(widthRatio, heightRatio)
|
||||
|
||||
launchContainer.getLocationOnScreen(launchContainerLocation)
|
||||
GhostView.calculateMatrix(ghostedView, launchContainer, ghostViewMatrix)
|
||||
ghostViewMatrix.postScale(
|
||||
scale, scale,
|
||||
ghostedViewState.centerX - launchContainerLocation[0],
|
||||
ghostedViewState.centerY - launchContainerLocation[1]
|
||||
)
|
||||
ghostViewMatrix.postTranslate(
|
||||
(state.leftChange + state.rightChange) / 2f,
|
||||
(state.topChange + state.bottomChange) / 2f
|
||||
(leftChange + rightChange) / 2f,
|
||||
(topChange + bottomChange) / 2f
|
||||
)
|
||||
ghostView.animationMatrix = ghostViewMatrix
|
||||
|
||||
backgroundView.top = state.top
|
||||
backgroundView.bottom = state.bottom
|
||||
backgroundView.left = state.left
|
||||
backgroundView.right = state.right
|
||||
// We need to take into account the background insets for the background position.
|
||||
val insets = backgroundInsets
|
||||
val topWithInsets = state.top - insets.top
|
||||
val leftWithInsets = state.left - insets.left
|
||||
val rightWithInsets = state.right + insets.right
|
||||
val bottomWithInsets = state.bottom + insets.bottom
|
||||
|
||||
backgroundView.top = topWithInsets - launchContainerLocation[1]
|
||||
backgroundView.bottom = bottomWithInsets - launchContainerLocation[1]
|
||||
backgroundView.left = leftWithInsets - launchContainerLocation[0]
|
||||
backgroundView.right = rightWithInsets - launchContainerLocation[0]
|
||||
|
||||
val backgroundDrawable = backgroundDrawable!!
|
||||
backgroundDrawable.wrapped?.let {
|
||||
@@ -207,7 +265,7 @@ open class GhostedViewLaunchAnimatorController(
|
||||
* [drawable] is a [LayerDrawable], this will return the first layer that is a
|
||||
* [GradientDrawable].
|
||||
*/
|
||||
private fun findGradientDrawable(drawable: Drawable): GradientDrawable? {
|
||||
fun findGradientDrawable(drawable: Drawable): GradientDrawable? {
|
||||
if (drawable is GradientDrawable) {
|
||||
return drawable
|
||||
}
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.systemui.animation
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffXfermode
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.util.Log
|
||||
import android.util.MathUtils
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AnimationUtils
|
||||
import android.view.animation.PathInterpolator
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private const val TAG = "LaunchAnimator"
|
||||
|
||||
/** A base class to animate a window launch (activity or dialog) from a view . */
|
||||
class LaunchAnimator @JvmOverloads constructor(
|
||||
context: Context,
|
||||
private val isForTesting: Boolean = false
|
||||
) {
|
||||
companion object {
|
||||
internal const val DEBUG = false
|
||||
const val ANIMATION_DURATION = 500L
|
||||
private const val ANIMATION_DURATION_FADE_OUT_CONTENT = 150L
|
||||
private const val ANIMATION_DURATION_FADE_IN_WINDOW = 183L
|
||||
private const val ANIMATION_DELAY_FADE_IN_WINDOW = ANIMATION_DURATION_FADE_OUT_CONTENT
|
||||
|
||||
private val WINDOW_FADE_IN_INTERPOLATOR = PathInterpolator(0f, 0f, 0.6f, 1f)
|
||||
private val SRC_MODE = PorterDuffXfermode(PorterDuff.Mode.SRC)
|
||||
|
||||
/**
|
||||
* Given the [linearProgress] of a launch animation, return the linear progress of the
|
||||
* sub-animation starting [delay] ms after the launch animation and that lasts [duration].
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getProgress(linearProgress: Float, delay: Long, duration: Long): Float {
|
||||
return MathUtils.constrain(
|
||||
(linearProgress * ANIMATION_DURATION - delay) / duration,
|
||||
0.0f,
|
||||
1.0f
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** The interpolator used for the width, height, Y position and corner radius. */
|
||||
private val animationInterpolator = AnimationUtils.loadInterpolator(context,
|
||||
R.interpolator.launch_animation_interpolator_y)
|
||||
|
||||
/** The interpolator used for the X position. */
|
||||
private val animationInterpolatorX = AnimationUtils.loadInterpolator(context,
|
||||
R.interpolator.launch_animation_interpolator_x)
|
||||
|
||||
private val launchContainerLocation = IntArray(2)
|
||||
private val cornerRadii = FloatArray(8)
|
||||
|
||||
/**
|
||||
* A controller that takes care of applying the animation to an expanding view.
|
||||
*
|
||||
* Note that all callbacks (onXXX methods) are all called on the main thread.
|
||||
*/
|
||||
interface Controller {
|
||||
/**
|
||||
* The container in which the view that started the animation will be animating together
|
||||
* with the opening window.
|
||||
*
|
||||
* This will be used to:
|
||||
* - Get the associated [Context].
|
||||
* - Compute whether we are expanding fully above the launch container.
|
||||
* - Apply surface transactions in sync with RenderThread when animating an activity
|
||||
* launch.
|
||||
*
|
||||
* This container can be changed to force this [Controller] to animate the expanding view
|
||||
* inside a different location, for instance to ensure correct layering during the
|
||||
* animation.
|
||||
*/
|
||||
var launchContainer: ViewGroup
|
||||
|
||||
/**
|
||||
* Return the [State] of the view that will be animated. We will animate from this state to
|
||||
* the final window state.
|
||||
*
|
||||
* Note: This state will be mutated and passed to [onLaunchAnimationProgress] during the
|
||||
* animation.
|
||||
*/
|
||||
fun createAnimatorState(): State
|
||||
|
||||
/**
|
||||
* The animation started. This is typically used to initialize any additional resource
|
||||
* needed for the animation. [isExpandingFullyAbove] will be true if the window is expanding
|
||||
* fully above the [launchContainer].
|
||||
*/
|
||||
fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {}
|
||||
|
||||
/** The animation made progress and the expandable view [state] should be updated. */
|
||||
fun onLaunchAnimationProgress(state: State, progress: Float, linearProgress: Float) {}
|
||||
|
||||
/**
|
||||
* The animation ended. This will be called *if and only if* [onLaunchAnimationStart] was
|
||||
* called previously. This is typically used to clean up the resources initialized when the
|
||||
* animation was started.
|
||||
*/
|
||||
fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {}
|
||||
}
|
||||
|
||||
/** The state of an expandable view during a [LaunchAnimator] animation. */
|
||||
open class State(
|
||||
/** The position of the view in screen space coordinates. */
|
||||
var top: Int = 0,
|
||||
var bottom: Int = 0,
|
||||
var left: Int = 0,
|
||||
var right: Int = 0,
|
||||
|
||||
var topCornerRadius: Float = 0f,
|
||||
var bottomCornerRadius: Float = 0f
|
||||
) {
|
||||
private val startTop = top
|
||||
|
||||
val width: Int
|
||||
get() = right - left
|
||||
|
||||
val height: Int
|
||||
get() = bottom - top
|
||||
|
||||
open val topChange: Int
|
||||
get() = top - startTop
|
||||
|
||||
val centerX: Float
|
||||
get() = left + width / 2f
|
||||
|
||||
val centerY: Float
|
||||
get() = top + height / 2f
|
||||
|
||||
/** Whether the expanding view should be visible or hidden. */
|
||||
var visible: Boolean = true
|
||||
}
|
||||
|
||||
interface Animation {
|
||||
/** Cancel the animation. */
|
||||
fun cancel()
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a launch animation controlled by [controller] towards [endState]. An intermediary
|
||||
* layer with [windowBackgroundColor] will fade in then fade out above the expanding view, and
|
||||
* should be the same background color as the opening (or closing) window. If [drawHole] is
|
||||
* true, then this intermediary layer will be drawn with SRC blending mode while it fades out.
|
||||
*
|
||||
* TODO(b/184121838): Remove [drawHole] and instead make the StatusBar draw this hole instead.
|
||||
*/
|
||||
fun startAnimation(
|
||||
controller: Controller,
|
||||
endState: State,
|
||||
windowBackgroundColor: Int,
|
||||
drawHole: Boolean = false
|
||||
): Animation {
|
||||
val state = controller.createAnimatorState()
|
||||
|
||||
// Start state.
|
||||
val startTop = state.top
|
||||
val startBottom = state.bottom
|
||||
val startLeft = state.left
|
||||
val startRight = state.right
|
||||
val startCenterX = (startLeft + startRight) / 2f
|
||||
val startWidth = startRight - startLeft
|
||||
val startTopCornerRadius = state.topCornerRadius
|
||||
val startBottomCornerRadius = state.bottomCornerRadius
|
||||
|
||||
// End state.
|
||||
var endTop = endState.top
|
||||
var endBottom = endState.bottom
|
||||
var endLeft = endState.left
|
||||
var endRight = endState.right
|
||||
var endCenterX = (endLeft + endRight) / 2f
|
||||
var endWidth = endRight - endLeft
|
||||
val endTopCornerRadius = endState.topCornerRadius
|
||||
val endBottomCornerRadius = endState.bottomCornerRadius
|
||||
|
||||
fun maybeUpdateEndState() {
|
||||
if (endTop != endState.top || endBottom != endState.bottom ||
|
||||
endLeft != endState.left || endRight != endState.right) {
|
||||
endTop = endState.top
|
||||
endBottom = endState.bottom
|
||||
endLeft = endState.left
|
||||
endRight = endState.right
|
||||
endCenterX = (endLeft + endRight) / 2f
|
||||
endWidth = endRight - endLeft
|
||||
}
|
||||
}
|
||||
|
||||
val launchContainer = controller.launchContainer
|
||||
val isExpandingFullyAbove = isExpandingFullyAbove(launchContainer, endState)
|
||||
|
||||
// We add an extra layer with the same color as the dialog/app splash screen background
|
||||
// color, which is usually the same color of the app background. We first fade in this layer
|
||||
// to hide the expanding view, then we fade it out with SRC mode to draw a hole in the
|
||||
// launch container and reveal the opening window.
|
||||
val windowBackgroundLayer = GradientDrawable().apply {
|
||||
setColor(windowBackgroundColor)
|
||||
alpha = 0
|
||||
}
|
||||
|
||||
// Update state.
|
||||
val animator = ValueAnimator.ofFloat(0f, 1f)
|
||||
animator.duration = if (isForTesting) 0 else ANIMATION_DURATION
|
||||
animator.interpolator = Interpolators.LINEAR
|
||||
|
||||
val launchContainerOverlay = launchContainer.overlay
|
||||
var cancelled = false
|
||||
animator.addListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationStart(animation: Animator?, isReverse: Boolean) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Animation started")
|
||||
}
|
||||
controller.onLaunchAnimationStart(isExpandingFullyAbove)
|
||||
|
||||
// Add the drawable to the launch container overlay. Overlays always draw
|
||||
// drawables after views, so we know that it will be drawn above any view added
|
||||
// by the controller.
|
||||
launchContainerOverlay.add(windowBackgroundLayer)
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Animation ended")
|
||||
}
|
||||
controller.onLaunchAnimationEnd(isExpandingFullyAbove)
|
||||
launchContainerOverlay.remove(windowBackgroundLayer)
|
||||
}
|
||||
})
|
||||
|
||||
animator.addUpdateListener { animation ->
|
||||
if (cancelled) {
|
||||
// TODO(b/184121838): Cancel the animator directly instead of just skipping the
|
||||
// update.
|
||||
return@addUpdateListener
|
||||
}
|
||||
|
||||
maybeUpdateEndState()
|
||||
|
||||
// TODO(b/184121838): Use reverse interpolators to get the same path/arc as the non
|
||||
// reversed animation.
|
||||
val linearProgress = animation.animatedFraction
|
||||
val progress = animationInterpolator.getInterpolation(linearProgress)
|
||||
val xProgress = animationInterpolatorX.getInterpolation(linearProgress)
|
||||
|
||||
val xCenter = MathUtils.lerp(startCenterX, endCenterX, xProgress)
|
||||
val halfWidth = MathUtils.lerp(startWidth, endWidth, progress) / 2f
|
||||
|
||||
state.top = MathUtils.lerp(startTop, endTop, progress).roundToInt()
|
||||
state.bottom = MathUtils.lerp(startBottom, endBottom, progress).roundToInt()
|
||||
state.left = (xCenter - halfWidth).roundToInt()
|
||||
state.right = (xCenter + halfWidth).roundToInt()
|
||||
|
||||
state.topCornerRadius =
|
||||
MathUtils.lerp(startTopCornerRadius, endTopCornerRadius, progress)
|
||||
state.bottomCornerRadius =
|
||||
MathUtils.lerp(startBottomCornerRadius, endBottomCornerRadius, progress)
|
||||
|
||||
// The expanding view can/should be hidden once it is completely covered by the opening
|
||||
// window.
|
||||
state.visible = getProgress(linearProgress, 0, ANIMATION_DURATION_FADE_OUT_CONTENT) < 1
|
||||
|
||||
applyStateToWindowBackgroundLayer(
|
||||
windowBackgroundLayer,
|
||||
state,
|
||||
linearProgress,
|
||||
launchContainer,
|
||||
drawHole
|
||||
)
|
||||
controller.onLaunchAnimationProgress(state, progress, linearProgress)
|
||||
}
|
||||
|
||||
animator.start()
|
||||
return object : Animation {
|
||||
override fun cancel() {
|
||||
cancelled = true
|
||||
animator.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Return whether we are expanding fully above the [launchContainer]. */
|
||||
internal fun isExpandingFullyAbove(launchContainer: View, endState: State): Boolean {
|
||||
launchContainer.getLocationOnScreen(launchContainerLocation)
|
||||
return endState.top <= launchContainerLocation[1] &&
|
||||
endState.bottom >= launchContainerLocation[1] + launchContainer.height &&
|
||||
endState.left <= launchContainerLocation[0] &&
|
||||
endState.right >= launchContainerLocation[0] + launchContainer.width
|
||||
}
|
||||
|
||||
private fun applyStateToWindowBackgroundLayer(
|
||||
drawable: GradientDrawable,
|
||||
state: State,
|
||||
linearProgress: Float,
|
||||
launchContainer: View,
|
||||
drawHole: Boolean
|
||||
) {
|
||||
// Update position.
|
||||
launchContainer.getLocationOnScreen(launchContainerLocation)
|
||||
drawable.setBounds(
|
||||
state.left - launchContainerLocation[0],
|
||||
state.top - launchContainerLocation[1],
|
||||
state.right - launchContainerLocation[0],
|
||||
state.bottom - launchContainerLocation[1]
|
||||
)
|
||||
|
||||
// Update radius.
|
||||
cornerRadii[0] = state.topCornerRadius
|
||||
cornerRadii[1] = state.topCornerRadius
|
||||
cornerRadii[2] = state.topCornerRadius
|
||||
cornerRadii[3] = state.topCornerRadius
|
||||
cornerRadii[4] = state.bottomCornerRadius
|
||||
cornerRadii[5] = state.bottomCornerRadius
|
||||
cornerRadii[6] = state.bottomCornerRadius
|
||||
cornerRadii[7] = state.bottomCornerRadius
|
||||
drawable.cornerRadii = cornerRadii
|
||||
|
||||
// We first fade in the background layer to hide the expanding view, then fade it out
|
||||
// with SRC mode to draw a hole punch in the status bar and reveal the opening window.
|
||||
val fadeInProgress = getProgress(linearProgress, 0, ANIMATION_DURATION_FADE_OUT_CONTENT)
|
||||
if (fadeInProgress < 1) {
|
||||
val alpha = Interpolators.LINEAR_OUT_SLOW_IN.getInterpolation(fadeInProgress)
|
||||
drawable.alpha = (alpha * 0xFF).roundToInt()
|
||||
} else {
|
||||
val fadeOutProgress = getProgress(
|
||||
linearProgress, ANIMATION_DELAY_FADE_IN_WINDOW, ANIMATION_DURATION_FADE_IN_WINDOW)
|
||||
val alpha = 1 - WINDOW_FADE_IN_INTERPOLATOR.getInterpolation(fadeOutProgress)
|
||||
drawable.alpha = (alpha * 0xFF).roundToInt()
|
||||
|
||||
if (drawHole) {
|
||||
drawable.setXfermode(SRC_MODE)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,8 @@ import android.content.Context;
|
||||
import android.os.Handler;
|
||||
|
||||
import com.android.internal.statusbar.IStatusBarService;
|
||||
import com.android.systemui.animation.ActivityLaunchAnimator;
|
||||
import com.android.systemui.animation.LaunchAnimator;
|
||||
import com.android.systemui.dagger.SysUISingleton;
|
||||
import com.android.systemui.dagger.qualifiers.Main;
|
||||
import com.android.systemui.dump.DumpManager;
|
||||
@@ -261,4 +263,20 @@ public interface StatusBarDependenciesModule {
|
||||
@Binds
|
||||
QSCarrierGroupController.SlotIndexResolver provideSlotIndexResolver(
|
||||
QSCarrierGroupController.SubscriptionManagerSlotIndexResolver impl);
|
||||
|
||||
/**
|
||||
*/
|
||||
@Provides
|
||||
@SysUISingleton
|
||||
static LaunchAnimator provideLaunchAnimator(Context context) {
|
||||
return new LaunchAnimator(context);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
@Provides
|
||||
@SysUISingleton
|
||||
static ActivityLaunchAnimator provideActivityLaunchAnimator(LaunchAnimator launchAnimator) {
|
||||
return new ActivityLaunchAnimator(launchAnimator);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ package com.android.systemui.statusbar.notification
|
||||
|
||||
import android.util.MathUtils
|
||||
import com.android.internal.annotations.VisibleForTesting
|
||||
import com.android.systemui.animation.ActivityLaunchAnimator
|
||||
import com.android.systemui.animation.Interpolators
|
||||
import com.android.systemui.animation.LaunchAnimator
|
||||
import kotlin.math.min
|
||||
|
||||
/** Parameters for the notifications expand animations. */
|
||||
@@ -15,7 +15,7 @@ class ExpandAnimationParameters(
|
||||
|
||||
topCornerRadius: Float = 0f,
|
||||
bottomCornerRadius: Float = 0f
|
||||
) : ActivityLaunchAnimator.State(top, bottom, left, right, topCornerRadius, bottomCornerRadius) {
|
||||
) : LaunchAnimator.State(top, bottom, left, right, topCornerRadius, bottomCornerRadius) {
|
||||
@VisibleForTesting
|
||||
constructor() : this(
|
||||
top = 0, bottom = 0, left = 0, right = 0, topCornerRadius = 0f, bottomCornerRadius = 0f
|
||||
@@ -55,6 +55,6 @@ class ExpandAnimationParameters(
|
||||
}
|
||||
|
||||
fun getProgress(delay: Long, duration: Long): Float {
|
||||
return ActivityLaunchAnimator.getProgress(linearProgress, delay, duration)
|
||||
return LaunchAnimator.getProgress(linearProgress, delay, duration)
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.android.systemui.statusbar.notification
|
||||
import android.view.ViewGroup
|
||||
import com.android.internal.jank.InteractionJankMonitor
|
||||
import com.android.systemui.animation.ActivityLaunchAnimator
|
||||
import com.android.systemui.animation.LaunchAnimator
|
||||
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
|
||||
import com.android.systemui.statusbar.notification.stack.NotificationListContainer
|
||||
import com.android.systemui.statusbar.phone.HeadsUpManagerPhone
|
||||
@@ -54,7 +55,7 @@ class NotificationLaunchAnimatorController(
|
||||
// Do nothing. Notifications are always animated inside their rootView.
|
||||
}
|
||||
|
||||
override fun createAnimatorState(): ActivityLaunchAnimator.State {
|
||||
override fun createAnimatorState(): LaunchAnimator.State {
|
||||
// If the notification panel is collapsed, the clip may be larger than the height.
|
||||
val height = max(0, notification.actualHeight - notification.clipBottomAmount)
|
||||
val location = notification.locationOnScreen
|
||||
@@ -72,12 +73,12 @@ class NotificationLaunchAnimatorController(
|
||||
notification.currentBackgroundRadiusTop
|
||||
}
|
||||
val params = ExpandAnimationParameters(
|
||||
top = windowTop,
|
||||
bottom = location[1] + height,
|
||||
left = location[0],
|
||||
right = location[0] + notification.width,
|
||||
topCornerRadius = topCornerRadius,
|
||||
bottomCornerRadius = notification.currentBackgroundRadiusBottom
|
||||
top = windowTop,
|
||||
bottom = location[1] + height,
|
||||
left = location[0],
|
||||
right = location[0] + notification.width,
|
||||
topCornerRadius = topCornerRadius,
|
||||
bottomCornerRadius = notification.currentBackgroundRadiusBottom
|
||||
)
|
||||
|
||||
params.startTranslationZ = notification.translationZ
|
||||
@@ -86,8 +87,8 @@ class NotificationLaunchAnimatorController(
|
||||
params.startClipTopAmount = notification.clipTopAmount
|
||||
if (notification.isChildInGroup) {
|
||||
params.startNotificationTop += notification.notificationParent.translationY
|
||||
val parentRoundedClip = Math.max(clipStartLocation
|
||||
- notification.notificationParent.locationOnScreen[1], 0)
|
||||
val parentRoundedClip = Math.max(
|
||||
clipStartLocation - notification.notificationParent.locationOnScreen[1], 0)
|
||||
params.parentStartRoundedTopClipping = parentRoundedClip
|
||||
|
||||
val parentClip = notification.notificationParent.clipTopAmount
|
||||
@@ -157,7 +158,7 @@ class NotificationLaunchAnimatorController(
|
||||
}
|
||||
|
||||
override fun onLaunchAnimationProgress(
|
||||
state: ActivityLaunchAnimator.State,
|
||||
state: LaunchAnimator.State,
|
||||
progress: Float,
|
||||
linearProgress: Float
|
||||
) {
|
||||
|
||||
@@ -100,8 +100,8 @@ import com.android.keyguard.dagger.KeyguardUserSwitcherComponent;
|
||||
import com.android.systemui.DejankUtils;
|
||||
import com.android.systemui.Dependency;
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.animation.ActivityLaunchAnimator;
|
||||
import com.android.systemui.animation.Interpolators;
|
||||
import com.android.systemui.animation.LaunchAnimator;
|
||||
import com.android.systemui.biometrics.AuthController;
|
||||
import com.android.systemui.classifier.Classifier;
|
||||
import com.android.systemui.classifier.FalsingCollector;
|
||||
@@ -209,7 +209,7 @@ public class NotificationPanelViewController extends PanelViewController {
|
||||
*/
|
||||
private static final int FLING_HIDE = 2;
|
||||
private static final long ANIMATION_DELAY_ICON_FADE_IN =
|
||||
ActivityLaunchAnimator.ANIMATION_DURATION - CollapsedStatusBarFragment.FADE_IN_DURATION
|
||||
LaunchAnimator.ANIMATION_DURATION - CollapsedStatusBarFragment.FADE_IN_DURATION
|
||||
- CollapsedStatusBarFragment.FADE_IN_DELAY - 48;
|
||||
|
||||
private final DozeParameters mDozeParameters;
|
||||
@@ -3574,7 +3574,7 @@ public class NotificationPanelViewController extends PanelViewController {
|
||||
}
|
||||
|
||||
public void applyLaunchAnimationProgress(float linearProgress) {
|
||||
boolean hideIcons = ActivityLaunchAnimator.getProgress(linearProgress,
|
||||
boolean hideIcons = LaunchAnimator.getProgress(linearProgress,
|
||||
ANIMATION_DELAY_ICON_FADE_IN, 100) == 0.0f;
|
||||
if (hideIcons != mHideIconsDuringLaunchAnimation) {
|
||||
mHideIconsDuringLaunchAnimation = hideIcons;
|
||||
|
||||
@@ -663,7 +663,7 @@ public class StatusBar extends SystemUI implements
|
||||
private final SysuiStatusBarStateController mStatusBarStateController;
|
||||
|
||||
private HeadsUpAppearanceController mHeadsUpAppearanceController;
|
||||
private ActivityLaunchAnimator mActivityLaunchAnimator;
|
||||
private final ActivityLaunchAnimator mActivityLaunchAnimator;
|
||||
private NotificationLaunchAnimatorControllerProvider mNotificationAnimationProvider;
|
||||
protected StatusBarNotificationPresenter mPresenter;
|
||||
private NotificationActivityStarter mNotificationActivityStarter;
|
||||
@@ -782,7 +782,8 @@ public class StatusBar extends SystemUI implements
|
||||
UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
|
||||
Optional<StartingSurface> startingSurfaceOptional,
|
||||
TunerService tunerService,
|
||||
DumpManager dumpManager) {
|
||||
DumpManager dumpManager,
|
||||
ActivityLaunchAnimator activityLaunchAnimator) {
|
||||
super(context);
|
||||
mNotificationsController = notificationsController;
|
||||
mLightBarController = lightBarController;
|
||||
@@ -886,6 +887,7 @@ public class StatusBar extends SystemUI implements
|
||||
});
|
||||
|
||||
mActivityIntentHelper = new ActivityIntentHelper(mContext);
|
||||
mActivityLaunchAnimator = activityLaunchAnimator;
|
||||
|
||||
// TODO(b/190746471): Find a better home for this.
|
||||
DateTimeView.setReceiverHandler(timeTickHandler);
|
||||
@@ -1433,7 +1435,7 @@ public class StatusBar extends SystemUI implements
|
||||
|
||||
private void setUpPresenter() {
|
||||
// Set up the initial notification state.
|
||||
mActivityLaunchAnimator = new ActivityLaunchAnimator(mKeyguardHandler, mContext);
|
||||
mActivityLaunchAnimator.setCallback(mKeyguardHandler);
|
||||
mNotificationAnimationProvider = new NotificationLaunchAnimatorControllerProvider(
|
||||
mNotificationShadeWindowViewController,
|
||||
mStackScrollerController.getNotificationListContainer(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.android.systemui.statusbar.phone
|
||||
|
||||
import com.android.systemui.animation.ActivityLaunchAnimator
|
||||
import com.android.systemui.animation.LaunchAnimator
|
||||
|
||||
/**
|
||||
* A [ActivityLaunchAnimator.Controller] that takes care of collapsing the status bar at the right
|
||||
@@ -22,7 +23,7 @@ class StatusBarLaunchAnimatorController(
|
||||
delegate.onLaunchAnimationStart(isExpandingFullyAbove)
|
||||
statusBar.notificationPanelViewController.setIsLaunchAnimationRunning(true)
|
||||
if (!isExpandingFullyAbove) {
|
||||
statusBar.collapsePanelWithDuration(ActivityLaunchAnimator.ANIMATION_DURATION.toInt())
|
||||
statusBar.collapsePanelWithDuration(LaunchAnimator.ANIMATION_DURATION.toInt())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +34,7 @@ class StatusBarLaunchAnimatorController(
|
||||
}
|
||||
|
||||
override fun onLaunchAnimationProgress(
|
||||
state: ActivityLaunchAnimator.State,
|
||||
state: LaunchAnimator.State,
|
||||
progress: Float,
|
||||
linearProgress: Float
|
||||
) {
|
||||
|
||||
@@ -28,6 +28,7 @@ import com.android.internal.logging.MetricsLogger;
|
||||
import com.android.keyguard.KeyguardUpdateMonitor;
|
||||
import com.android.keyguard.ViewMediatorCallback;
|
||||
import com.android.systemui.InitController;
|
||||
import com.android.systemui.animation.ActivityLaunchAnimator;
|
||||
import com.android.systemui.assist.AssistManager;
|
||||
import com.android.systemui.broadcast.BroadcastDispatcher;
|
||||
import com.android.systemui.classifier.FalsingCollector;
|
||||
@@ -227,7 +228,8 @@ public interface StatusBarPhoneModule {
|
||||
UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
|
||||
Optional<StartingSurface> startingSurfaceOptional,
|
||||
TunerService tunerService,
|
||||
DumpManager dumpManager) {
|
||||
DumpManager dumpManager,
|
||||
ActivityLaunchAnimator activityLaunchAnimator) {
|
||||
return new StatusBar(
|
||||
context,
|
||||
notificationsController,
|
||||
@@ -321,6 +323,7 @@ public interface StatusBarPhoneModule {
|
||||
unlockedScreenOffAnimationController,
|
||||
startingSurfaceOptional,
|
||||
tunerService,
|
||||
dumpManager);
|
||||
dumpManager,
|
||||
activityLaunchAnimator);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ import org.mockito.junit.MockitoJUnit
|
||||
@RunWithLooper
|
||||
class ActivityLaunchAnimatorTest : SysuiTestCase() {
|
||||
private val launchContainer = LinearLayout(mContext)
|
||||
private val launchAnimator = LaunchAnimator(mContext, isForTesting = true)
|
||||
@Mock lateinit var callback: ActivityLaunchAnimator.Callback
|
||||
@Spy private val controller = TestLaunchAnimatorController(launchContainer)
|
||||
@Mock lateinit var iCallback: IRemoteAnimationFinishedCallback
|
||||
@@ -56,7 +57,8 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() {
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
activityLaunchAnimator = ActivityLaunchAnimator(callback, mContext)
|
||||
activityLaunchAnimator = ActivityLaunchAnimator(launchAnimator)
|
||||
activityLaunchAnimator.callback = callback
|
||||
}
|
||||
|
||||
private fun startIntentWithAnimation(
|
||||
@@ -120,7 +122,8 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun animatesIfActivityIsAlreadyOpenAndIsOnKeyguard() {
|
||||
`when`(callback.isOnKeyguard()).thenReturn(true)
|
||||
val animator = ActivityLaunchAnimator(callback, context)
|
||||
val animator = ActivityLaunchAnimator(launchAnimator)
|
||||
animator.callback = callback
|
||||
|
||||
val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java)
|
||||
var animationAdapter: RemoteAnimationAdapter? = null
|
||||
@@ -208,7 +211,7 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() {
|
||||
private class TestLaunchAnimatorController(
|
||||
override var launchContainer: ViewGroup
|
||||
) : ActivityLaunchAnimator.Controller {
|
||||
override fun createAnimatorState() = ActivityLaunchAnimator.State(
|
||||
override fun createAnimatorState() = LaunchAnimator.State(
|
||||
top = 100,
|
||||
bottom = 200,
|
||||
left = 300,
|
||||
@@ -232,7 +235,7 @@ private class TestLaunchAnimatorController(
|
||||
}
|
||||
|
||||
override fun onLaunchAnimationProgress(
|
||||
state: ActivityLaunchAnimator.State,
|
||||
state: LaunchAnimator.State,
|
||||
progress: Float,
|
||||
linearProgress: Float
|
||||
) {
|
||||
|
||||
@@ -32,7 +32,7 @@ class GhostedViewLaunchAnimatorControllerTest : SysuiTestCase() {
|
||||
fun animatingOrphanViewDoesNotCrash() {
|
||||
val ghostedView = LinearLayout(mContext)
|
||||
val controller = GhostedViewLaunchAnimatorController(ghostedView)
|
||||
val state = ActivityLaunchAnimator.State(top = 0, bottom = 0, left = 0, right = 0)
|
||||
val state = LaunchAnimator.State(top = 0, bottom = 0, left = 0, right = 0)
|
||||
|
||||
controller.onIntentStarted(willAnimate = true)
|
||||
controller.onLaunchAnimationStart(isExpandingFullyAbove = true)
|
||||
|
||||
@@ -77,6 +77,7 @@ import com.android.keyguard.ViewMediatorCallback;
|
||||
import com.android.systemui.InitController;
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.SysuiTestCase;
|
||||
import com.android.systemui.animation.ActivityLaunchAnimator;
|
||||
import com.android.systemui.assist.AssistManager;
|
||||
import com.android.systemui.broadcast.BroadcastDispatcher;
|
||||
import com.android.systemui.classifier.FalsingCollectorFake;
|
||||
@@ -268,6 +269,7 @@ public class StatusBarTest extends SysuiTestCase {
|
||||
@Mock private StartingSurface mStartingSurface;
|
||||
@Mock private OperatorNameViewController mOperatorNameViewController;
|
||||
@Mock private OperatorNameViewController.Factory mOperatorNameViewControllerFactory;
|
||||
@Mock private ActivityLaunchAnimator mActivityLaunchAnimator;
|
||||
private ShadeController mShadeController;
|
||||
private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
|
||||
private FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock);
|
||||
@@ -442,7 +444,8 @@ public class StatusBarTest extends SysuiTestCase {
|
||||
mUnlockedScreenOffAnimationController,
|
||||
Optional.of(mStartingSurface),
|
||||
mTunerService,
|
||||
mock(DumpManager.class));
|
||||
mock(DumpManager.class),
|
||||
mActivityLaunchAnimator);
|
||||
when(mKeyguardViewMediator.registerStatusBar(any(StatusBar.class), any(ViewGroup.class),
|
||||
any(NotificationPanelViewController.class), any(BiometricUnlockController.class),
|
||||
any(ViewGroup.class), any(KeyguardBypassController.class)))
|
||||
|
||||
Reference in New Issue
Block a user