summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Jeremy Sim <jeremysim@google.com> 2024-05-10 19:24:09 -0700
committer Jeremy Sim <jeremysim@google.com> 2024-08-02 15:57:44 -0700
commit19ed634782d057e75bc8623bcadc2ceda7bfc6f1 (patch)
treefdeb376a3de41d874af82c36e459955717bbd37d
parent14427b11f7473cb37f719fbc2b849da2faee6515 (diff)
Improve splitscreen swap animation
Motion quality polish. The swap animation now has a new duration and interpolator, new rounded corners, and animates one of the split tasks "behind" the divider with a shrink-and-grow motion. Also ensured that another swap cannot be triggered while a swap is already in progress. Fixes: 291893598 Test: Visually confirmed at 10x slowdown Flag: EXEMPT bugfix Change-Id: I039c29c8a1a2c0400b5390a66bddb6d8341481f8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java171
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java2
2 files changed, 141 insertions, 32 deletions
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
index 51f9de8305f8..7413dfeb6f04 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
@@ -53,6 +53,8 @@ import android.view.RoundedCorner;
import android.view.SurfaceControl;
import android.view.WindowInsets;
import android.view.WindowManager;
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
@@ -68,6 +70,8 @@ import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayImeController;
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.DisplayLayout;
+
+import com.android.wm.shell.common.pip.PipUtils;
import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition;
import com.android.wm.shell.common.split.SplitScreenConstants.SnapPosition;
import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
@@ -87,10 +91,18 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
public static final int PARALLAX_ALIGN_CENTER = 2;
public static final int FLING_RESIZE_DURATION = 250;
- private static final int FLING_SWITCH_DURATION = 350;
private static final int FLING_ENTER_DURATION = 450;
private static final int FLING_EXIT_DURATION = 450;
+ // Animation specs for the swap animation
+ private static final int SWAP_ANIMATION_TOTAL_DURATION = 500;
+ private static final float SWAP_ANIMATION_SHRINK_DURATION = 83;
+ private static final float SWAP_ANIMATION_SHRINK_MARGIN_DP = 14;
+ private static final Interpolator SHRINK_INTERPOLATOR =
+ new PathInterpolator(0.2f, 0f, 0f, 1f);
+ private static final Interpolator GROW_INTERPOLATOR =
+ new PathInterpolator(0.45f, 0f, 0.5f, 1f);
+
private int mDividerWindowWidth;
private int mDividerInsets;
private int mDividerSize;
@@ -134,6 +146,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
private final InteractionJankMonitor mInteractionJankMonitor;
private boolean mIsLeftRightSplit;
private ValueAnimator mDividerFlingAnimator;
+ private AnimatorSet mSwapAnimator;
public SplitLayout(String windowName, Context context, Configuration configuration,
SplitLayoutHandler splitLayoutHandler,
@@ -579,6 +592,10 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
}
void onDoubleTappedDivider() {
+ if (isCurrentlySwapping()) {
+ return;
+ }
+
mSplitLayoutHandler.onDoubleTappedDivider();
}
@@ -685,7 +702,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
}
/** Switch both surface position with animation. */
- public void splitSwitching(SurfaceControl.Transaction t, SurfaceControl leash1,
+ public void playSwapAnimation(SurfaceControl.Transaction t, SurfaceControl leash1,
SurfaceControl leash2, Consumer<Rect> finishCallback) {
final Rect insets = getDisplayStableInsets(mContext);
insets.set(mIsLeftRightSplit ? insets.left : 0, mIsLeftRightSplit ? 0 : insets.top,
@@ -693,32 +710,38 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
final int dividerPos = mDividerSnapAlgorithm.calculateNonDismissingSnapTarget(
mIsLeftRightSplit ? mBounds2.width() : mBounds2.height()).position;
- final Rect distBounds1 = new Rect();
- final Rect distBounds2 = new Rect();
- final Rect distDividerBounds = new Rect();
- // Compute dist bounds.
- updateBounds(dividerPos, distBounds2, distBounds1, distDividerBounds,
+ final Rect endBounds1 = new Rect();
+ final Rect endBounds2 = new Rect();
+ final Rect endDividerBounds = new Rect();
+ // Compute destination bounds.
+ updateBounds(dividerPos, endBounds2, endBounds1, endDividerBounds,
false /* setEffectBounds */);
// Offset to real position under root container.
- distBounds1.offset(-mRootBounds.left, -mRootBounds.top);
- distBounds2.offset(-mRootBounds.left, -mRootBounds.top);
- distDividerBounds.offset(-mRootBounds.left, -mRootBounds.top);
-
- ValueAnimator animator1 = moveSurface(t, leash1, getRefBounds1(), distBounds1,
- -insets.left, -insets.top);
- ValueAnimator animator2 = moveSurface(t, leash2, getRefBounds2(), distBounds2,
- insets.left, insets.top);
+ endBounds1.offset(-mRootBounds.left, -mRootBounds.top);
+ endBounds2.offset(-mRootBounds.left, -mRootBounds.top);
+ endDividerBounds.offset(-mRootBounds.left, -mRootBounds.top);
+
+ ValueAnimator animator1 = moveSurface(t, leash1, getRefBounds1(), endBounds1,
+ -insets.left, -insets.top, true /* roundCorners */, true /* addShrinkAnimation */);
+ ValueAnimator animator2 = moveSurface(t, leash2, getRefBounds2(), endBounds2,
+ insets.left, insets.top, true /* roundCorners */, false /* addShrinkAnimation */);
ValueAnimator animator3 = moveSurface(t, getDividerLeash(), getRefDividerBounds(),
- distDividerBounds, 0 /* offsetX */, 0 /* offsetY */);
+ endDividerBounds, 0 /* offsetX */, 0 /* offsetY */, false /* roundCorners */,
+ false /* addShrinkAnimation */);
- AnimatorSet set = new AnimatorSet();
- set.playTogether(animator1, animator2, animator3);
- set.setDuration(FLING_SWITCH_DURATION);
- set.addListener(new AnimatorListenerAdapter() {
+ mSwapAnimator = new AnimatorSet();
+ mSwapAnimator.playTogether(animator1, animator2, animator3);
+ mSwapAnimator.setDuration(SWAP_ANIMATION_TOTAL_DURATION);
+ mSwapAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
mInteractionJankMonitor.begin(getDividerLeash(),
mContext, CUJ_SPLIT_SCREEN_DOUBLE_TAP_DIVIDER);
+
+ // The right/bottom app moves above the divider, the left/top app moves below.
+ t.setLayer(leash1, Integer.MIN_VALUE);
+ t.setLayer(getDividerLeash(), 0);
+ t.setLayer(leash2, Integer.MAX_VALUE);
}
@Override
@@ -734,33 +757,119 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
mInteractionJankMonitor.cancel(CUJ_SPLIT_SCREEN_DOUBLE_TAP_DIVIDER);
}
});
- set.start();
+ mSwapAnimator.start();
+ }
+
+ /** Returns true if a swap animation is currently playing. */
+ public boolean isCurrentlySwapping() {
+ return mSwapAnimator != null && mSwapAnimator.isRunning();
}
+ /**
+ * Animates a task leash across the screen. Currently used only for the swap animation.
+ *
+ * @param leash The task being animated.
+ * @param roundCorners Whether we should round the corners of the task while animating.
+ * @param addShrinkAnimation Whether we should a shrink-and-grow effect to the task while it is
+ * moving. (Simulates moving behind the divider.)
+ */
private ValueAnimator moveSurface(SurfaceControl.Transaction t, SurfaceControl leash,
- Rect start, Rect end, float offsetX, float offsetY) {
+ Rect start, Rect end, float offsetX, float offsetY, boolean roundCorners,
+ boolean addShrinkAnimation) {
Rect tempStart = new Rect(start);
Rect tempEnd = new Rect(end);
final float diffX = tempEnd.left - tempStart.left;
final float diffY = tempEnd.top - tempStart.top;
final float diffWidth = tempEnd.width() - tempStart.width();
final float diffHeight = tempEnd.height() - tempStart.height();
+
+ // Get display measurements (for possible shrink animation).
+ final RoundedCorner roundedCorner = mSplitWindowManager.getDividerView().getDisplay()
+ .getRoundedCorner(0 /* position */);
+ float cornerRadius = roundedCorner == null ? 0 : roundedCorner.getRadius();
+ float shrinkMarginPx = PipUtils.dpToPx(
+ SWAP_ANIMATION_SHRINK_MARGIN_DP, mContext.getResources().getDisplayMetrics());
+ float shrinkAmountPx = shrinkMarginPx * 2;
+
+ // Timing calculations
+ float shrinkPortion = SWAP_ANIMATION_SHRINK_DURATION / SWAP_ANIMATION_TOTAL_DURATION;
+ float growPortion = 1 - shrinkPortion;
+
ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
+ animator.setInterpolator(Interpolators.EMPHASIZED);
animator.addUpdateListener(animation -> {
if (leash == null) return;
+ if (roundCorners) {
+ // Add rounded corners to the task leash while it is animating.
+ t.setCornerRadius(leash, cornerRadius);
+ }
+
+ final float progress = (float) animation.getAnimatedValue();
+ float instantaneousX = tempStart.left + progress * diffX;
+ float instantaneousY = tempStart.top + progress * diffY;
+ int width = (int) (tempStart.width() + progress * diffWidth);
+ int height = (int) (tempStart.height() + progress * diffHeight);
+
+ if (addShrinkAnimation) {
+ float shrinkDiffX; // the position adjustments needed for this frame
+ float shrinkDiffY;
+ float shrinkScaleX; // the scale adjustments needed for this frame
+ float shrinkScaleY;
+
+ // Find the max amount we will be shrinking this leash, as a proportion (e.g. 0.1f).
+ float maxShrinkX = shrinkAmountPx / height;
+ float maxShrinkY = shrinkAmountPx / width;
+
+ // Find if we are in the shrinking part of the animation, or the growing part.
+ boolean shrinking = progress <= shrinkPortion;
+
+ if (shrinking) {
+ // Find how far into the shrink portion we are (e.g. 0.5f).
+ float shrinkProgress = progress / shrinkPortion;
+ // Find how much we should have progressed in shrinking the leash (e.g. 0.8f).
+ float interpolatedShrinkProgress =
+ SHRINK_INTERPOLATOR.getInterpolation(shrinkProgress);
+ // Find how much width proportion we should be taking off (e.g. 0.1f)
+ float widthProportionLost = maxShrinkX * interpolatedShrinkProgress;
+ shrinkScaleX = 1 - widthProportionLost;
+ // Find how much height proportion we should be taking off (e.g. 0.1f)
+ float heightProportionLost = maxShrinkY * interpolatedShrinkProgress;
+ shrinkScaleY = 1 - heightProportionLost;
+ // Add a small amount to the leash's position to keep the task centered.
+ shrinkDiffX = (width * widthProportionLost) / 2;
+ shrinkDiffY = (height * heightProportionLost) / 2;
+ } else {
+ // Find how far into the grow portion we are (e.g. 0.5f).
+ float growProgress = (progress - shrinkPortion) / growPortion;
+ // Find how much we should have progressed in growing the leash (e.g. 0.8f).
+ float interpolatedGrowProgress =
+ GROW_INTERPOLATOR.getInterpolation(growProgress);
+ // Find how much width proportion we should be taking off (e.g. 0.1f)
+ float widthProportionLost = maxShrinkX * (1 - interpolatedGrowProgress);
+ shrinkScaleX = 1 - widthProportionLost;
+ // Find how much height proportion we should be taking off (e.g. 0.1f)
+ float heightProportionLost = maxShrinkY * (1 - interpolatedGrowProgress);
+ shrinkScaleY = 1 - heightProportionLost;
+ // Add a small amount to the leash's position to keep the task centered.
+ shrinkDiffX = (width * widthProportionLost) / 2;
+ shrinkDiffY = (height * heightProportionLost) / 2;
+ }
+
+ instantaneousX += shrinkDiffX;
+ instantaneousY += shrinkDiffY;
+ width *= shrinkScaleX;
+ height *= shrinkScaleY;
+ // Set scale on the leash's contents.
+ t.setScale(leash, shrinkScaleX, shrinkScaleY);
+ }
- final float scale = (float) animation.getAnimatedValue();
- final float distX = tempStart.left + scale * diffX;
- final float distY = tempStart.top + scale * diffY;
- final int width = (int) (tempStart.width() + scale * diffWidth);
- final int height = (int) (tempStart.height() + scale * diffHeight);
if (offsetX == 0 && offsetY == 0) {
- t.setPosition(leash, distX, distY);
+ t.setPosition(leash, instantaneousX, instantaneousY);
t.setWindowCrop(leash, width, height);
} else {
- final int diffOffsetX = (int) (scale * offsetX);
- final int diffOffsetY = (int) (scale * offsetY);
- t.setPosition(leash, distX + diffOffsetX, distY + diffOffsetY);
+ final int diffOffsetX = (int) (progress * offsetX);
+ final int diffOffsetY = (int) (progress * offsetY);
+ t.setPosition(leash, instantaneousX + diffOffsetX, instantaneousY + diffOffsetY);
mTempRect.set(0, 0, width, height);
mTempRect.offsetTo(-diffOffsetX, -diffOffsetY);
t.setCrop(leash, mTempRect);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 2531ff150f43..adf16d26477a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -1456,7 +1456,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage;
final SurfaceControl bottomRightScreenshot = ScreenshotUtils.takeScreenshot(t,
bottomRightStage.mRootLeash, mTempRect1, Integer.MAX_VALUE - 1);
- mSplitLayout.splitSwitching(t, topLeftStage.mRootLeash, bottomRightStage.mRootLeash,
+ mSplitLayout.playSwapAnimation(t, topLeftStage.mRootLeash, bottomRightStage.mRootLeash,
insets -> {
WindowContainerTransaction wct = new WindowContainerTransaction();
setSideStagePosition(reverseSplitPosition(mSideStagePosition), wct);