Merge changes Ia5accb74,Ic4d7def4,I30bd56fc into sc-v2-dev
* changes: Change DialogLaunchAnimator X-axis interpolator Extract interpolators and durations out of LaunchAnimator Synchronize dialog launch animation using BLAST
This commit is contained in:
committed by
Android (Google) Code Review
commit
7339a87bf3
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:pathData="M 0, 0 C 0.1217, 0.0462, 0.15, 0.4686, 0.1667, 0.66 C 0.1834, 0.8878, 0.1667, 1, 1, 1" />
|
||||
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:pathData="M 0,0 C 0.05, 0, 0.133333, 0.06, 0.166666, 0.4 C 0.208333, 0.82, 0.25, 1, 1, 1" />
|
||||
@@ -21,6 +21,7 @@ import android.app.ActivityTaskManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.TaskInfo
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.Path
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.os.Looper
|
||||
@@ -34,6 +35,7 @@ import android.view.SyncRtSurfaceTransactionApplier
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.view.animation.Interpolator
|
||||
import android.view.animation.PathInterpolator
|
||||
import com.android.internal.annotations.VisibleForTesting
|
||||
import com.android.internal.policy.ScreenDecorationsUtils
|
||||
@@ -45,16 +47,46 @@ 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 launchAnimator: LaunchAnimator) {
|
||||
class ActivityLaunchAnimator(
|
||||
private val launchAnimator: LaunchAnimator = LaunchAnimator(TIMINGS, INTERPOLATORS)
|
||||
) {
|
||||
companion object {
|
||||
@JvmField
|
||||
val TIMINGS = LaunchAnimator.Timings(
|
||||
totalDuration = 500L,
|
||||
contentBeforeFadeOutDelay = 0L,
|
||||
contentBeforeFadeOutDuration = 150L,
|
||||
contentAfterFadeInDelay = 150L,
|
||||
contentAfterFadeInDuration = 183L
|
||||
)
|
||||
|
||||
val INTERPOLATORS = LaunchAnimator.Interpolators(
|
||||
positionInterpolator = Interpolators.EMPHASIZED,
|
||||
positionXInterpolator = createPositionXInterpolator(),
|
||||
contentBeforeFadeOutInterpolator = Interpolators.LINEAR_OUT_SLOW_IN,
|
||||
contentAfterFadeInInterpolator = PathInterpolator(0f, 0f, 0.6f, 1f)
|
||||
)
|
||||
|
||||
/** Durations & interpolators for the navigation bar fading in & out. */
|
||||
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 =
|
||||
LaunchAnimator.ANIMATION_DURATION - ANIMATION_DURATION_NAV_FADE_IN
|
||||
private val ANIMATION_DELAY_NAV_FADE_IN =
|
||||
TIMINGS.totalDuration - ANIMATION_DURATION_NAV_FADE_IN
|
||||
|
||||
private val NAV_FADE_IN_INTERPOLATOR = Interpolators.STANDARD_DECELERATE
|
||||
private val NAV_FADE_OUT_INTERPOLATOR = PathInterpolator(0.2f, 0f, 1f, 1f)
|
||||
|
||||
/** The time we wait before timing out the remote animation after starting the intent. */
|
||||
private const val LAUNCH_TIMEOUT = 1000L
|
||||
|
||||
private val NAV_FADE_IN_INTERPOLATOR = PathInterpolator(0f, 0f, 0f, 1f)
|
||||
private val NAV_FADE_OUT_INTERPOLATOR = PathInterpolator(0.2f, 0f, 1f, 1f)
|
||||
private fun createPositionXInterpolator(): Interpolator {
|
||||
val path = Path().apply {
|
||||
moveTo(0f, 0f)
|
||||
cubicTo(0.1217f, 0.0462f, 0.15f, 0.4686f, 0.1667f, 0.66f)
|
||||
cubicTo(0.1834f, 0.8878f, 0.1667f, 1f, 1f, 1f)
|
||||
}
|
||||
return PathInterpolator(path)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,8 +139,8 @@ class ActivityLaunchAnimator(private val launchAnimator: LaunchAnimator) {
|
||||
val animationAdapter = if (!hideKeyguardWithAnimation) {
|
||||
RemoteAnimationAdapter(
|
||||
runner,
|
||||
LaunchAnimator.ANIMATION_DURATION,
|
||||
LaunchAnimator.ANIMATION_DURATION - 150 /* statusBarTransitionDelay */
|
||||
TIMINGS.totalDuration,
|
||||
TIMINGS.totalDuration - 150 /* statusBarTransitionDelay */
|
||||
)
|
||||
} else {
|
||||
null
|
||||
@@ -448,7 +480,7 @@ class ActivityLaunchAnimator(private val launchAnimator: LaunchAnimator) {
|
||||
state: LaunchAnimator.State,
|
||||
linearProgress: Float
|
||||
) {
|
||||
val fadeInProgress = LaunchAnimator.getProgress(linearProgress,
|
||||
val fadeInProgress = LaunchAnimator.getProgress(TIMINGS, linearProgress,
|
||||
ANIMATION_DELAY_NAV_FADE_IN, ANIMATION_DURATION_NAV_FADE_OUT)
|
||||
|
||||
val params = SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(navigationBar.leash)
|
||||
@@ -463,7 +495,7 @@ class ActivityLaunchAnimator(private val launchAnimator: LaunchAnimator) {
|
||||
.withWindowCrop(windowCrop)
|
||||
.withVisibility(true)
|
||||
} else {
|
||||
val fadeOutProgress = LaunchAnimator.getProgress(linearProgress, 0,
|
||||
val fadeOutProgress = LaunchAnimator.getProgress(TIMINGS, linearProgress, 0,
|
||||
ANIMATION_DURATION_NAV_FADE_OUT)
|
||||
params.withAlpha(1f - NAV_FADE_OUT_INTERPOLATOR.getInterpolation(fadeOutProgress))
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.animation.ValueAnimator
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.Rect
|
||||
import android.os.Looper
|
||||
@@ -28,10 +27,11 @@ import android.service.dreams.IDreamManager
|
||||
import android.util.Log
|
||||
import android.util.MathUtils
|
||||
import android.view.GhostView
|
||||
import android.view.SurfaceControl
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewTreeObserver.OnPreDrawListener
|
||||
import android.view.ViewRootImpl
|
||||
import android.view.WindowManager
|
||||
import android.widget.FrameLayout
|
||||
import kotlin.math.roundToInt
|
||||
@@ -42,12 +42,20 @@ private const val TAG = "DialogLaunchAnimator"
|
||||
* A class that allows dialogs to be started in a seamless way from a view that is transforming
|
||||
* nicely into the starting dialog.
|
||||
*/
|
||||
class DialogLaunchAnimator(
|
||||
private val context: Context,
|
||||
private val launchAnimator: LaunchAnimator,
|
||||
private val dreamManager: IDreamManager
|
||||
class DialogLaunchAnimator @JvmOverloads constructor(
|
||||
private val dreamManager: IDreamManager,
|
||||
private val launchAnimator: LaunchAnimator = LaunchAnimator(TIMINGS, INTERPOLATORS),
|
||||
private var isForTesting: Boolean = false
|
||||
) {
|
||||
private companion object {
|
||||
private val TIMINGS = ActivityLaunchAnimator.TIMINGS
|
||||
|
||||
// We use the same interpolator for X and Y axis to make sure the dialog does not move out
|
||||
// of the screen bounds during the animation.
|
||||
private val INTERPOLATORS = ActivityLaunchAnimator.INTERPOLATORS.copy(
|
||||
positionXInterpolator = ActivityLaunchAnimator.INTERPOLATORS.positionInterpolator
|
||||
)
|
||||
|
||||
private val TAG_LAUNCH_ANIMATION_RUNNING = R.id.launch_animation_running
|
||||
}
|
||||
|
||||
@@ -96,14 +104,14 @@ class DialogLaunchAnimator(
|
||||
animateFrom.setTag(TAG_LAUNCH_ANIMATION_RUNNING, true)
|
||||
|
||||
val animatedDialog = AnimatedDialog(
|
||||
context,
|
||||
launchAnimator,
|
||||
dreamManager,
|
||||
animateFrom,
|
||||
onDialogDismissed = { openedDialogs.remove(it) },
|
||||
dialog = dialog,
|
||||
animateBackgroundBoundsChange,
|
||||
animatedParent
|
||||
animatedParent,
|
||||
isForTesting
|
||||
)
|
||||
|
||||
openedDialogs.add(animatedDialog)
|
||||
@@ -157,7 +165,6 @@ class DialogLaunchAnimator(
|
||||
}
|
||||
|
||||
private class AnimatedDialog(
|
||||
private val context: Context,
|
||||
private val launchAnimator: LaunchAnimator,
|
||||
private val dreamManager: IDreamManager,
|
||||
|
||||
@@ -174,10 +181,16 @@ private class AnimatedDialog(
|
||||
val dialog: Dialog,
|
||||
|
||||
/** Whether we should animate the dialog background when its bounds change. */
|
||||
private val animateBackgroundBoundsChange: Boolean,
|
||||
animateBackgroundBoundsChange: Boolean,
|
||||
|
||||
/** Launch animation corresponding to the parent [AnimatedDialog]. */
|
||||
private val parentAnimatedDialog: AnimatedDialog? = null
|
||||
private val parentAnimatedDialog: AnimatedDialog? = null,
|
||||
|
||||
/**
|
||||
* Whether we are currently running in a test, in which case we need to disable
|
||||
* synchronization.
|
||||
*/
|
||||
private val isForTesting: Boolean
|
||||
) {
|
||||
/**
|
||||
* The DecorView of this dialog window.
|
||||
@@ -266,14 +279,14 @@ private class AnimatedDialog(
|
||||
// and the view that we added so that we can dismiss the dialog when this view is
|
||||
// clicked. This is necessary because DecorView overrides onTouchEvent and therefore we
|
||||
// can't set the click listener directly on the (now fullscreen) DecorView.
|
||||
val fullscreenTransparentBackground = FrameLayout(context)
|
||||
val fullscreenTransparentBackground = FrameLayout(dialog.context)
|
||||
decorView.addView(
|
||||
fullscreenTransparentBackground,
|
||||
0 /* index */,
|
||||
FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
|
||||
)
|
||||
|
||||
val dialogContentWithBackground = FrameLayout(context)
|
||||
val dialogContentWithBackground = FrameLayout(dialog.context)
|
||||
dialogContentWithBackground.background = decorView.background
|
||||
|
||||
// Make the window background transparent. Note that setting the window (or DecorView)
|
||||
@@ -365,59 +378,77 @@ private class AnimatedDialog(
|
||||
// Show the dialog.
|
||||
dialog.show()
|
||||
|
||||
// Add a temporary touch surface ghost as soon as the window is ready to draw. This
|
||||
// temporary ghost will be drawn together with the touch surface, but in the dialog
|
||||
// window. Once it is drawn, we will make the touch surface invisible, and then start the
|
||||
// animation. We do all this synchronization to avoid flicker that would occur if we made
|
||||
// the touch surface invisible too early (before its ghost is drawn), leading to one or more
|
||||
// frames with a hole instead of the touch surface (or its ghost).
|
||||
decorView.viewTreeObserver.addOnPreDrawListener(object : OnPreDrawListener {
|
||||
override fun onPreDraw(): Boolean {
|
||||
decorView.viewTreeObserver.removeOnPreDrawListener(this)
|
||||
addTemporaryTouchSurfaceGhost()
|
||||
return true
|
||||
}
|
||||
})
|
||||
decorView.invalidate()
|
||||
addTouchSurfaceGhost()
|
||||
}
|
||||
|
||||
private fun addTemporaryTouchSurfaceGhost() {
|
||||
private fun addTouchSurfaceGhost() {
|
||||
if (decorView.viewRootImpl == null) {
|
||||
// Make sure that we have access to the dialog view root to synchronize the creation of
|
||||
// the ghost.
|
||||
decorView.post(::addTouchSurfaceGhost)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a ghost of the touch surface (which will make the touch surface invisible) and add
|
||||
// it to the dialog. We will wait for this ghost to be drawn before starting the animation.
|
||||
val ghost = GhostView.addGhost(touchSurface, decorView)
|
||||
|
||||
// The ghost of the touch surface was just created, so the touch surface was made invisible.
|
||||
// We make it visible again until the ghost is actually drawn.
|
||||
touchSurface.visibility = View.VISIBLE
|
||||
|
||||
// Wait for the ghost to be drawn before continuing.
|
||||
ghost.viewTreeObserver.addOnPreDrawListener(object : OnPreDrawListener {
|
||||
override fun onPreDraw(): Boolean {
|
||||
ghost.viewTreeObserver.removeOnPreDrawListener(this)
|
||||
onTouchSurfaceGhostDrawn()
|
||||
return true
|
||||
}
|
||||
// it to the host dialog. We trigger a one off synchronization to make sure that this is
|
||||
// done in sync between the two different windows.
|
||||
synchronizeNextDraw(then = {
|
||||
isTouchSurfaceGhostDrawn = true
|
||||
maybeStartLaunchAnimation()
|
||||
})
|
||||
ghost.invalidate()
|
||||
GhostView.addGhost(touchSurface, decorView)
|
||||
|
||||
// The ghost of the touch surface was just created, so the touch surface is currently
|
||||
// invisible. We need to make sure that it stays invisible as long as the dialog is shown or
|
||||
// animating.
|
||||
(touchSurface as? LaunchableView)?.setShouldBlockVisibilityChanges(true)
|
||||
}
|
||||
|
||||
private fun onTouchSurfaceGhostDrawn() {
|
||||
// Make the touch surface invisible and make sure that it stays invisible as long as the
|
||||
// dialog is shown or animating.
|
||||
touchSurface.visibility = View.INVISIBLE
|
||||
(touchSurface as? LaunchableView)?.setShouldBlockVisibilityChanges(true)
|
||||
/**
|
||||
* Synchronize the next draw of the touch surface and dialog view roots so that they are
|
||||
* performed at the same time, in the same transaction. This is necessary to make sure that the
|
||||
* ghost of the touch surface is drawn at the same time as the touch surface is made invisible
|
||||
* (or inversely, removed from the UI when the touch surface is made visible).
|
||||
*/
|
||||
private fun synchronizeNextDraw(then: () -> Unit) {
|
||||
if (isForTesting || !touchSurface.isAttachedToWindow || touchSurface.viewRootImpl == null ||
|
||||
!decorView.isAttachedToWindow || decorView.viewRootImpl == null) {
|
||||
// No need to synchronize if either the touch surface or dialog view is not attached
|
||||
// to a window.
|
||||
then()
|
||||
return
|
||||
}
|
||||
|
||||
// Add a pre draw listener to (maybe) start the animation once the touch surface is
|
||||
// actually invisible.
|
||||
touchSurface.viewTreeObserver.addOnPreDrawListener(object : OnPreDrawListener {
|
||||
override fun onPreDraw(): Boolean {
|
||||
touchSurface.viewTreeObserver.removeOnPreDrawListener(this)
|
||||
isTouchSurfaceGhostDrawn = true
|
||||
maybeStartLaunchAnimation()
|
||||
return true
|
||||
// Consume the next frames of both view roots to make sure the ghost view is drawn at
|
||||
// exactly the same time as when the touch surface is made invisible.
|
||||
var remainingTransactions = 0
|
||||
val mergedTransactions = SurfaceControl.Transaction()
|
||||
|
||||
fun onTransaction(transaction: SurfaceControl.Transaction?) {
|
||||
remainingTransactions--
|
||||
transaction?.let { mergedTransactions.merge(it) }
|
||||
|
||||
if (remainingTransactions == 0) {
|
||||
mergedTransactions.apply()
|
||||
then()
|
||||
}
|
||||
})
|
||||
touchSurface.invalidate()
|
||||
}
|
||||
|
||||
fun consumeNextDraw(viewRootImpl: ViewRootImpl) {
|
||||
if (viewRootImpl.consumeNextDraw(::onTransaction)) {
|
||||
remainingTransactions++
|
||||
|
||||
// Make sure we trigger a traversal.
|
||||
viewRootImpl.view.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
consumeNextDraw(touchSurface.viewRootImpl)
|
||||
consumeNextDraw(decorView.viewRootImpl)
|
||||
|
||||
if (remainingTransactions == 0) {
|
||||
then()
|
||||
}
|
||||
}
|
||||
|
||||
private fun findFirstViewGroupWithBackground(view: View): ViewGroup? {
|
||||
@@ -483,7 +514,7 @@ private class AnimatedDialog(
|
||||
|
||||
private fun onDialogDismissed() {
|
||||
if (Looper.myLooper() != Looper.getMainLooper()) {
|
||||
context.mainExecutor.execute { onDialogDismissed() }
|
||||
dialog.context.mainExecutor.execute { onDialogDismissed() }
|
||||
return
|
||||
}
|
||||
|
||||
@@ -556,25 +587,12 @@ private class AnimatedDialog(
|
||||
.removeOnLayoutChangeListener(backgroundLayoutListener)
|
||||
}
|
||||
|
||||
// The animated ghost was just removed. We create a temporary ghost that will be
|
||||
// removed only once we draw the touch surface, to avoid flickering that would
|
||||
// happen when removing the ghost too early (before the touch surface is drawn).
|
||||
GhostView.addGhost(touchSurface, decorView)
|
||||
|
||||
touchSurface.viewTreeObserver.addOnPreDrawListener(object : OnPreDrawListener {
|
||||
override fun onPreDraw(): Boolean {
|
||||
touchSurface.viewTreeObserver.removeOnPreDrawListener(this)
|
||||
|
||||
// Now that the touch surface was drawn, we can remove the temporary ghost
|
||||
// and instantly dismiss the dialog.
|
||||
GhostView.removeGhost(touchSurface)
|
||||
onAnimationFinished(true /* instantDismiss */)
|
||||
onDialogDismissed(this@AnimatedDialog)
|
||||
|
||||
return true
|
||||
}
|
||||
// Make sure that the removal of the ghost and making the touch surface visible is
|
||||
// done at the same time.
|
||||
synchronizeNextDraw(then = {
|
||||
onAnimationFinished(true /* instantDismiss */)
|
||||
onDialogDismissed(this@AnimatedDialog)
|
||||
})
|
||||
touchSurface.invalidate()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -27,25 +27,19 @@ 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 android.view.animation.Interpolator
|
||||
import com.android.systemui.animation.Interpolators.LINEAR
|
||||
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
|
||||
class LaunchAnimator(
|
||||
private val timings: Timings,
|
||||
private val interpolators: Interpolators
|
||||
) {
|
||||
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)
|
||||
|
||||
/**
|
||||
@@ -53,23 +47,20 @@ class LaunchAnimator @JvmOverloads constructor(
|
||||
* sub-animation starting [delay] ms after the launch animation and that lasts [duration].
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getProgress(linearProgress: Float, delay: Long, duration: Long): Float {
|
||||
fun getProgress(
|
||||
timings: Timings,
|
||||
linearProgress: Float,
|
||||
delay: Long,
|
||||
duration: Long
|
||||
): Float {
|
||||
return MathUtils.constrain(
|
||||
(linearProgress * ANIMATION_DURATION - delay) / duration,
|
||||
(linearProgress * timings.totalDuration - 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)
|
||||
|
||||
@@ -159,6 +150,45 @@ class LaunchAnimator @JvmOverloads constructor(
|
||||
fun cancel()
|
||||
}
|
||||
|
||||
/** The timings (durations and delays) used by this animator. */
|
||||
class Timings(
|
||||
/** The total duration of the animation. */
|
||||
val totalDuration: Long,
|
||||
|
||||
/** The time to wait before fading out the expanding content. */
|
||||
val contentBeforeFadeOutDelay: Long,
|
||||
|
||||
/** The duration of the expanding content fade out. */
|
||||
val contentBeforeFadeOutDuration: Long,
|
||||
|
||||
/**
|
||||
* The time to wait before fading in the expanded content (usually an activity or dialog
|
||||
* window).
|
||||
*/
|
||||
val contentAfterFadeInDelay: Long,
|
||||
|
||||
/** The duration of the expanded content fade in. */
|
||||
val contentAfterFadeInDuration: Long
|
||||
)
|
||||
|
||||
/** The interpolators used by this animator. */
|
||||
data class Interpolators(
|
||||
/** The interpolator used for the Y position, width, height and corner radius. */
|
||||
val positionInterpolator: Interpolator,
|
||||
|
||||
/**
|
||||
* The interpolator used for the X position. This can be different than
|
||||
* [positionInterpolator] to create an arc-path during the animation.
|
||||
*/
|
||||
val positionXInterpolator: Interpolator = positionInterpolator,
|
||||
|
||||
/** The interpolator used when fading out the expanding content. */
|
||||
val contentBeforeFadeOutInterpolator: Interpolator,
|
||||
|
||||
/** The interpolator used when fading in the expanded content. */
|
||||
val contentAfterFadeInInterpolator: Interpolator
|
||||
)
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -221,8 +251,8 @@ class LaunchAnimator @JvmOverloads constructor(
|
||||
|
||||
// Update state.
|
||||
val animator = ValueAnimator.ofFloat(0f, 1f)
|
||||
animator.duration = if (isForTesting) 0 else ANIMATION_DURATION
|
||||
animator.interpolator = Interpolators.LINEAR
|
||||
animator.duration = timings.totalDuration
|
||||
animator.interpolator = LINEAR
|
||||
|
||||
val launchContainerOverlay = launchContainer.overlay
|
||||
var cancelled = false
|
||||
@@ -260,8 +290,8 @@ class LaunchAnimator @JvmOverloads constructor(
|
||||
// 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 progress = interpolators.positionInterpolator.getInterpolation(linearProgress)
|
||||
val xProgress = interpolators.positionXInterpolator.getInterpolation(linearProgress)
|
||||
|
||||
val xCenter = MathUtils.lerp(startCenterX, endCenterX, xProgress)
|
||||
val halfWidth = MathUtils.lerp(startWidth, endWidth, progress) / 2f
|
||||
@@ -278,7 +308,12 @@ class LaunchAnimator @JvmOverloads constructor(
|
||||
|
||||
// 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
|
||||
state.visible = getProgress(
|
||||
timings,
|
||||
linearProgress,
|
||||
timings.contentBeforeFadeOutDelay,
|
||||
timings.contentBeforeFadeOutDuration
|
||||
) < 1
|
||||
|
||||
applyStateToWindowBackgroundLayer(
|
||||
windowBackgroundLayer,
|
||||
@@ -337,14 +372,25 @@ class LaunchAnimator @JvmOverloads constructor(
|
||||
|
||||
// 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)
|
||||
val fadeInProgress = getProgress(
|
||||
timings,
|
||||
linearProgress,
|
||||
timings.contentBeforeFadeOutDelay,
|
||||
timings.contentBeforeFadeOutDuration
|
||||
)
|
||||
if (fadeInProgress < 1) {
|
||||
val alpha = Interpolators.LINEAR_OUT_SLOW_IN.getInterpolation(fadeInProgress)
|
||||
val alpha =
|
||||
interpolators.contentBeforeFadeOutInterpolator.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)
|
||||
timings,
|
||||
linearProgress,
|
||||
timings.contentAfterFadeInDelay,
|
||||
timings.contentAfterFadeInDuration
|
||||
)
|
||||
val alpha =
|
||||
1 - interpolators.contentAfterFadeInInterpolator.getInterpolation(fadeOutProgress)
|
||||
drawable.alpha = (alpha * 0xFF).roundToInt()
|
||||
|
||||
if (drawHole) {
|
||||
|
||||
@@ -25,7 +25,6 @@ import android.service.dreams.IDreamManager;
|
||||
import com.android.internal.statusbar.IStatusBarService;
|
||||
import com.android.systemui.animation.ActivityLaunchAnimator;
|
||||
import com.android.systemui.animation.DialogLaunchAnimator;
|
||||
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;
|
||||
@@ -301,24 +300,15 @@ public interface StatusBarDependenciesModule {
|
||||
*/
|
||||
@Provides
|
||||
@SysUISingleton
|
||||
static LaunchAnimator provideLaunchAnimator(Context context) {
|
||||
return new LaunchAnimator(context);
|
||||
static ActivityLaunchAnimator provideActivityLaunchAnimator() {
|
||||
return new ActivityLaunchAnimator();
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
@Provides
|
||||
@SysUISingleton
|
||||
static ActivityLaunchAnimator provideActivityLaunchAnimator(LaunchAnimator launchAnimator) {
|
||||
return new ActivityLaunchAnimator(launchAnimator);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
@Provides
|
||||
@SysUISingleton
|
||||
static DialogLaunchAnimator provideDialogLaunchAnimator(Context context,
|
||||
LaunchAnimator launchAnimator, IDreamManager dreamManager) {
|
||||
return new DialogLaunchAnimator(context, launchAnimator, dreamManager);
|
||||
static DialogLaunchAnimator provideDialogLaunchAnimator(IDreamManager dreamManager) {
|
||||
return new DialogLaunchAnimator(dreamManager);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ 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
|
||||
@@ -55,6 +56,7 @@ class ExpandAnimationParameters(
|
||||
}
|
||||
|
||||
fun getProgress(delay: Long, duration: Long): Float {
|
||||
return LaunchAnimator.getProgress(linearProgress, delay, duration)
|
||||
return LaunchAnimator.getProgress(ActivityLaunchAnimator.TIMINGS, linearProgress, delay,
|
||||
duration)
|
||||
}
|
||||
}
|
||||
@@ -110,6 +110,7 @@ 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;
|
||||
@@ -225,7 +226,8 @@ public class NotificationPanelViewController extends PanelViewController {
|
||||
*/
|
||||
private static final int FLING_HIDE = 2;
|
||||
private static final long ANIMATION_DELAY_ICON_FADE_IN =
|
||||
LaunchAnimator.ANIMATION_DURATION - CollapsedStatusBarFragment.FADE_IN_DURATION
|
||||
ActivityLaunchAnimator.TIMINGS.getTotalDuration()
|
||||
- CollapsedStatusBarFragment.FADE_IN_DURATION
|
||||
- CollapsedStatusBarFragment.FADE_IN_DELAY - 48;
|
||||
|
||||
private final DozeParameters mDozeParameters;
|
||||
@@ -3629,8 +3631,8 @@ public class NotificationPanelViewController extends PanelViewController {
|
||||
}
|
||||
|
||||
public void applyLaunchAnimationProgress(float linearProgress) {
|
||||
boolean hideIcons = LaunchAnimator.getProgress(linearProgress,
|
||||
ANIMATION_DELAY_ICON_FADE_IN, 100) == 0.0f;
|
||||
boolean hideIcons = LaunchAnimator.getProgress(ActivityLaunchAnimator.TIMINGS,
|
||||
linearProgress, ANIMATION_DELAY_ICON_FADE_IN, 100) == 0.0f;
|
||||
if (hideIcons != mHideIconsDuringLaunchAnimation) {
|
||||
mHideIconsDuringLaunchAnimation = hideIcons;
|
||||
if (!hideIcons) {
|
||||
|
||||
@@ -23,7 +23,8 @@ class StatusBarLaunchAnimatorController(
|
||||
delegate.onLaunchAnimationStart(isExpandingFullyAbove)
|
||||
statusBar.notificationPanelViewController.setIsLaunchAnimationRunning(true)
|
||||
if (!isExpandingFullyAbove) {
|
||||
statusBar.collapsePanelWithDuration(LaunchAnimator.ANIMATION_DURATION.toInt())
|
||||
statusBar.collapsePanelWithDuration(
|
||||
ActivityLaunchAnimator.TIMINGS.totalDuration.toInt())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ import org.mockito.junit.MockitoJUnit
|
||||
@RunWithLooper
|
||||
class ActivityLaunchAnimatorTest : SysuiTestCase() {
|
||||
private val launchContainer = LinearLayout(mContext)
|
||||
private val launchAnimator = LaunchAnimator(mContext, isForTesting = true)
|
||||
private val launchAnimator = LaunchAnimator(TEST_TIMINGS, TEST_INTERPOLATORS)
|
||||
@Mock lateinit var callback: ActivityLaunchAnimator.Callback
|
||||
@Spy private val controller = TestLaunchAnimatorController(launchContainer)
|
||||
@Mock lateinit var iCallback: IRemoteAnimationFinishedCallback
|
||||
|
||||
@@ -33,7 +33,7 @@ import org.mockito.junit.MockitoJUnit
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
@TestableLooper.RunWithLooper
|
||||
class DialogLaunchAnimatorTest : SysuiTestCase() {
|
||||
private val launchAnimator = LaunchAnimator(context, isForTesting = true)
|
||||
private val launchAnimator = LaunchAnimator(TEST_TIMINGS, TEST_INTERPOLATORS)
|
||||
private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
|
||||
private val attachedViews = mutableSetOf<View>()
|
||||
|
||||
@@ -42,7 +42,8 @@ class DialogLaunchAnimatorTest : SysuiTestCase() {
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
dialogLaunchAnimator = DialogLaunchAnimator(context, launchAnimator, dreamManager)
|
||||
dialogLaunchAnimator = DialogLaunchAnimator(
|
||||
dreamManager, launchAnimator, isForTesting = true)
|
||||
}
|
||||
|
||||
@After
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.android.systemui.animation
|
||||
|
||||
/**
|
||||
* A [LaunchAnimator.Timings] to be used in tests.
|
||||
*
|
||||
* Note that all timings except the total duration are non-zero to avoid divide-by-zero exceptions
|
||||
* when computing the progress of a sub-animation (the contents fade in/out).
|
||||
*/
|
||||
val TEST_TIMINGS = LaunchAnimator.Timings(
|
||||
totalDuration = 0L,
|
||||
contentBeforeFadeOutDelay = 1L,
|
||||
contentBeforeFadeOutDuration = 1L,
|
||||
contentAfterFadeInDelay = 1L,
|
||||
contentAfterFadeInDuration = 1L
|
||||
)
|
||||
|
||||
/** A [LaunchAnimator.Interpolators] to be used in tests. */
|
||||
val TEST_INTERPOLATORS = LaunchAnimator.Interpolators(
|
||||
positionInterpolator = Interpolators.STANDARD,
|
||||
positionXInterpolator = Interpolators.STANDARD,
|
||||
contentBeforeFadeOutInterpolator = Interpolators.STANDARD,
|
||||
contentAfterFadeInInterpolator = Interpolators.STANDARD
|
||||
)
|
||||
Reference in New Issue
Block a user