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

Commit da1bb958 authored by Jeremy Sim's avatar Jeremy Sim
Browse files

Flexible Split: Reference class

Adds SplitSpec, a class that stores the bounds of all valid split layouts (recalculated whenever config changes).

These bounds can be accessed through SplitState, and are intended for general use in flexible split (as a single source of truth), though they are currently only calculated and not used.

Bug: 349828130
Flag: com.android.wm.shell.enable_flexible_two_app_split
Test: Only generates class with flag on. No effect on behavior, either with flag on or off.
Change-Id: I1bd3dd6221b179c5d42cc7bb0e674070c4136e4c
parent 117c9b02
Loading
Loading
Loading
Loading
+19 −1
Original line number Diff line number Diff line
@@ -159,7 +159,8 @@ public class SplitScreenConstants {
     * {@link PersistentSnapPosition} + {@link #NOT_IN_SPLIT}.
     */
    @IntDef(value = {
            NOT_IN_SPLIT,
            NOT_IN_SPLIT, // user is not in split screen
            SNAP_TO_NONE, // in "free snap mode," where apps are fully resizable
            SNAP_TO_2_33_66,
            SNAP_TO_2_50_50,
            SNAP_TO_2_66_33,
@@ -171,6 +172,23 @@ public class SplitScreenConstants {
    })
    public @interface SplitScreenState {}

    /** Converts a {@link SplitScreenState} to a human-readable string. */
    public static String stateToString(@SplitScreenState int state) {
        return switch (state) {
            case NOT_IN_SPLIT -> "NOT_IN_SPLIT";
            case SNAP_TO_NONE -> "SNAP_TO_NONE";
            case SNAP_TO_2_33_66 -> "SNAP_TO_2_33_66";
            case SNAP_TO_2_50_50 -> "SNAP_TO_2_50_50";
            case SNAP_TO_2_66_33 -> "SNAP_TO_2_66_33";
            case SNAP_TO_2_90_10 -> "SNAP_TO_2_90_10";
            case SNAP_TO_2_10_90 -> "SNAP_TO_2_10_90";
            case SNAP_TO_3_33_33_33 -> "SNAP_TO_3_33_33_33";
            case SNAP_TO_3_45_45_10 -> "SNAP_TO_3_45_45_10";
            case SNAP_TO_3_10_45_45 -> "SNAP_TO_3_10_45_45";
            default -> "UNKNOWN";
        };
    }

    /**
     * Checks if the snapPosition in question is a {@link PersistentSnapPosition}.
     */
+2 −2
Original line number Diff line number Diff line
@@ -352,8 +352,8 @@ public class DividerSnapAlgorithm {
                ? mPinnedTaskbarInsets.right : mPinnedTaskbarInsets.bottom;

        float ratio = areOffscreenRatiosSupported()
                ? SplitLayout.OFFSCREEN_ASYMMETRIC_RATIO
                : SplitLayout.ONSCREEN_ONLY_ASYMMETRIC_RATIO;
                ? SplitSpec.OFFSCREEN_ASYMMETRIC_RATIO
                : SplitSpec.ONSCREEN_ONLY_ASYMMETRIC_RATIO;
        int size = (int) (ratio * (end - start)) - mDividerSize / 2;

        int leftTopPosition = start + pinnedTaskbarShiftStart + size;
+24 −15
Original line number Diff line number Diff line
@@ -112,11 +112,6 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
    private static final int FLING_EXIT_DURATION = 450;
    private static final int FLING_OFFSCREEN_DURATION = 500;

    /** A split ratio used on larger screens, where we can fit both apps onscreen. */
    public static final float ONSCREEN_ONLY_ASYMMETRIC_RATIO = 0.33f;
    /** A split ratio used on smaller screens, where we place one app mostly offscreen. */
    public static final float OFFSCREEN_ASYMMETRIC_RATIO = 0.1f;

    // Here are some (arbitrarily decided) layer definitions used during animations to make sure the
    // layers stay in order. (During transitions, everything is reparented onto a transition root
    // and can be freely relayered.)
@@ -236,7 +231,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
        updateDividerConfig(mContext);

        mRootBounds.set(configuration.windowConfiguration.getBounds());
        mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds);
        updateLayouts();
        mInteractionJankMonitor = InteractionJankMonitor.getInstance();
        resetDividerPosition();
        updateInvisibleRect();
@@ -490,7 +485,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
        mIsLargeScreen = configuration.smallestScreenWidthDp >= 600;
        mIsLeftRightSplit = SplitScreenUtils.isLeftRightSplit(mAllowLeftRightSplitInPortrait,
                configuration);
        mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds);
        updateLayouts();
        updateDividerConfig(mContext);
        initDividerPosition(mTempRect, wasLeftRightSplit);
        updateInvisibleRect();
@@ -518,7 +513,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
        mRootBounds.set(tmpRect);
        mIsLeftRightSplit = SplitScreenUtils.isLeftRightSplit(mAllowLeftRightSplitInPortrait,
                mIsLargeScreen, mRootBounds.width() >= mRootBounds.height());
        mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds);
        updateLayouts();
        initDividerPosition(mTempRect, wasLeftRightSplit);
    }

@@ -652,7 +647,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
        if (!mPinnedTaskbarInsets.equals(pinnedTaskbarInsets)) {
            mPinnedTaskbarInsets = pinnedTaskbarInsets;
            // Refresh the DividerSnapAlgorithm.
            mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds);
            updateLayouts();
            // If the divider is no longer placed on a snap point, animate it to the nearest one.
            DividerSnapAlgorithm.SnapTarget snapTarget =
                    findSnapTarget(mDividerPosition, 0, false /* hardDismiss */);
@@ -824,8 +819,22 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
        return mDividerSnapAlgorithm.calculateSnapTarget(position, velocity, hardDismiss);
    }

    private DividerSnapAlgorithm getSnapAlgorithm(Context context, Rect rootBounds) {
        final Rect insets = getDisplayStableInsets(context);
    /**
     * (Re)calculates the split screen logic for this particular display/orientation. Refreshes the
     * DividerSnapAlgorithm, which controls divider snap points, and populates a map in SplitState
     * with bounds for all valid split layouts.
     */
    private void updateLayouts() {
        // Update SplitState map

        if (Flags.enableFlexibleTwoAppSplit()) {
            mSplitState.populateLayouts(
                    mRootBounds, mDividerSize, mIsLeftRightSplit, mPinnedTaskbarInsets.toRect());
        }

        // Get new DividerSnapAlgorithm

        final Rect insets = getDisplayStableInsets(mContext);

        // Make split axis insets value same as the larger one to avoid bounds1 and bounds2
        // have difference for avoiding size-compat mode when switching unresizable apps in
@@ -835,10 +844,10 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
            insets.set(insets.left, largerInsets, insets.right, largerInsets);
        }

        return new DividerSnapAlgorithm(
                context.getResources(),
                rootBounds.width(),
                rootBounds.height(),
        mDividerSnapAlgorithm = new DividerSnapAlgorithm(
                mContext.getResources(),
                mRootBounds.width(),
                mRootBounds.height(),
                mDividerSize,
                mIsLeftRightSplit,
                insets,
+183 −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 com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_10_90;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_33_66;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_66_33;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_90_10;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_3_10_45_45;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_3_33_33_33;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_3_45_45_10;
import static com.android.wm.shell.shared.split.SplitScreenConstants.stateToString;

import android.graphics.Rect;
import android.graphics.RectF;
import android.util.Log;

import com.android.wm.shell.shared.split.SplitScreenConstants.SplitScreenState;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * A reference class that stores the split layouts available in this device/orientation. Layouts are
 * available as lists of RectFs, where each RectF represents the bounds of an app.
 */
public class SplitSpec {
    private static final String TAG = "SplitSpec";
    private static final boolean DEBUG = true;

    /** A split ratio used on larger screens, where we can fit both apps onscreen. */
    public static final float ONSCREEN_ONLY_ASYMMETRIC_RATIO = 0.33f;
    /** A split ratio used on smaller screens, where we place one app mostly offscreen. */
    public static final float OFFSCREEN_ASYMMETRIC_RATIO = 0.1f;
    /** A 50-50 split ratio. */
    public static final float MIDDLE_RATIO = 0.5f;

    private final boolean mIsLeftRightSplit;
    /** The usable display area, considering insets that affect split bounds. */
    private final RectF mUsableArea;
    /** Half the divider size. */
    private final float mHalfDiv;

    /** A large map that stores all valid split layouts. */
    private final Map<Integer, List<RectF>> mLayouts = new HashMap<>();

    /** Constructor; initializes the layout map. */
    public SplitSpec(Rect displayBounds, int dividerSize, boolean isLeftRightSplit,
            Rect pinnedTaskbarInsets) {
        mIsLeftRightSplit = isLeftRightSplit;
        mUsableArea = new RectF(displayBounds);
        mUsableArea.left += pinnedTaskbarInsets.left;
        mUsableArea.top += pinnedTaskbarInsets.top;
        mUsableArea.right -= pinnedTaskbarInsets.right;
        mUsableArea.bottom -= pinnedTaskbarInsets.bottom;
        mHalfDiv = dividerSize / 2f;

        // The "start" position, considering insets.
        float s = isLeftRightSplit ? mUsableArea.left : mUsableArea.top;
        // The "end" position, considering insets.
        float e = isLeftRightSplit ? mUsableArea.right : mUsableArea.bottom;
        // The "length" of the usable display (width or height). Apps are arranged along this axis.
        float l = e - s;
        float divPos;
        float divPos2;

        // SNAP_TO_2_10_90
        divPos = s + (l * OFFSCREEN_ASYMMETRIC_RATIO);
        createAppLayout(SNAP_TO_2_10_90, divPos);

        // SNAP_TO_2_33_66
        divPos = s + (l * ONSCREEN_ONLY_ASYMMETRIC_RATIO);
        createAppLayout(SNAP_TO_2_33_66, divPos);

        // SNAP_TO_2_50_50
        divPos = s + (l * MIDDLE_RATIO);
        createAppLayout(SNAP_TO_2_50_50, divPos);

        // SNAP_TO_2_66_33
        divPos = s + (l * (1 - ONSCREEN_ONLY_ASYMMETRIC_RATIO));
        createAppLayout(SNAP_TO_2_66_33, divPos);

        // SNAP_TO_2_90_10
        divPos = s + (l * (1 - OFFSCREEN_ASYMMETRIC_RATIO));
        createAppLayout(SNAP_TO_2_90_10, divPos);

        // SNAP_TO_3_10_45_45
        divPos = s + (l * OFFSCREEN_ASYMMETRIC_RATIO);
        divPos2 = e - ((l * (1 - OFFSCREEN_ASYMMETRIC_RATIO)) / 2f);
        createAppLayout(SNAP_TO_3_10_45_45, divPos, divPos2);

        // SNAP_TO_3_33_33_33
        divPos = s + (l * ONSCREEN_ONLY_ASYMMETRIC_RATIO);
        divPos2 = e - (l * ONSCREEN_ONLY_ASYMMETRIC_RATIO);
        createAppLayout(SNAP_TO_3_33_33_33, divPos, divPos2);

        // SNAP_TO_3_45_45_10
        divPos = s + ((l * (1 - OFFSCREEN_ASYMMETRIC_RATIO)) / 2f);
        divPos2 = e - (l * OFFSCREEN_ASYMMETRIC_RATIO);
        createAppLayout(SNAP_TO_3_45_45_10, divPos, divPos2);

        if (DEBUG) {
            dump();
        }
    }

    /**
     * Creates a two-app layout and enters it into the layout map.
     * @param divPos The position of the divider.
     */
    private void createAppLayout(@SplitScreenState int state, float divPos) {
        List<RectF> list = new ArrayList<>();
        RectF rect1 = new RectF(mUsableArea);
        RectF rect2 = new RectF(mUsableArea);
        if (mIsLeftRightSplit) {
            rect1.right = divPos - mHalfDiv;
            rect2.left = divPos + mHalfDiv;
        } else {
            rect1.top = divPos - mHalfDiv;
            rect2.bottom = divPos + mHalfDiv;
        }
        list.add(rect1);
        list.add(rect2);
        mLayouts.put(state, list);
    }

    /**
     * Creates a three-app layout and enters it into the layout map.
     * @param divPos1 The position of the first divider.
     * @param divPos2 The position of the second divider.
     */
    private void createAppLayout(@SplitScreenState int state, float divPos1, float divPos2) {
        List<RectF> list = new ArrayList<>();
        RectF rect1 = new RectF(mUsableArea);
        RectF rect2 = new RectF(mUsableArea);
        RectF rect3 = new RectF(mUsableArea);
        if (mIsLeftRightSplit) {
            rect1.right = divPos1 - mHalfDiv;
            rect2.left = divPos1 + mHalfDiv;
            rect2.right = divPos2 - mHalfDiv;
            rect3.left = divPos2 + mHalfDiv;
        } else {
            rect1.right = divPos1 - mHalfDiv;
            rect2.left = divPos1 + mHalfDiv;
            rect3.right = divPos2 - mHalfDiv;
            rect3.left = divPos2 + mHalfDiv;
        }
        list.add(rect1);
        list.add(rect2);
        list.add(rect3);
        mLayouts.put(state, list);
    }

    /** Logs all calculated layouts */
    private void dump() {
        mLayouts.forEach((k, v) -> {
            Log.d(TAG, stateToString(k));
            v.forEach(rect -> Log.d(TAG, " - " + rect.toShortString()));
        });
    }

    /** Returns the layout associated with a given split state. */
    List<RectF> getSpec(@SplitScreenState int state) {
        return mLayouts.get(state);
    }
}
+18 −0
Original line number Diff line number Diff line
@@ -19,11 +19,17 @@ package com.android.wm.shell.common.split;
import static com.android.wm.shell.shared.split.SplitScreenConstants.NOT_IN_SPLIT;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SplitScreenState;

import android.graphics.Rect;
import android.graphics.RectF;

import java.util.List;

/**
 * A class that manages the "state" of split screen. See {@link SplitScreenState} for definitions.
 */
public class SplitState {
    private @SplitScreenState int mState = NOT_IN_SPLIT;
    private SplitSpec mSplitSpec;

    /** Updates the current state of split screen on this device. */
    public void set(@SplitScreenState int newState) {
@@ -39,4 +45,16 @@ public class SplitState {
    public void exit() {
        set(NOT_IN_SPLIT);
    }

    /** Refresh the valid layouts for this display/orientation. */
    public void populateLayouts(Rect displayBounds, int dividerSize, boolean isLeftRightSplit,
            Rect pinnedTaskbarInsets) {
        mSplitSpec =
                new SplitSpec(displayBounds, dividerSize, isLeftRightSplit, pinnedTaskbarInsets);
    }

    /** Returns the layout associated with a given split state. */
    public List<RectF> getLayout(@SplitScreenState int state) {
        return mSplitSpec.getSpec(state);
    }
}
Loading