Improve dismiss split transition

Improve this transition by seperating it as 2 transitions:
1. Only dismiss invisible split side and extand another one side
   root task as full screen bounds.
2. After first transition finished, fade-out the decor surface
   and reparent the child task to TDA top.

Bug: 200245358
Test: manual
Change-Id: I1a0c4d1715765e7de0ce2628e978cab392d2b07e
This commit is contained in:
Tony Huang
2022-06-17 18:31:41 +08:00
parent 0641bef299
commit d656ee9d28
9 changed files with 144 additions and 49 deletions

View File

@@ -50,6 +50,8 @@ import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.R;
import com.android.wm.shell.common.SurfaceUtils;
import java.util.function.Consumer;
/**
* Handles split decor like showing resizing hint for a specific split.
*/
@@ -212,7 +214,7 @@ public class SplitDecorManager extends WindowlessWindowManager {
newBounds.height() / 2 - mIconSize / 2);
if (animate) {
startFadeAnimation(show, false /* isResized */);
startFadeAnimation(show, null /* finishedConsumer */);
mShown = show;
}
}
@@ -243,15 +245,29 @@ public class SplitDecorManager extends WindowlessWindowManager {
mFadeAnimator.cancel();
}
if (mShown) {
startFadeAnimation(false /* show */, true /* isResized */);
mShown = false;
fadeOutDecor(null /* finishedCallback */);
} else {
// Decor surface is hidden so release it directly.
releaseDecor(t);
}
}
private void startFadeAnimation(boolean show, boolean isResized) {
/** Fade-out decor surface with animation end callback, if decor is hidden, run the callback
* directly. */
public void fadeOutDecor(Runnable finishedCallback) {
if (mShown) {
startFadeAnimation(false /* show */, transaction -> {
releaseDecor(transaction);
if (finishedCallback != null) finishedCallback.run();
});
mShown = false;
} else {
if (finishedCallback != null) finishedCallback.run();
}
}
private void startFadeAnimation(boolean show,
Consumer<SurfaceControl.Transaction> finishedConsumer) {
final SurfaceControl.Transaction animT = new SurfaceControl.Transaction();
mFadeAnimator = ValueAnimator.ofFloat(0f, 1f);
mFadeAnimator.setDuration(FADE_DURATION);
@@ -285,8 +301,8 @@ public class SplitDecorManager extends WindowlessWindowManager {
animT.hide(mIconLeash);
}
}
if (isResized) {
releaseDecor(animT);
if (finishedConsumer != null) {
finishedConsumer.accept(animT);
}
animT.apply();
animT.close();

View File

@@ -32,6 +32,7 @@ import static com.android.wm.shell.animation.Interpolators.SLOWDOWN_INTERPOLATOR
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -449,11 +450,13 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
switch (snapTarget.flag) {
case FLAG_DISMISS_START:
flingDividePosition(currentPosition, snapTarget.position,
() -> mSplitLayoutHandler.onSnappedToDismiss(false /* bottomOrRight */));
() -> mSplitLayoutHandler.onSnappedToDismiss(false /* bottomOrRight */,
EXIT_REASON_DRAG_DIVIDER));
break;
case FLAG_DISMISS_END:
flingDividePosition(currentPosition, snapTarget.position,
() -> mSplitLayoutHandler.onSnappedToDismiss(true /* bottomOrRight */));
() -> mSplitLayoutHandler.onSnappedToDismiss(true /* bottomOrRight */,
EXIT_REASON_DRAG_DIVIDER));
break;
default:
flingDividePosition(currentPosition, snapTarget.position,
@@ -509,6 +512,14 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
isLandscape ? DOCKED_LEFT : DOCKED_TOP /* dockSide */);
}
/** Fling divider from current position to end or start position then exit */
public void flingDividerToDismiss(boolean toEnd, int reason) {
final int target = toEnd ? mDividerSnapAlgorithm.getDismissEndTarget().position
: mDividerSnapAlgorithm.getDismissStartTarget().position;
flingDividePosition(getDividePosition(), target,
() -> mSplitLayoutHandler.onSnappedToDismiss(toEnd, reason));
}
@VisibleForTesting
void flingDividePosition(int from, int to, @Nullable Runnable flingFinishedCallback) {
if (from == to) {
@@ -758,7 +769,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
public interface SplitLayoutHandler {
/** Calls when dismissing split. */
void onSnappedToDismiss(boolean snappedToEnd);
void onSnappedToDismiss(boolean snappedToEnd, int reason);
/**
* Calls when resizing the split bounds.

View File

@@ -45,6 +45,11 @@ class MainStage extends StageTaskListener {
iconProvider);
}
@Override
void dismiss(WindowContainerTransaction wct, boolean toTop) {
deactivate(wct, toTop);
}
boolean isActive() {
return mIsActive;
}

View File

@@ -42,6 +42,11 @@ class SideStage extends StageTaskListener {
iconProvider);
}
@Override
void dismiss(WindowContainerTransaction wct, boolean toTop) {
removeAllTasks(wct, toTop);
}
boolean removeAllTasks(WindowContainerTransaction wct, boolean toTop) {
if (mChildrenTaskInfo.size() == 0) return false;
wct.reparentTasks(

View File

@@ -18,8 +18,6 @@ package com.android.wm.shell.splitscreen;
import static android.app.ActivityManager.START_SUCCESS;
import static android.app.ActivityManager.START_TASK_TO_FRONT;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION;
import static android.view.Display.DEFAULT_DISPLAY;
@@ -107,15 +105,15 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
RemoteCallable<SplitScreenController>, ShellTaskOrganizer.FocusListener {
private static final String TAG = SplitScreenController.class.getSimpleName();
static final int EXIT_REASON_UNKNOWN = 0;
static final int EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW = 1;
static final int EXIT_REASON_APP_FINISHED = 2;
static final int EXIT_REASON_DEVICE_FOLDED = 3;
static final int EXIT_REASON_DRAG_DIVIDER = 4;
static final int EXIT_REASON_RETURN_HOME = 5;
static final int EXIT_REASON_ROOT_TASK_VANISHED = 6;
static final int EXIT_REASON_SCREEN_LOCKED = 7;
static final int EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP = 8;
public static final int EXIT_REASON_UNKNOWN = 0;
public static final int EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW = 1;
public static final int EXIT_REASON_APP_FINISHED = 2;
public static final int EXIT_REASON_DEVICE_FOLDED = 3;
public static final int EXIT_REASON_DRAG_DIVIDER = 4;
public static final int EXIT_REASON_RETURN_HOME = 5;
public static final int EXIT_REASON_ROOT_TASK_VANISHED = 6;
public static final int EXIT_REASON_SCREEN_LOCKED = 7;
public static final int EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP = 8;
public static final int EXIT_REASON_CHILD_TASK_ENTER_PIP = 9;
@IntDef(value = {
EXIT_REASON_UNKNOWN,

View File

@@ -189,6 +189,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
private boolean mShouldUpdateRecents;
private boolean mExitSplitScreenOnHide;
private boolean mIsDividerRemoteAnimating;
private boolean mIsExiting;
private boolean mResizingSplits;
/** The target stage to dismiss to when unlock after folded. */
@@ -734,7 +735,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
private void applyExitSplitScreen(@Nullable StageTaskListener childrenToTop,
WindowContainerTransaction wct, @ExitReason int exitReason) {
if (!mMainStage.isActive()) return;
if (!mMainStage.isActive() || mIsExiting) return;
mRecentTasks.ifPresent(recentTasks -> {
// Notify recents if we are exiting in a way that breaks the pair, and disable further
@@ -746,21 +747,45 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
});
mShouldUpdateRecents = false;
// When the exit split-screen is caused by one of the task enters auto pip,
// we want the tasks to be put to bottom instead of top, otherwise it will end up
// a fullscreen plus a pinned task instead of pinned only at the end of the transition.
final boolean fromEnteringPip = exitReason == EXIT_REASON_CHILD_TASK_ENTER_PIP;
mSideStage.removeAllTasks(wct, !fromEnteringPip && mSideStage == childrenToTop);
mMainStage.deactivate(wct, !fromEnteringPip && mMainStage == childrenToTop);
wct.reorder(mRootTaskInfo.token, false /* onTop */);
mTaskOrganizer.applyTransaction(wct);
if (childrenToTop == null) {
mSideStage.removeAllTasks(wct, false /* toTop */);
mMainStage.deactivate(wct, false /* toTop */);
wct.reorder(mRootTaskInfo.token, false /* onTop */);
onTransitionAnimationComplete();
} else {
// Expand to top side split as full screen for fading out decor animation and dismiss
// another side split(Moving its children to bottom).
mIsExiting = true;
final StageTaskListener tempFullStage = childrenToTop;
final StageTaskListener dismissStage = mMainStage == childrenToTop
? mSideStage : mMainStage;
tempFullStage.resetBounds(wct);
wct.setSmallestScreenWidthDp(tempFullStage.mRootTaskInfo.token,
mRootTaskInfo.configuration.smallestScreenWidthDp);
dismissStage.dismiss(wct, false /* toTop */);
}
mSyncQueue.queue(wct);
mSyncQueue.runInSync(t -> {
t.setWindowCrop(mMainStage.mRootLeash, null)
.setWindowCrop(mSideStage.mRootLeash, null);
t.setPosition(mMainStage.mRootLeash, 0, 0)
.setPosition(mSideStage.mRootLeash, 0, 0);
setDividerVisibility(false, t);
// In this case, exit still under progress, fade out the split decor after first WCT
// done and do remaining WCT after animation finished.
if (childrenToTop != null) {
childrenToTop.fadeOutDecor(() -> {
WindowContainerTransaction finishedWCT = new WindowContainerTransaction();
mIsExiting = false;
childrenToTop.dismiss(finishedWCT, true /* toTop */);
wct.reorder(mRootTaskInfo.token, false /* toTop */);
mTaskOrganizer.applyTransaction(finishedWCT);
onTransitionAnimationComplete();
});
}
});
onTransitionAnimationComplete();
Slog.i(TAG, "applyExitSplitScreen, reason = " + exitReasonToString(exitReason));
// Log the exit
if (childrenToTop != null) {
@@ -930,9 +955,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
}
private void onStageChildTaskEnterPip(StageListenerImpl stageListener, int taskId) {
exitSplitScreen(stageListener == mMainStageListener ? mMainStage : mSideStage,
EXIT_REASON_CHILD_TASK_ENTER_PIP);
private void onStageChildTaskEnterPip() {
// When the exit split-screen is caused by one of the task enters auto pip,
// we want both tasks to be put to bottom instead of top, otherwise it will end up
// a fullscreen plus a pinned task instead of pinned only at the end of the transition.
exitSplitScreen(null, EXIT_REASON_CHILD_TASK_ENTER_PIP);
}
private void updateRecentTasksSplitPair() {
@@ -1178,13 +1205,25 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
private void onStageHasChildrenChanged(StageListenerImpl stageListener) {
final boolean hasChildren = stageListener.mHasChildren;
final boolean isSideStage = stageListener == mSideStageListener;
if (!hasChildren) {
if (!hasChildren && !mIsExiting) {
if (isSideStage && mMainStageListener.mVisible) {
// Exit to main stage if side stage no longer has children.
exitSplitScreen(mMainStage, EXIT_REASON_APP_FINISHED);
if (ENABLE_SHELL_TRANSITIONS) {
exitSplitScreen(mMainStage, EXIT_REASON_APP_FINISHED);
} else {
mSplitLayout.flingDividerToDismiss(
mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT,
EXIT_REASON_APP_FINISHED);
}
} else if (!isSideStage && mSideStageListener.mVisible) {
// Exit to side stage if main stage no longer has children.
exitSplitScreen(mSideStage, EXIT_REASON_APP_FINISHED);
if (ENABLE_SHELL_TRANSITIONS) {
exitSplitScreen(mSideStage, EXIT_REASON_APP_FINISHED);
} else {
mSplitLayout.flingDividerToDismiss(
mSideStagePosition != SPLIT_POSITION_BOTTOM_OR_RIGHT,
EXIT_REASON_APP_FINISHED);
}
}
} else if (isSideStage && !mMainStage.isActive()) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
@@ -1208,12 +1247,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
@Override
public void onSnappedToDismiss(boolean bottomOrRight) {
public void onSnappedToDismiss(boolean bottomOrRight, int reason) {
final boolean mainStageToTop =
bottomOrRight ? mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT
: mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT;
if (!ENABLE_SHELL_TRANSITIONS) {
exitSplitScreen(mainStageToTop ? mMainStage : mSideStage, EXIT_REASON_DRAG_DIVIDER);
exitSplitScreen(mainStageToTop ? mMainStage : mSideStage, reason);
return;
}
@@ -1615,7 +1654,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
/** Called to clean-up state and do house-keeping after the animation is done. */
public void onTransitionAnimationComplete() {
// If still playing, let it finish.
if (!mMainStage.isActive()) {
if (!mMainStage.isActive() && !mIsExiting) {
// Update divider state after animation so that it is still around and positioned
// properly for the animation itself.
mSplitLayout.release();
@@ -1911,8 +1950,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
@Override
public void onChildTaskEnterPip(int taskId) {
StageCoordinator.this.onStageChildTaskEnterPip(this, taskId);
public void onChildTaskEnterPip() {
StageCoordinator.this.onStageChildTaskEnterPip();
}
@Override

View File

@@ -71,7 +71,7 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
void onChildTaskStatusChanged(int taskId, boolean present, boolean visible);
void onChildTaskEnterPip(int taskId);
void onChildTaskEnterPip();
void onRootTaskVanished();
@@ -103,6 +103,11 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this);
}
/**
* General function for dismiss this stage.
*/
void dismiss(WindowContainerTransaction wct, boolean toTop) {}
int getChildCount() {
return mChildrenTaskInfo.size();
}
@@ -255,7 +260,7 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
return;
}
if (taskInfo.getWindowingMode() == WINDOWING_MODE_PINNED) {
mCallbacks.onChildTaskEnterPip(taskId);
mCallbacks.onChildTaskEnterPip();
}
sendStatusChanged();
} else {
@@ -297,6 +302,14 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
}
}
void fadeOutDecor(Runnable finishedCallback) {
if (mSplitDecorManager != null) {
mSplitDecorManager.fadeOutDecor(finishedCallback);
} else {
finishedCallback.run();
}
}
void addTask(ActivityManager.RunningTaskInfo task, WindowContainerTransaction wct) {
// Clear overridden bounds and windowing mode to make sure the child task can inherit
// windowing mode and bounds from split root.
@@ -330,6 +343,11 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
}
}
void resetBounds(WindowContainerTransaction wct) {
wct.setBounds(mRootTaskInfo.token, null);
wct.setAppBounds(mRootTaskInfo.token, null);
}
void onSplitScreenListenerRegistered(SplitScreen.SplitScreenListener listener,
@StageType int stage) {
for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) {

View File

@@ -133,7 +133,7 @@ public class SplitLayoutTests extends ShellTestCase {
mSplitLayout.snapToTarget(0 /* currentPosition */, snapTarget);
waitDividerFlingFinished();
verify(mSplitLayoutHandler).onSnappedToDismiss(eq(false));
verify(mSplitLayoutHandler).onSnappedToDismiss(eq(false), anyInt());
}
@Test
@@ -145,7 +145,7 @@ public class SplitLayoutTests extends ShellTestCase {
mSplitLayout.snapToTarget(0 /* currentPosition */, snapTarget);
waitDividerFlingFinished();
verify(mSplitLayoutHandler).onSnappedToDismiss(eq(true));
verify(mSplitLayoutHandler).onSnappedToDismiss(eq(true), anyInt());
}
@Test

View File

@@ -127,6 +127,9 @@ public class StageCoordinatorTests extends ShellTestCase {
mRootTask = new TestRunningTaskInfoBuilder().build();
mRootLeash = new SurfaceControl.Builder(mSurfaceSession).setName("test").build();
mStageCoordinator.onTaskAppeared(mRootTask, mRootLeash);
mSideStage.mRootTaskInfo = new TestRunningTaskInfoBuilder().build();
mMainStage.mRootTaskInfo = new TestRunningTaskInfoBuilder().build();
}
@Test
@@ -224,8 +227,8 @@ public class StageCoordinatorTests extends ShellTestCase {
mStageCoordinator.exitSplitScreen(testTaskId, EXIT_REASON_RETURN_HOME);
verify(mMainStage).reorderChild(eq(testTaskId), eq(true),
any(WindowContainerTransaction.class));
verify(mSideStage).removeAllTasks(any(WindowContainerTransaction.class), eq(false));
verify(mMainStage).deactivate(any(WindowContainerTransaction.class), eq(true));
verify(mSideStage).dismiss(any(WindowContainerTransaction.class), eq(false));
verify(mMainStage).resetBounds(any(WindowContainerTransaction.class));
}
@Test
@@ -237,8 +240,8 @@ public class StageCoordinatorTests extends ShellTestCase {
mStageCoordinator.exitSplitScreen(testTaskId, EXIT_REASON_RETURN_HOME);
verify(mSideStage).reorderChild(eq(testTaskId), eq(true),
any(WindowContainerTransaction.class));
verify(mSideStage).removeAllTasks(any(WindowContainerTransaction.class), eq(true));
verify(mMainStage).deactivate(any(WindowContainerTransaction.class), eq(false));
verify(mSideStage).resetBounds(any(WindowContainerTransaction.class));
verify(mMainStage).dismiss(any(WindowContainerTransaction.class), eq(false));
}
@Test