mirror of
https://github.com/LawnchairLauncher/lawnchair.git
synced 2026-03-04 09:56:49 +00:00
Add animation runner for alt-tab desktop app launch
Create a remote transition supporting alt-tab app-launches (app launches caused by a user using alt-tab to select a minimized app) and any minimization caused by hitting the window limit during such an app launch. Test: manual, and WindowAnimatorTest Bug: 349791584 Flag: com.android.window.flags.enable_desktop_app_launch_alttab_transitions Change-Id: I6474fff351f3d7681ca25cd7331e4955e3d1c6e0
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.launcher3.desktop
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.os.IBinder
|
||||
import android.view.SurfaceControl.Transaction
|
||||
import android.view.WindowManager.TRANSIT_OPEN
|
||||
import android.view.WindowManager.TRANSIT_TO_BACK
|
||||
import android.view.WindowManager.TRANSIT_TO_FRONT
|
||||
import android.window.IRemoteTransitionFinishedCallback
|
||||
import android.window.RemoteTransitionStub
|
||||
import android.window.TransitionInfo
|
||||
import android.window.TransitionInfo.Change
|
||||
import androidx.core.animation.addListener
|
||||
import com.android.app.animation.Interpolators
|
||||
import com.android.quickstep.RemoteRunnable
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
/**
|
||||
* [android.window.RemoteTransition] for Desktop app launches.
|
||||
*
|
||||
* This transition supports minimize-changes, i.e. in a launch-transition, if a window is moved back
|
||||
* ([android.view.WindowManager.TRANSIT_TO_BACK]) this transition will apply a minimize animation to
|
||||
* that window.
|
||||
*/
|
||||
class DesktopAppLaunchTransition(private val context: Context, private val mainExecutor: Executor) :
|
||||
RemoteTransitionStub() {
|
||||
|
||||
override fun startAnimation(
|
||||
token: IBinder,
|
||||
info: TransitionInfo,
|
||||
t: Transaction,
|
||||
transitionFinishedCallback: IRemoteTransitionFinishedCallback,
|
||||
) {
|
||||
val safeTransitionFinishedCallback = RemoteRunnable {
|
||||
transitionFinishedCallback.onTransitionFinished(/* wct= */ null, /* sct= */ null)
|
||||
}
|
||||
mainExecutor.execute {
|
||||
runAnimators(info, safeTransitionFinishedCallback)
|
||||
t.apply()
|
||||
}
|
||||
}
|
||||
|
||||
private fun runAnimators(info: TransitionInfo, finishedCallback: RemoteRunnable) {
|
||||
val animators = mutableListOf<Animator>()
|
||||
val animatorFinishedCallback: (Animator) -> Unit = { animator ->
|
||||
animators -= animator
|
||||
if (animators.isEmpty()) finishedCallback.run()
|
||||
}
|
||||
animators += createAnimators(info, animatorFinishedCallback)
|
||||
animators.forEach { it.start() }
|
||||
}
|
||||
|
||||
private fun createAnimators(
|
||||
info: TransitionInfo,
|
||||
finishCallback: (Animator) -> Unit,
|
||||
): List<Animator> {
|
||||
val transaction = Transaction()
|
||||
val launchAnimator =
|
||||
createLaunchAnimator(getLaunchChange(info), transaction, finishCallback)
|
||||
val minimizeChange = getMinimizeChange(info) ?: return listOf(launchAnimator)
|
||||
val minimizeAnimator = createMinimizeAnimator(minimizeChange, transaction, finishCallback)
|
||||
return listOf(launchAnimator, minimizeAnimator)
|
||||
}
|
||||
|
||||
private fun getLaunchChange(info: TransitionInfo): Change =
|
||||
requireNotNull(info.changes.firstOrNull { change -> change.mode in LAUNCH_CHANGE_MODES }) {
|
||||
"expected an app launch Change"
|
||||
}
|
||||
|
||||
private fun getMinimizeChange(info: TransitionInfo): Change? =
|
||||
info.changes.firstOrNull { change -> change.mode == TRANSIT_TO_BACK }
|
||||
|
||||
private fun createLaunchAnimator(
|
||||
change: Change,
|
||||
transaction: Transaction,
|
||||
onAnimFinish: (Animator) -> Unit,
|
||||
): Animator {
|
||||
val boundsAnimator =
|
||||
WindowAnimator.createBoundsAnimator(
|
||||
context,
|
||||
launchBoundsAnimationDef,
|
||||
change,
|
||||
transaction,
|
||||
)
|
||||
val alphaAnimator =
|
||||
ValueAnimator.ofFloat(0f, 1f).apply {
|
||||
duration = LAUNCH_ANIM_ALPHA_DURATION_MS
|
||||
interpolator = Interpolators.LINEAR
|
||||
addUpdateListener { animation ->
|
||||
transaction.setAlpha(change.leash, animation.animatedValue as Float).apply()
|
||||
}
|
||||
}
|
||||
return AnimatorSet().apply {
|
||||
playTogether(boundsAnimator, alphaAnimator)
|
||||
addListener(onEnd = { animation -> onAnimFinish(animation) })
|
||||
}
|
||||
}
|
||||
|
||||
private fun createMinimizeAnimator(
|
||||
change: Change,
|
||||
transaction: Transaction,
|
||||
onAnimFinish: (Animator) -> Unit,
|
||||
): Animator {
|
||||
val boundsAnimator =
|
||||
WindowAnimator.createBoundsAnimator(
|
||||
context,
|
||||
minimizeBoundsAnimationDef,
|
||||
change,
|
||||
transaction,
|
||||
)
|
||||
val alphaAnimator =
|
||||
ValueAnimator.ofFloat(1f, 0f).apply {
|
||||
duration = MINIMIZE_ANIM_ALPHA_DURATION_MS
|
||||
interpolator = Interpolators.LINEAR
|
||||
addUpdateListener { animation ->
|
||||
transaction.setAlpha(change.leash, animation.animatedValue as Float).apply()
|
||||
}
|
||||
}
|
||||
return AnimatorSet().apply {
|
||||
playTogether(boundsAnimator, alphaAnimator)
|
||||
addListener(onEnd = { animation -> onAnimFinish(animation) })
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LAUNCH_CHANGE_MODES = intArrayOf(TRANSIT_OPEN, TRANSIT_TO_FRONT)
|
||||
|
||||
private const val LAUNCH_ANIM_ALPHA_DURATION_MS = 100L
|
||||
private const val MINIMIZE_ANIM_ALPHA_DURATION_MS = 100L
|
||||
|
||||
private val launchBoundsAnimationDef =
|
||||
WindowAnimator.BoundsAnimationParams(
|
||||
durationMs = 300,
|
||||
startOffsetYDp = 12f,
|
||||
startScale = 0.97f,
|
||||
interpolator = Interpolators.STANDARD_DECELERATE,
|
||||
)
|
||||
|
||||
private val minimizeBoundsAnimationDef =
|
||||
WindowAnimator.BoundsAnimationParams(
|
||||
durationMs = 200,
|
||||
endOffsetYDp = 12f,
|
||||
endScale = 0.97f,
|
||||
interpolator = Interpolators.STANDARD_ACCELERATE,
|
||||
)
|
||||
}
|
||||
}
|
||||
100
quickstep/src/com/android/launcher3/desktop/WindowAnimator.kt
Normal file
100
quickstep/src/com/android/launcher3/desktop/WindowAnimator.kt
Normal file
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.launcher3.desktop
|
||||
|
||||
import android.animation.RectEvaluator
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.util.TypedValue
|
||||
import android.view.SurfaceControl
|
||||
import android.view.animation.Interpolator
|
||||
import android.window.TransitionInfo
|
||||
|
||||
/** Creates animations that can be applied to windows/surfaces. */
|
||||
object WindowAnimator {
|
||||
|
||||
/** Parameters defining a window bounds animation. */
|
||||
data class BoundsAnimationParams(
|
||||
val durationMs: Long,
|
||||
val startOffsetYDp: Float = 0f,
|
||||
val endOffsetYDp: Float = 0f,
|
||||
val startScale: Float = 1f,
|
||||
val endScale: Float = 1f,
|
||||
val interpolator: Interpolator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates an animator to reposition and scale the bounds of the leash of the given change.
|
||||
*
|
||||
* @param boundsAnimDef the parameters for the animation itself (duration, scale, position)
|
||||
* @param change the change to which the animation should be applied
|
||||
* @param transaction the transaction to apply the animation to
|
||||
*/
|
||||
fun createBoundsAnimator(
|
||||
context: Context,
|
||||
boundsAnimDef: BoundsAnimationParams,
|
||||
change: TransitionInfo.Change,
|
||||
transaction: SurfaceControl.Transaction,
|
||||
): ValueAnimator {
|
||||
val startBounds =
|
||||
createBounds(
|
||||
context,
|
||||
change.startAbsBounds,
|
||||
boundsAnimDef.startScale,
|
||||
boundsAnimDef.startOffsetYDp,
|
||||
)
|
||||
val leash = change.leash
|
||||
val endBounds =
|
||||
createBounds(
|
||||
context,
|
||||
change.startAbsBounds,
|
||||
boundsAnimDef.endScale,
|
||||
boundsAnimDef.endOffsetYDp,
|
||||
)
|
||||
return ValueAnimator.ofObject(RectEvaluator(), startBounds, endBounds).apply {
|
||||
duration = boundsAnimDef.durationMs
|
||||
interpolator = boundsAnimDef.interpolator
|
||||
addUpdateListener { animation ->
|
||||
val animBounds = animation.animatedValue as Rect
|
||||
val animScale = 1 - (1 - boundsAnimDef.endScale) * animation.animatedFraction
|
||||
transaction
|
||||
.setPosition(leash, animBounds.left.toFloat(), animBounds.top.toFloat())
|
||||
.setScale(leash, animScale, animScale)
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createBounds(context: Context, origBounds: Rect, scale: Float, offsetYDp: Float) =
|
||||
Rect(origBounds).apply {
|
||||
check(scale in 0.0..1.0)
|
||||
// Scale the bounds down with an anchor in the center
|
||||
inset(
|
||||
(origBounds.width().toFloat() * (1 - scale) / 2).toInt(),
|
||||
(origBounds.height().toFloat() * (1 - scale) / 2).toInt(),
|
||||
)
|
||||
val offsetYPx =
|
||||
TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
offsetYDp,
|
||||
context.resources.displayMetrics,
|
||||
)
|
||||
.toInt()
|
||||
offset(/* dx= */ 0, offsetYPx)
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.android.launcher3.taskbar;
|
||||
|
||||
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
|
||||
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
|
||||
|
||||
import android.animation.Animator;
|
||||
@@ -31,6 +32,7 @@ import androidx.annotation.Nullable;
|
||||
import com.android.internal.jank.Cuj;
|
||||
import com.android.launcher3.Utilities;
|
||||
import com.android.launcher3.anim.AnimatorListeners;
|
||||
import com.android.launcher3.desktop.DesktopAppLaunchTransition;
|
||||
import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
|
||||
import com.android.launcher3.taskbar.overlay.TaskbarOverlayDragLayer;
|
||||
import com.android.launcher3.views.BaseDragLayer;
|
||||
@@ -41,6 +43,7 @@ import com.android.systemui.shared.recents.model.Task;
|
||||
import com.android.systemui.shared.recents.model.ThumbnailData;
|
||||
import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
|
||||
import com.android.systemui.shared.system.QuickStepContract;
|
||||
import com.android.window.flags.Flags;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.util.List;
|
||||
@@ -181,7 +184,7 @@ public class KeyboardQuickSwitchViewController {
|
||||
Runnable onFinishCallback = () -> InteractionJankMonitorWrapper.end(
|
||||
Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH);
|
||||
TaskbarActivityContext context = mControllers.taskbarActivityContext;
|
||||
RemoteTransition remoteTransition = new RemoteTransition(new SlideInRemoteTransition(
|
||||
final RemoteTransition slideInTransition = new RemoteTransition(new SlideInRemoteTransition(
|
||||
Utilities.isRtl(mControllers.taskbarActivityContext.getResources()),
|
||||
context.getDeviceProfile().overviewPageSpacing,
|
||||
QuickStepContract.getWindowCornerRadius(context),
|
||||
@@ -195,7 +198,7 @@ public class KeyboardQuickSwitchViewController {
|
||||
SystemUiProxy.INSTANCE.get(mKeyboardQuickSwitchView.getContext())
|
||||
.showDesktopApps(
|
||||
mKeyboardQuickSwitchView.getDisplay().getDisplayId(),
|
||||
remoteTransition));
|
||||
slideInTransition));
|
||||
return -1;
|
||||
}
|
||||
// Even with a valid index, this can be null if the user tries to quick switch before the
|
||||
@@ -208,6 +211,13 @@ public class KeyboardQuickSwitchViewController {
|
||||
// Ignore attempts to run the selected task if it is already running.
|
||||
return -1;
|
||||
}
|
||||
RemoteTransition remoteTransition = slideInTransition;
|
||||
if (mOnDesktop && task.task1.isMinimized
|
||||
&& Flags.enableDesktopAppLaunchAlttabTransitions()) {
|
||||
// This app is being unminimized - use our own transition runner.
|
||||
remoteTransition = new RemoteTransition(
|
||||
new DesktopAppLaunchTransition(context, MAIN_EXECUTOR));
|
||||
}
|
||||
mControllers.taskbarActivityContext.handleGroupTaskLaunch(
|
||||
task,
|
||||
remoteTransition,
|
||||
|
||||
@@ -1234,7 +1234,8 @@ public class TaskbarActivityContext extends BaseTaskbarContext {
|
||||
}
|
||||
} else if (tag instanceof TaskItemInfo info) {
|
||||
UI_HELPER_EXECUTOR.execute(() ->
|
||||
SystemUiProxy.INSTANCE.get(this).showDesktopApp(info.getTaskId()));
|
||||
SystemUiProxy.INSTANCE.get(this).showDesktopApp(
|
||||
info.getTaskId(), /* remoteTransition= */ null));
|
||||
mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(
|
||||
/* stash= */ true);
|
||||
} else if (tag instanceof WorkspaceItemInfo) {
|
||||
@@ -1325,7 +1326,8 @@ public class TaskbarActivityContext extends BaseTaskbarContext {
|
||||
GroupTask task,
|
||||
@Nullable RemoteTransition remoteTransition,
|
||||
boolean onDesktop) {
|
||||
handleGroupTaskLaunch(task, remoteTransition, onDesktop, null, null);
|
||||
handleGroupTaskLaunch(task, remoteTransition, onDesktop,
|
||||
/* onStartCallback= */ null, /* onFinishCallback= */ null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1349,17 +1351,24 @@ public class TaskbarActivityContext extends BaseTaskbarContext {
|
||||
UI_HELPER_EXECUTOR.execute(() ->
|
||||
SystemUiProxy.INSTANCE.get(this).showDesktopApps(getDisplay().getDisplayId(),
|
||||
remoteTransition));
|
||||
} else if (onDesktop) {
|
||||
return;
|
||||
}
|
||||
if (onDesktop) {
|
||||
boolean useRemoteTransition = task.task1.isMinimized
|
||||
&& com.android.window.flags.Flags.enableDesktopAppLaunchAlttabTransitions();
|
||||
UI_HELPER_EXECUTOR.execute(() -> {
|
||||
if (onStartCallback != null) {
|
||||
onStartCallback.run();
|
||||
}
|
||||
SystemUiProxy.INSTANCE.get(this).showDesktopApp(task.task1.key.id);
|
||||
SystemUiProxy.INSTANCE.get(this).showDesktopApp(
|
||||
task.task1.key.id, useRemoteTransition ? remoteTransition : null);
|
||||
if (onFinishCallback != null) {
|
||||
onFinishCallback.run();
|
||||
}
|
||||
});
|
||||
} else if (task.task2 == null) {
|
||||
return;
|
||||
}
|
||||
if (task.task2 == null) {
|
||||
UI_HELPER_EXECUTOR.execute(() -> {
|
||||
ActivityOptions activityOptions =
|
||||
makeDefaultActivityOptions(SPLASH_SCREEN_STYLE_UNDEFINED).options;
|
||||
@@ -1368,9 +1377,9 @@ public class TaskbarActivityContext extends BaseTaskbarContext {
|
||||
ActivityManagerWrapper.getInstance().startActivityFromRecents(
|
||||
task.task1.key, activityOptions);
|
||||
});
|
||||
} else {
|
||||
mControllers.uiController.launchSplitTasks(task, remoteTransition);
|
||||
return;
|
||||
}
|
||||
mControllers.uiController.launchSplitTasks(task, remoteTransition);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1436,10 +1436,10 @@ public class SystemUiProxy implements ISystemUiProxy, NavHandle, SafeCloseable {
|
||||
/**
|
||||
* If task with the given id is on the desktop, bring it to front
|
||||
*/
|
||||
public void showDesktopApp(int taskId) {
|
||||
public void showDesktopApp(int taskId, @Nullable RemoteTransition transition) {
|
||||
if (mDesktopMode != null) {
|
||||
try {
|
||||
mDesktopMode.showDesktopApp(taskId);
|
||||
mDesktopMode.showDesktopApp(taskId, transition);
|
||||
} catch (RemoteException e) {
|
||||
Log.w(TAG, "Failed call showDesktopApp", e);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.quickstep.desktop
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Rect
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.SurfaceControl
|
||||
import android.window.TransitionInfo
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.SmallTest
|
||||
import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread
|
||||
import com.android.app.animation.Interpolators
|
||||
import com.android.launcher3.desktop.WindowAnimator
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentMatchers.any
|
||||
import org.mockito.ArgumentMatchers.anyFloat
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.whenever
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class WindowAnimatorTest {
|
||||
|
||||
private val context = mock<Context>()
|
||||
private val resources = mock<Resources>()
|
||||
private val transaction = mock<SurfaceControl.Transaction>()
|
||||
private val change = mock<TransitionInfo.Change>()
|
||||
private val leash = mock<SurfaceControl>()
|
||||
|
||||
private val displayMetrics = DisplayMetrics().apply { density = 1f }
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
whenever(context.resources).thenReturn(resources)
|
||||
whenever(resources.displayMetrics).thenReturn(displayMetrics)
|
||||
whenever(change.leash).thenReturn(leash)
|
||||
whenever(change.startAbsBounds).thenReturn(START_BOUNDS)
|
||||
whenever(transaction.setPosition(any(), anyFloat(), anyFloat())).thenReturn(transaction)
|
||||
whenever(transaction.setScale(any(), anyFloat(), anyFloat())).thenReturn(transaction)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createBoundsAnimator_returnsCorrectDefaultAnimatorParams() = runOnUiThread {
|
||||
val boundsAnimParams =
|
||||
WindowAnimator.BoundsAnimationParams(
|
||||
durationMs = 100L,
|
||||
interpolator = Interpolators.STANDARD_ACCELERATE,
|
||||
)
|
||||
|
||||
val valueAnimator =
|
||||
WindowAnimator.createBoundsAnimator(context, boundsAnimParams, change, transaction)
|
||||
|
||||
assertThat(valueAnimator.duration).isEqualTo(100L)
|
||||
assertThat(valueAnimator.interpolator).isEqualTo(Interpolators.STANDARD_ACCELERATE)
|
||||
assertStartAndEndBounds(valueAnimator, startBounds = START_BOUNDS, endBounds = START_BOUNDS)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createBoundsAnimator_startScaleAndOffset_returnsCorrectBounds() = runOnUiThread {
|
||||
val bounds = Rect(/* left= */ 100, /* top= */ 200, /* right= */ 300, /* bottom= */ 400)
|
||||
whenever(change.startAbsBounds).thenReturn(bounds)
|
||||
val boundsAnimParams =
|
||||
WindowAnimator.BoundsAnimationParams(
|
||||
durationMs = 100L,
|
||||
startOffsetYDp = 10f,
|
||||
startScale = 0.5f,
|
||||
interpolator = Interpolators.STANDARD_ACCELERATE,
|
||||
)
|
||||
|
||||
val valueAnimator =
|
||||
WindowAnimator.createBoundsAnimator(context, boundsAnimParams, change, transaction)
|
||||
|
||||
assertStartAndEndBounds(
|
||||
valueAnimator,
|
||||
startBounds =
|
||||
Rect(/* left= */ 150, /* top= */ 260, /* right= */ 250, /* bottom= */ 360),
|
||||
endBounds = bounds,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createBoundsAnimator_endScaleAndOffset_returnsCorrectBounds() = runOnUiThread {
|
||||
val bounds = Rect(/* left= */ 100, /* top= */ 200, /* right= */ 300, /* bottom= */ 400)
|
||||
whenever(change.startAbsBounds).thenReturn(bounds)
|
||||
val boundsAnimParams =
|
||||
WindowAnimator.BoundsAnimationParams(
|
||||
durationMs = 100L,
|
||||
endOffsetYDp = 10f,
|
||||
endScale = 0.5f,
|
||||
interpolator = Interpolators.STANDARD_ACCELERATE,
|
||||
)
|
||||
|
||||
val valueAnimator =
|
||||
WindowAnimator.createBoundsAnimator(context, boundsAnimParams, change, transaction)
|
||||
|
||||
assertStartAndEndBounds(
|
||||
valueAnimator,
|
||||
startBounds = bounds,
|
||||
endBounds = Rect(/* left= */ 150, /* top= */ 260, /* right= */ 250, /* bottom= */ 360),
|
||||
)
|
||||
}
|
||||
|
||||
private fun assertStartAndEndBounds(
|
||||
valueAnimator: ValueAnimator,
|
||||
startBounds: Rect,
|
||||
endBounds: Rect,
|
||||
) {
|
||||
valueAnimator.start()
|
||||
valueAnimator.animatedValue
|
||||
assertThat(valueAnimator.animatedValue).isEqualTo(startBounds)
|
||||
valueAnimator.end()
|
||||
assertThat(valueAnimator.animatedValue).isEqualTo(endBounds)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val START_BOUNDS =
|
||||
Rect(/* left= */ 10, /* top= */ 20, /* right= */ 30, /* bottom= */ 40)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user