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

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

Merge "Flexible 2-app split: Parallax (landscape)" into main

parents a59603c7 a250cad8
Loading
Loading
Loading
Loading
+324 −0
Original line number Diff line number Diff line
/*
 * 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.wm.shell.common.split;

import static android.view.WindowManager.DOCKED_BOTTOM;
import static android.view.WindowManager.DOCKED_INVALID;
import static android.view.WindowManager.DOCKED_LEFT;
import static android.view.WindowManager.DOCKED_RIGHT;
import static android.view.WindowManager.DOCKED_TOP;

import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_ALIGN_CENTER;
import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_DISMISSING;
import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_FLEX;
import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_NONE;
import static com.android.wm.shell.shared.animation.Interpolators.DIM_INTERPOLATOR;
import static com.android.wm.shell.shared.animation.Interpolators.SLOWDOWN_INTERPOLATOR;

import android.graphics.Point;
import android.graphics.Rect;
import android.view.SurfaceControl;
import android.view.WindowManager;

/**
 * This class governs how and when parallax and dimming effects are applied to task surfaces,
 * usually when the divider is being moved around by the user (or during an animation).
 */
class ResizingEffectPolicy {
    private final SplitLayout mSplitLayout;
    /** The parallax algorithm we are currently using. */
    private final int mParallaxType;

    int mShrinkSide = DOCKED_INVALID;

    // The current dismissing side.
    int mDismissingSide = DOCKED_INVALID;

    /**
     * A {@link Point} that stores a single x and y value, representing the parallax translation
     * we use on the app that the divider is moving toward. The app is either shrinking in size or
     * getting pushed off the screen.
     */
    final Point mRetreatingSideParallax = new Point();
    /**
     * A {@link Point} that stores a single x and y value, representing the parallax translation
     * we use on the app that the divider is moving away from. The app is either growing in size or
     * getting pulled onto the screen.
     */
    final Point mAdvancingSideParallax = new Point();

    // The dimming value to hint the dismissing side and progress.
    float mDismissingDimValue = 0.0f;

    /**
     * Content bounds for the app that the divider is moving toward. This is the content that is
     * currently drawn at the start of the divider movement. It stays unchanged throughout the
     * divider's movement.
     */
    final Rect mRetreatingContent = new Rect();
    /**
     * Surface bounds for the app that the divider is moving toward. This is the "canvas" on
     * which an app could potentially be drawn. It changes on every frame as the divider moves
     * around.
     */
    final Rect mRetreatingSurface = new Rect();
    /**
     * Content bounds for the app that the divider is moving away from. This is the content that
     * is currently drawn at the start of the divider movement. It stays unchanged throughout
     * the divider's movement.
     */
    final Rect mAdvancingContent = new Rect();
    /**
     * Surface bounds for the app that the divider is moving away from. This is the "canvas" on
     * which an app could potentially be drawn. It changes on every frame as the divider moves
     * around.
     */
    final Rect mAdvancingSurface = new Rect();

    final Rect mTempRect = new Rect();
    final Rect mTempRect2 = new Rect();

    ResizingEffectPolicy(int parallaxType, SplitLayout splitLayout) {
        mParallaxType = parallaxType;
        mSplitLayout = splitLayout;
    }

    /**
     * Calculates the desired parallax values and stores them in {@link #mRetreatingSideParallax}
     * and {@link #mAdvancingSideParallax}. These values will be then be applied in
     * {@link #adjustRootSurface}.
     *
     * @param position    The divider's position on the screen (x-coordinate in left-right split,
     *                    y-coordinate in top-bottom split).
     */
    void applyDividerPosition(
            int position, boolean isLeftRightSplit, DividerSnapAlgorithm snapAlgorithm) {
        mDismissingSide = DOCKED_INVALID;
        mRetreatingSideParallax.set(0, 0);
        mAdvancingSideParallax.set(0, 0);
        mDismissingDimValue = 0;
        Rect displayBounds = mSplitLayout.getRootBounds();

        int totalDismissingDistance = 0;
        if (position < snapAlgorithm.getFirstSplitTarget().position) {
            mDismissingSide = isLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP;
            totalDismissingDistance = snapAlgorithm.getDismissStartTarget().position
                    - snapAlgorithm.getFirstSplitTarget().position;
        } else if (position > snapAlgorithm.getLastSplitTarget().position) {
            mDismissingSide = isLeftRightSplit ? DOCKED_RIGHT : DOCKED_BOTTOM;
            totalDismissingDistance = snapAlgorithm.getLastSplitTarget().position
                    - snapAlgorithm.getDismissEndTarget().position;
        }

        final boolean topLeftShrink = isLeftRightSplit
                ? position < mSplitLayout.getTopLeftContentBounds().right
                : position < mSplitLayout.getTopLeftContentBounds().bottom;
        if (topLeftShrink) {
            mShrinkSide = isLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP;
            mRetreatingContent.set(mSplitLayout.getTopLeftContentBounds());
            mRetreatingSurface.set(mSplitLayout.getTopLeftBounds());
            mAdvancingContent.set(mSplitLayout.getBottomRightContentBounds());
            mAdvancingSurface.set(mSplitLayout.getBottomRightBounds());
        } else {
            mShrinkSide = isLeftRightSplit ? DOCKED_RIGHT : DOCKED_BOTTOM;
            mRetreatingContent.set(mSplitLayout.getBottomRightContentBounds());
            mRetreatingSurface.set(mSplitLayout.getBottomRightBounds());
            mAdvancingContent.set(mSplitLayout.getTopLeftContentBounds());
            mAdvancingSurface.set(mSplitLayout.getTopLeftBounds());
        }

        if (mDismissingSide != DOCKED_INVALID) {
            float fraction =
                    Math.max(0, Math.min(snapAlgorithm.calculateDismissingFraction(position), 1f));
            mDismissingDimValue = DIM_INTERPOLATOR.getInterpolation(fraction);
            if (mParallaxType == PARALLAX_DISMISSING) {
                fraction = calculateParallaxDismissingFraction(fraction, mDismissingSide);
                if (isLeftRightSplit) {
                    mRetreatingSideParallax.x = (int) (fraction * totalDismissingDistance);
                } else {
                    mRetreatingSideParallax.y = (int) (fraction * totalDismissingDistance);
                }
            }
        }

        if (mParallaxType == PARALLAX_ALIGN_CENTER) {
            if (isLeftRightSplit) {
                mRetreatingSideParallax.x =
                        (mRetreatingSurface.width() - mRetreatingContent.width()) / 2;
            } else {
                mRetreatingSideParallax.y =
                        (mRetreatingSurface.height() - mRetreatingContent.height()) / 2;
            }
        } else if (mParallaxType == PARALLAX_FLEX) {
            // Whether an app is getting pushed offscreen by the divider.
            boolean isRetreatingOffscreen = !displayBounds.contains(mRetreatingSurface);
            // Whether an app was getting pulled onscreen at the beginning of the drag.
            boolean advancingSideStartedOffscreen = !displayBounds.contains(mAdvancingContent);

            // The simpler case when an app gets pushed offscreen (e.g. 50:50 -> 90:10)
            if (isRetreatingOffscreen && !advancingSideStartedOffscreen) {
                // On the left side, we use parallax to simulate the contents sticking to the
                // divider. This is because surfaces naturally expand to the bottom and right,
                // so when a surface's area expands, the contents stick to the left. This is
                // correct behavior on the right-side surface, but not the left.
                if (topLeftShrink) {
                    if (isLeftRightSplit) {
                        mRetreatingSideParallax.x =
                                mRetreatingSurface.width() - mRetreatingContent.width();
                    } else {
                        mRetreatingSideParallax.y =
                                mRetreatingSurface.height() - mRetreatingContent.height();
                    }
                }
                // All other cases (e.g. 10:90 -> 50:50, 10:90 -> 90:10, 10:90 -> dismiss)
            } else {
                mTempRect.set(mRetreatingSurface);
                Point rootOffset = new Point();
                // 10:90 -> 50:50, 10:90, or dismiss right
                if (advancingSideStartedOffscreen) {
                    // We have to handle a complicated case here to keep the parallax smooth.
                    // When the divider crosses the 50% mark, the retreating-side app surface
                    // will start expanding offscreen. This is expected and unavoidable, but
                    // makes the parallax look disjointed. In order to preserve the illusion,
                    // we add another offset (rootOffset) to simulate the surface staying
                    // onscreen.
                    mTempRect.intersect(displayBounds);
                    if (mRetreatingSurface.left < displayBounds.left) {
                        rootOffset.x = displayBounds.left - mRetreatingSurface.left;
                    }
                    if (mRetreatingSurface.top < displayBounds.top) {
                        rootOffset.y = displayBounds.top - mRetreatingSurface.top;
                    }

                    // On the left side, we again have to simulate the contents sticking to the
                    // divider.
                    if (!topLeftShrink) {
                        if (isLeftRightSplit) {
                            mAdvancingSideParallax.x =
                                    mAdvancingSurface.width() - mAdvancingContent.width();
                        } else {
                            mAdvancingSideParallax.y =
                                    mAdvancingSurface.height() - mAdvancingContent.height();
                        }
                    }
                }

                // In all these cases, the shrinking app also receives a center parallax.
                if (isLeftRightSplit) {
                    mRetreatingSideParallax.x = rootOffset.x
                            + ((mTempRect.width() - mRetreatingContent.width()) / 2);
                } else {
                    mRetreatingSideParallax.y = rootOffset.y
                            + ((mTempRect.height() - mRetreatingContent.height()) / 2);
                }
            }
        }
    }

    /**
     * @return for a specified {@code fraction}, this returns an adjusted value that simulates a
     * slowing down parallax effect
     */
    private float calculateParallaxDismissingFraction(float fraction, int dockSide) {
        float result = SLOWDOWN_INTERPOLATOR.getInterpolation(fraction) / 3.5f;

        // Less parallax at the top, just because.
        if (dockSide == WindowManager.DOCKED_TOP) {
            result /= 2f;
        }
        return result;
    }

    /** Applies the calculated parallax and dimming values to task surfaces. */
    void adjustRootSurface(SurfaceControl.Transaction t,
            SurfaceControl leash1, SurfaceControl leash2) {
        SurfaceControl retreatingLeash = null;
        SurfaceControl advancingLeash = null;

        if (mParallaxType == PARALLAX_DISMISSING) {
            switch (mDismissingSide) {
                case DOCKED_TOP:
                case DOCKED_LEFT:
                    retreatingLeash = leash1;
                    mTempRect.set(mSplitLayout.getTopLeftBounds());
                    advancingLeash = leash2;
                    mTempRect2.set(mSplitLayout.getBottomRightBounds());
                    break;
                case DOCKED_BOTTOM:
                case DOCKED_RIGHT:
                    retreatingLeash = leash2;
                    mTempRect.set(mSplitLayout.getBottomRightBounds());
                    advancingLeash = leash1;
                    mTempRect2.set(mSplitLayout.getTopLeftBounds());
                    break;
            }
        } else if (mParallaxType == PARALLAX_ALIGN_CENTER || mParallaxType == PARALLAX_FLEX) {
            switch (mShrinkSide) {
                case DOCKED_TOP:
                case DOCKED_LEFT:
                    retreatingLeash = leash1;
                    mTempRect.set(mSplitLayout.getTopLeftBounds());
                    advancingLeash = leash2;
                    mTempRect2.set(mSplitLayout.getBottomRightBounds());
                    break;
                case DOCKED_BOTTOM:
                case DOCKED_RIGHT:
                    retreatingLeash = leash2;
                    mTempRect.set(mSplitLayout.getBottomRightBounds());
                    advancingLeash = leash1;
                    mTempRect2.set(mSplitLayout.getTopLeftBounds());
                    break;
            }
        }
        if (mParallaxType != PARALLAX_NONE
                && retreatingLeash != null && advancingLeash != null) {
            t.setPosition(retreatingLeash, mTempRect.left + mRetreatingSideParallax.x,
                    mTempRect.top + mRetreatingSideParallax.y);
            // Transform the screen-based split bounds to surface-based crop bounds.
            mTempRect.offsetTo(-mRetreatingSideParallax.x, -mRetreatingSideParallax.y);
            t.setWindowCrop(retreatingLeash, mTempRect);

            t.setPosition(advancingLeash, mTempRect2.left + mAdvancingSideParallax.x,
                    mTempRect2.top + mAdvancingSideParallax.y);
            // Transform the screen-based split bounds to surface-based crop bounds.
            mTempRect2.offsetTo(-mAdvancingSideParallax.x, -mAdvancingSideParallax.y);
            t.setWindowCrop(advancingLeash, mTempRect2);
        }
    }

    void adjustDimSurface(SurfaceControl.Transaction t,
            SurfaceControl dimLayer1, SurfaceControl dimLayer2) {
        SurfaceControl targetDimLayer;
        switch (mDismissingSide) {
            case DOCKED_TOP:
            case DOCKED_LEFT:
                targetDimLayer = dimLayer1;
                break;
            case DOCKED_BOTTOM:
            case DOCKED_RIGHT:
                targetDimLayer = dimLayer2;
                break;
            case DOCKED_INVALID:
            default:
                t.setAlpha(dimLayer1, 0).hide(dimLayer1);
                t.setAlpha(dimLayer2, 0).hide(dimLayer2);
                return;
        }
        t.setAlpha(targetDimLayer, mDismissingDimValue)
                .setVisibility(targetDimLayer, mDismissingDimValue > 0.001f);
    }
}
+34 −4
Original line number Diff line number Diff line
@@ -227,10 +227,37 @@ public class SplitDecorManager extends WindowlessWindowManager {
        mInstantaneousBounds.setEmpty();
    }

    /** Showing resizing hint. */
    /**
     * Called on every frame when an app is getting resized, and controls the showing & hiding of
     * the app veil. IMPORTANT: There is one SplitDecorManager for each task, so if two tasks are
     * getting resized simultaneously, this method is called in parallel on the other
     * SplitDecorManager too. In general, we want to hide the app behind a veil when:
     *   a) the app is stretching past its original bounds (because app content layout doesn't
     *      update mid-stretch).
     *   b) the app is resizing down from fullscreen (because there is no parallax effect that
     *      makes every app look good in this scenario).
     * In the world of flexible split, where apps can go offscreen, there is an exception to this:
     *   - We do NOT hide the app when it is going offscreen, even though it is technically
     *     getting larger and would qualify for condition (a). Instead, we use parallax to give
     *     the illusion that the app is getting pushed offscreen by the divider.
     *
     * @param resizingTask The task that is getting resized.
     * @param newBounds The bounds that that we are updating this surface to. This can be an
     *                  instantaneous bounds, just for a frame, during a drag or animation.
     * @param sideBounds The bounds of the OPPOSITE task in the split layout. This is used just for
     *                   reference/calculation, the surface of the other app won't be set here.
     * @param displayBounds The bounds of the entire display.
     * @param t The transaction on which these changes will be bundled.
     * @param offsetX The x-translation applied to the task surface for parallax. Will be used to
     *                position the task screenshot and/or icon veil.
     * @param offsetY The x-translation applied to the task surface for parallax. Will be used to
     *                position the task screenshot and/or icon veil.
     * @param immediately {@code true} if the veil should transition in/out instantly, with no
     *                                animation.
     */
    public void onResizing(ActivityManager.RunningTaskInfo resizingTask, Rect newBounds,
            Rect sideBounds, SurfaceControl.Transaction t, int offsetX, int offsetY,
            boolean immediately) {
            Rect sideBounds, Rect displayBounds, SurfaceControl.Transaction t, int offsetX,
            int offsetY, boolean immediately) {
        if (mVeilIconView == null) {
            return;
        }
@@ -252,7 +279,10 @@ public class SplitDecorManager extends WindowlessWindowManager {
        final boolean isStretchingPastOriginalBounds =
                newBounds.width() > mOldMainBounds.width()
                        || newBounds.height() > mOldMainBounds.height();
        final boolean showVeil = isResizingDownFromFullscreen || isStretchingPastOriginalBounds;
        final boolean isFullyOnscreen = displayBounds.contains(newBounds);
        boolean showVeil = isFullyOnscreen
                && (isResizingDownFromFullscreen || isStretchingPastOriginalBounds);

        final boolean update = showVeil != mShown;
        if (update && mFadeAnimator != null && mFadeAnimator.isRunning()) {
            // If we need to animate and animator still running, cancel it before we ensure both
+15 −173

File changed.

Preview size limit exceeded, changes collapsed.

+1 −1
Original line number Diff line number Diff line
@@ -43,7 +43,7 @@ import java.util.Map;
 */
public class SplitSpec {
    private static final String TAG = "SplitSpec";
    private static final boolean DEBUG = true;
    private static final boolean DEBUG = false;

    /** A split ratio used on larger screens, where we can fit both apps onscreen. */
    public static final float ONSCREEN_ONLY_ASYMMETRIC_RATIO = 0.33f;
+15 −9
Original line number Diff line number Diff line
@@ -35,7 +35,9 @@ import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER;

import static com.android.wm.shell.Flags.enableFlexibleSplit;
import static com.android.wm.shell.Flags.enableFlexibleTwoAppSplit;
import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_ALIGN_CENTER;
import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_FLEX;
import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPosition;
import static com.android.wm.shell.common.split.SplitScreenUtils.splitFailureMessage;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN;
@@ -127,7 +129,6 @@ import com.android.internal.logging.InstanceId;
import com.android.internal.policy.FoldLockSettingsObserver;
import com.android.internal.protolog.ProtoLog;
import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.Flags;
import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayController;
@@ -2007,7 +2008,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
            // If all stages are filled, create new SplitBounds and update Recents.
            if (mainStageTopTaskId != INVALID_TASK_ID && sideStageTopTaskId != INVALID_TASK_ID) {
                int currentSnapPosition = mSplitLayout.calculateCurrentSnapPosition();
                if (Flags.enableFlexibleTwoAppSplit()) {
                if (enableFlexibleTwoAppSplit()) {
                    // Split screen can be laid out in such a way that some of the apps are
                    // offscreen. For the purposes of passing SplitBounds up to launcher (for use in
                    // thumbnails etc.), we crop the bounds down to the screen size.
@@ -2064,10 +2065,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
        mRootTaskLeash = leash;

        if (mSplitLayout == null) {
            int parallaxType = enableFlexibleTwoAppSplit() ? PARALLAX_FLEX : PARALLAX_ALIGN_CENTER;
            mSplitLayout = new SplitLayout(TAG + "SplitDivider", mContext,
                    mRootTaskInfo.configuration, this, mParentContainerCallbacks,
                    mDisplayController, mDisplayImeController, mTaskOrganizer,
                    PARALLAX_ALIGN_CENTER /* parallaxType */, mSplitState, mMainHandler);
                    mDisplayController, mDisplayImeController, mTaskOrganizer, parallaxType,
                    mSplitState, mMainHandler);
            mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSplitLayout);
        }

@@ -2407,6 +2409,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
        updateSurfaceBounds(layout, t, shouldUseParallaxEffect);
        getMainStageBounds(mTempRect1);
        getSideStageBounds(mTempRect2);
        Rect displayBounds = mSplitLayout.getRootBounds();

        if (enableFlexibleSplit()) {
            StageTaskListener ltStage =
                    mStageOrderOperator.getStageForLegacyPosition(SPLIT_POSITION_TOP_OR_LEFT,
@@ -2414,12 +2418,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
            StageTaskListener brStage =
                    mStageOrderOperator.getStageForLegacyPosition(SPLIT_POSITION_BOTTOM_OR_RIGHT,
                            false /*checkAllStagesIfNotActive*/);
            ltStage.onResizing(mTempRect1, mTempRect2, t, offsetX, offsetY, mShowDecorImmediately);
            brStage.onResizing(mTempRect2, mTempRect1, t, offsetX, offsetY, mShowDecorImmediately);
            ltStage.onResizing(mTempRect1, mTempRect2, displayBounds, t, offsetX, offsetY,
                    mShowDecorImmediately);
            brStage.onResizing(mTempRect2, mTempRect1, displayBounds, t, offsetX, offsetY,
                    mShowDecorImmediately);
        } else {
            mMainStage.onResizing(mTempRect1, mTempRect2, t, offsetX, offsetY,
            mMainStage.onResizing(mTempRect1, mTempRect2, displayBounds, t, offsetX, offsetY,
                    mShowDecorImmediately);
            mSideStage.onResizing(mTempRect2, mTempRect1, t, offsetX, offsetY,
            mSideStage.onResizing(mTempRect2, mTempRect1, displayBounds, t, offsetX, offsetY,
                    mShowDecorImmediately);
        }
        t.apply();
@@ -2466,7 +2472,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
            mSplitLayout.populateTouchZones();
        }, mainDecor, sideDecor, decorManagers);

        if (Flags.enableFlexibleTwoAppSplit()) {
        if (enableFlexibleTwoAppSplit()) {
            switch (layout.calculateCurrentSnapPosition()) {
                case SNAP_TO_2_10_90 -> grantFocusToPosition(false /* leftOrTop */);
                case SNAP_TO_2_90_10 -> grantFocusToPosition(true /* leftOrTop */);
Loading