Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5a8b8cbf authored by Jeremy Sim's avatar Jeremy Sim Committed by Android (Google) Code Review
Browse files

Merge changes from topic "better-swap" into main

* changes:
  Add veil for swap animations when insets are in play
  Improve splitscreen swap animation
parents 447a7bde 0d6ce4c7
Loading
Loading
Loading
Loading
+130 −25
Original line number Diff line number Diff line
@@ -23,7 +23,10 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMA
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;

import static com.android.wm.shell.common.split.SplitLayout.BEHIND_APP_VEIL_LAYER;
import static com.android.wm.shell.common.split.SplitLayout.FRONT_APP_VEIL_LAYER;
import static com.android.wm.shell.common.split.SplitScreenConstants.FADE_DURATION;
import static com.android.wm.shell.common.split.SplitScreenConstants.VEIL_DELAY_DURATION;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -74,7 +77,7 @@ public class SplitDecorManager extends WindowlessWindowManager {
    private final SurfaceSession mSurfaceSession;

    private Drawable mIcon;
    private ImageView mResizingIconView;
    private ImageView mVeilIconView;
    private SurfaceControlViewHost mViewHost;
    private SurfaceControl mHostLeash;
    private SurfaceControl mIconLeash;
@@ -83,13 +86,14 @@ public class SplitDecorManager extends WindowlessWindowManager {
    private SurfaceControl mScreenshot;

    private boolean mShown;
    private boolean mIsResizing;
    /** True if the task is going through some kind of transition (moving or changing size). */
    private boolean mIsCurrentlyChanging;
    /** The original bounds of the main task, captured at the beginning of a resize transition. */
    private final Rect mOldMainBounds = new Rect();
    /** The original bounds of the side task, captured at the beginning of a resize transition. */
    private final Rect mOldSideBounds = new Rect();
    /** The current bounds of the main task, mid-resize. */
    private final Rect mResizingBounds = new Rect();
    private final Rect mInstantaneousBounds = new Rect();
    private final Rect mTempRect = new Rect();
    private ValueAnimator mFadeAnimator;
    private ValueAnimator mScreenshotAnimator;
@@ -134,7 +138,7 @@ public class SplitDecorManager extends WindowlessWindowManager {
        mIconSize = context.getResources().getDimensionPixelSize(R.dimen.split_icon_size);
        final FrameLayout rootLayout = (FrameLayout) LayoutInflater.from(context)
                .inflate(R.layout.split_decor, null);
        mResizingIconView = rootLayout.findViewById(R.id.split_resizing_icon);
        mVeilIconView = rootLayout.findViewById(R.id.split_resizing_icon);

        final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
                0 /* width */, 0 /* height */, TYPE_APPLICATION_OVERLAY,
@@ -191,28 +195,28 @@ public class SplitDecorManager extends WindowlessWindowManager {
        }
        mHostLeash = null;
        mIcon = null;
        mResizingIconView = null;
        mIsResizing = false;
        mVeilIconView = null;
        mIsCurrentlyChanging = false;
        mShown = false;
        mOldMainBounds.setEmpty();
        mOldSideBounds.setEmpty();
        mResizingBounds.setEmpty();
        mInstantaneousBounds.setEmpty();
    }

    /** Showing resizing hint. */
    public void onResizing(ActivityManager.RunningTaskInfo resizingTask, Rect newBounds,
            Rect sideBounds, SurfaceControl.Transaction t, int offsetX, int offsetY,
            boolean immediately) {
        if (mResizingIconView == null) {
        if (mVeilIconView == null) {
            return;
        }

        if (!mIsResizing) {
            mIsResizing = true;
        if (!mIsCurrentlyChanging) {
            mIsCurrentlyChanging = true;
            mOldMainBounds.set(newBounds);
            mOldSideBounds.set(sideBounds);
        }
        mResizingBounds.set(newBounds);
        mInstantaneousBounds.set(newBounds);
        mOffsetX = offsetX;
        mOffsetY = offsetY;

@@ -254,8 +258,8 @@ public class SplitDecorManager extends WindowlessWindowManager {

        if (mIcon == null && resizingTask.topActivityInfo != null) {
            mIcon = mIconProvider.getIcon(resizingTask.topActivityInfo);
            mResizingIconView.setImageDrawable(mIcon);
            mResizingIconView.setVisibility(View.VISIBLE);
            mVeilIconView.setImageDrawable(mIcon);
            mVeilIconView.setVisibility(View.VISIBLE);

            WindowManager.LayoutParams lp =
                    (WindowManager.LayoutParams) mViewHost.getView().getLayoutParams();
@@ -275,7 +279,12 @@ public class SplitDecorManager extends WindowlessWindowManager {
                t.setAlpha(mIconLeash, showVeil ? 1f : 0f);
                t.setVisibility(mIconLeash, showVeil);
            } else {
                startFadeAnimation(showVeil, false, null);
                startFadeAnimation(
                        showVeil,
                        false /* releaseSurface */,
                        null /* finishedCallback */,
                        false /* addDelay */
                );
            }
            mShown = showVeil;
        }
@@ -320,19 +329,19 @@ public class SplitDecorManager extends WindowlessWindowManager {
            mScreenshotAnimator.start();
        }

        if (mResizingIconView == null) {
        if (mVeilIconView == null) {
            if (mRunningAnimationCount == 0 && animFinishedCallback != null) {
                animFinishedCallback.accept(false);
            }
            return;
        }

        mIsResizing = false;
        mIsCurrentlyChanging = false;
        mOffsetX = 0;
        mOffsetY = 0;
        mOldMainBounds.setEmpty();
        mOldSideBounds.setEmpty();
        mResizingBounds.setEmpty();
        mInstantaneousBounds.setEmpty();
        if (mFadeAnimator != null && mFadeAnimator.isRunning()) {
            if (!mShown) {
                // If fade-out animation is running, just add release callback to it.
@@ -356,7 +365,7 @@ public class SplitDecorManager extends WindowlessWindowManager {
                if (mRunningAnimationCount == 0 && animFinishedCallback != null) {
                    animFinishedCallback.accept(true);
                }
            });
            }, false /* addDelay */);
        } else {
            // Decor surface is hidden so release it directly.
            releaseDecor(t);
@@ -366,9 +375,94 @@ public class SplitDecorManager extends WindowlessWindowManager {
        }
    }

    /**
     * Called (on every frame) when two split apps are swapping, and a veil is needed.
     */
    public void drawNextVeilFrameForSwapAnimation(ActivityManager.RunningTaskInfo resizingTask,
            Rect newBounds, SurfaceControl.Transaction t, boolean isGoingBehind,
            SurfaceControl leash, float iconOffsetX, float iconOffsetY) {
        if (mVeilIconView == null) {
            return;
        }

        if (!mIsCurrentlyChanging) {
            mIsCurrentlyChanging = true;
        }

        mInstantaneousBounds.set(newBounds);
        mOffsetX = (int) iconOffsetX;
        mOffsetY = (int) iconOffsetY;

        t.setLayer(leash, isGoingBehind ? BEHIND_APP_VEIL_LAYER : FRONT_APP_VEIL_LAYER);

        if (!mShown) {
            if (mFadeAnimator != null && mFadeAnimator.isRunning()) {
                // Cancel mFadeAnimator if it is running
                mFadeAnimator.cancel();
            }
        }

        if (mBackgroundLeash == null) {
            // Initialize background
            mBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash,
                    RESIZING_BACKGROUND_SURFACE_NAME, mSurfaceSession);
            t.setColor(mBackgroundLeash, getResizingBackgroundColor(resizingTask))
                    .setLayer(mBackgroundLeash, Integer.MAX_VALUE - 1);
        }

        if (mIcon == null && resizingTask.topActivityInfo != null) {
            // Initialize icon
            mIcon = mIconProvider.getIcon(resizingTask.topActivityInfo);
            mVeilIconView.setImageDrawable(mIcon);
            mVeilIconView.setVisibility(View.VISIBLE);

            WindowManager.LayoutParams lp =
                    (WindowManager.LayoutParams) mViewHost.getView().getLayoutParams();
            lp.width = mIconSize;
            lp.height = mIconSize;
            mViewHost.relayout(lp);

            t.setLayer(mIconLeash, Integer.MAX_VALUE);
        }

        t.setPosition(mIconLeash,
                newBounds.width() / 2 - mIconSize / 2 - mOffsetX,
                newBounds.height() / 2 - mIconSize / 2 - mOffsetY);

        // If this is the first frame, we need to trigger the veil's fade-in animation.
        if (!mShown) {
            startFadeAnimation(
                    true /* show */,
                    false /* releaseSurface */,
                    null /* finishedCallball */,
                    false /* addDelay */
            );
            mShown = true;
        }
    }

    /** Called at the end of the swap animation. */
    public void fadeOutVeilAndCleanUp(SurfaceControl.Transaction t) {
        if (mVeilIconView == null) {
            return;
        }

        // Recenter icon
        t.setPosition(mIconLeash,
                mInstantaneousBounds.width() / 2f - mIconSize / 2f,
                mInstantaneousBounds.height() / 2f - mIconSize / 2f);

        mIsCurrentlyChanging = false;
        mOffsetX = 0;
        mOffsetY = 0;
        mInstantaneousBounds.setEmpty();

        fadeOutDecor(() -> {}, true /* addDelay */);
    }

    /** Screenshot host leash and attach on it if meet some conditions */
    public void screenshotIfNeeded(SurfaceControl.Transaction t) {
        if (!mShown && mIsResizing && !mOldMainBounds.equals(mResizingBounds)) {
        if (!mShown && mIsCurrentlyChanging && !mOldMainBounds.equals(mInstantaneousBounds)) {
            if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) {
                mScreenshotAnimator.cancel();
            } else if (mScreenshot != null) {
@@ -386,7 +480,7 @@ public class SplitDecorManager extends WindowlessWindowManager {
    public void setScreenshotIfNeeded(SurfaceControl screenshot, SurfaceControl.Transaction t) {
        if (screenshot == null || !screenshot.isValid()) return;

        if (!mShown && mIsResizing && !mOldMainBounds.equals(mResizingBounds)) {
        if (!mShown && mIsCurrentlyChanging && !mOldMainBounds.equals(mInstantaneousBounds)) {
            if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) {
                mScreenshotAnimator.cancel();
            } else if (mScreenshot != null) {
@@ -401,24 +495,35 @@ public class SplitDecorManager extends WindowlessWindowManager {

    /** Fade-out decor surface with animation end callback, if decor is hidden, run the callback
     * directly. */
    public void fadeOutDecor(Runnable finishedCallback) {
    public void fadeOutDecor(Runnable finishedCallback, boolean addDelay) {
        if (mShown) {
            // If previous animation is running, just cancel it.
            if (mFadeAnimator != null && mFadeAnimator.isRunning()) {
                mFadeAnimator.cancel();
            }

            startFadeAnimation(false /* show */, true, finishedCallback);
            startFadeAnimation(
                    false /* show */, true /* releaseSurface */, finishedCallback, addDelay);
            mShown = false;
        } else {
            if (finishedCallback != null) finishedCallback.run();
        }
    }

    /**
     * Fades the veil in or out. Called at the first frame of a movement or resize when a veil is
     * needed (with show = true), and called again at the end (with show = false).
     * @param addDelay If true, adds a short delay before fading out to get the app behind the veil
     *                 time to redraw.
     */
    private void startFadeAnimation(boolean show, boolean releaseSurface,
            Runnable finishedCallback) {
            Runnable finishedCallback, boolean addDelay) {
        final SurfaceControl.Transaction animT = new SurfaceControl.Transaction();

        mFadeAnimator = ValueAnimator.ofFloat(0f, 1f);
        if (addDelay) {
            mFadeAnimator.setStartDelay(VEIL_DELAY_DURATION);
        }
        mFadeAnimator.setDuration(FADE_DURATION);
        mFadeAnimator.addUpdateListener(valueAnimator-> {
            final float progress = (float) valueAnimator.getAnimatedValue();
@@ -481,8 +586,8 @@ public class SplitDecorManager extends WindowlessWindowManager {
        }

        if (mIcon != null) {
            mResizingIconView.setVisibility(View.GONE);
            mResizingIconView.setImageDrawable(null);
            mVeilIconView.setVisibility(View.GONE);
            mVeilIconView.setImageDrawable(null);
            t.hide(mIconLeash);
            mIcon = null;
        }
+178 −35

File changed.

Preview size limit exceeded, changes collapsed.

+2 −0
Original line number Diff line number Diff line
@@ -29,6 +29,8 @@ import com.android.wm.shell.shared.TransitionUtil;
public class SplitScreenConstants {
    /** Duration used for every split fade-in or fade-out. */
    public static final int FADE_DURATION = 133;
    /** Duration where we keep an app veiled to allow it to redraw itself behind the scenes. */
    public static final int VEIL_DELAY_DURATION = 400;

    /** Key for passing in widget intents when invoking split from launcher workspace. */
    public static final String KEY_EXTRA_WIDGET_INTENT = "key_extra_widget_intent";
+28 −27
Original line number Diff line number Diff line
@@ -123,10 +123,10 @@ 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.LaunchAdjacentController;
import com.android.wm.shell.common.ScreenshotUtils;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.common.split.SplitDecorManager;
import com.android.wm.shell.common.split.SplitLayout;
import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition;
import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
@@ -1010,40 +1010,41 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
        mTempRect1.setEmpty();
        final StageTaskListener topLeftStage =
                mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage;
        final SurfaceControl topLeftScreenshot = ScreenshotUtils.takeScreenshot(t,
                topLeftStage.mRootLeash, mTempRect1, Integer.MAX_VALUE - 1);
        final StageTaskListener bottomRightStage =
                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,

        // Don't allow windows or divider to be focused during animation (mRootTaskInfo is the
        // parent of all 3 leaves). We don't want the user to be able to tap and focus a window
        // while it is moving across the screen, because granting focus also recalculates the
        // layering order, which is in delicate balance during this animation.
        WindowContainerTransaction noFocus = new WindowContainerTransaction();
        noFocus.setFocusable(mRootTaskInfo.token, false);
        mSyncQueue.queue(noFocus);

        mSplitLayout.playSwapAnimation(t, topLeftStage, bottomRightStage,
                insets -> {
                    // Runs at the end of the swap animation
                    SplitDecorManager decorManager1 = topLeftStage.getDecorManager();
                    SplitDecorManager decorManager2 = bottomRightStage.getDecorManager();

                    WindowContainerTransaction wct = new WindowContainerTransaction();

                    // Restore focus-ability to the windows and divider
                    wct.setFocusable(mRootTaskInfo.token, true);

                    setSideStagePosition(reverseSplitPosition(mSideStagePosition), wct);
                    mSyncQueue.queue(wct);
                    mSyncQueue.runInSync(st -> {
                        updateSurfaceBounds(mSplitLayout, st, false /* applyResizingOffset */);
                        st.setPosition(topLeftScreenshot, -insets.left, -insets.top);
                        st.setPosition(bottomRightScreenshot, insets.left, insets.top);

                        final ValueAnimator va = ValueAnimator.ofFloat(1, 0);
                        va.addUpdateListener(valueAnimator-> {
                            final float progress = (float) valueAnimator.getAnimatedValue();
                            t.setAlpha(topLeftScreenshot, progress);
                            t.setAlpha(bottomRightScreenshot, progress);
                            t.apply();
                        });
                        va.addListener(new AnimatorListenerAdapter() {
                            @Override
                            public void onAnimationEnd(
                                    @androidx.annotation.NonNull Animator animation) {
                                t.remove(topLeftScreenshot);
                                t.remove(bottomRightScreenshot);
                                t.apply();
                                mTransactionPool.release(t);
                            }
                        });
                        va.start();

                        // updateSurfaceBounds(), above, officially puts the two apps in their new
                        // stages. Starting on the next frame, all calculations are made using the
                        // new layouts/insets. So any follow-up animations on the same leashes below
                        // should contain some cleanup/repositioning to prevent jank.

                        // Play follow-up animations if needed
                        decorManager1.fadeOutVeilAndCleanUp(st);
                        decorManager2.fadeOutVeilAndCleanUp(st);
                    });
                });

+14 −2
Original line number Diff line number Diff line
@@ -69,7 +69,7 @@ import java.util.function.Predicate;
 *
 * @see StageCoordinator
 */
class StageTaskListener implements ShellTaskOrganizer.TaskListener {
public class StageTaskListener implements ShellTaskOrganizer.TaskListener {
    private static final String TAG = StageTaskListener.class.getSimpleName();

    /** Callback interface for listening to changes in a split-screen stage. */
@@ -162,6 +162,18 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
        return getChildTaskInfo(predicate) != null;
    }

    public SurfaceControl getRootLeash() {
        return mRootLeash;
    }

    public ActivityManager.RunningTaskInfo getRunningTaskInfo() {
        return mRootTaskInfo;
    }

    public SplitDecorManager getDecorManager() {
        return mSplitDecorManager;
    }

    @Nullable
    private ActivityManager.RunningTaskInfo getChildTaskInfo(
            Predicate<ActivityManager.RunningTaskInfo> predicate) {
@@ -335,7 +347,7 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {

    void fadeOutDecor(Runnable finishedCallback) {
        if (mSplitDecorManager != null) {
            mSplitDecorManager.fadeOutDecor(finishedCallback);
            mSplitDecorManager.fadeOutDecor(finishedCallback, false /* addDelay */);
        } else {
            finishedCallback.run();
        }