Loading libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java +2 −2 Original line number Diff line number Diff line Loading @@ -90,8 +90,8 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { private int mHandleRegionHeight; /** * Tracks divider bar visible bounds in screen-based coordination. Used to calculate with * insets. * This is not the visible bounds you see on screen, but the actual behind-the-scenes window * bounds, which is larger. */ private final Rect mDividerBounds = new Rect(); private final Rect mTempRect = new Rect(); Loading libs/WindowManager/Shell/src/com/android/wm/shell/common/split/OffscreenTouchZone.java 0 → 100644 +148 −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.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; import static com.android.wm.shell.common.split.SplitLayout.RESTING_TOUCH_LAYER; import android.content.Context; import android.content.res.Configuration; import android.graphics.PixelFormat; import android.os.Binder; import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.view.WindowlessWindowManager; import com.android.wm.shell.common.SyncTransactionQueue; /** * Holds and manages a single touchable surface. These are used in offscreen split layouts, where * we use them as a signal that the user wants to bring an offscreen app back onscreen. * <br> * Split root * / | \ * Stage root Divider Stage root * / \ * Task *this class* * */ public class OffscreenTouchZone { private static final String TAG = "OffscreenTouchZone"; /** * Whether this touch zone is on the top/left or the bottom/right screen edge. */ private final boolean mIsTopLeft; /** The function that will be run when this zone is tapped. */ private final Runnable mOnClickRunnable; private SurfaceControlViewHost mViewHost; /** * @param isTopLeft Whether the desired touch zone will be on the top/left or the bottom/right * screen edge. * @param runnable The function to run when the touch zone is tapped. */ OffscreenTouchZone(boolean isTopLeft, Runnable runnable) { mIsTopLeft = isTopLeft; mOnClickRunnable = runnable; } /** Sets up a touch zone. */ public void inflate(Context context, Configuration config, SyncTransactionQueue syncQueue, SurfaceControl stageRoot) { View touchableView = new View(context); touchableView.setOnTouchListener(new OffscreenTouchListener()); // Set WM flags, tokens, and sizing on the touchable view. It will be the same size as its // parent, the stage root. // TODO (b/349828130): It's a bit wasteful to have the touch zone cover the whole app // surface, even extending offscreen (keeps buffer active in memory), so can trim it down // to the visible onscreen area in a future patch. WindowManager.LayoutParams lp = new WindowManager.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.TYPE_INPUT_CONSUMER, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT); lp.token = new Binder(); lp.setTitle(TAG); lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY; touchableView.setLayoutParams(lp); // Create a new leash under our stage leash. final SurfaceControl.Builder builder = new SurfaceControl.Builder() .setContainerLayer() .setName(TAG + (mIsTopLeft ? "TopLeft" : "BottomRight")) .setCallsite("OffscreenTouchZone::init"); builder.setParent(stageRoot); SurfaceControl leash = builder.build(); // Create a ViewHost that will hold our view. WindowlessWindowManager wwm = new WindowlessWindowManager(config, leash, null); mViewHost = new SurfaceControlViewHost(context, context.getDisplay(), wwm, "SplitTouchZones"); mViewHost.setView(touchableView, lp); // Create a transaction so that we can activate and reposition our surface. SurfaceControl.Transaction t = new SurfaceControl.Transaction(); // Set layer to maximum. We want this surface to be above the app layer, or else touches // will be blocked. t.setLayer(leash, RESTING_TOUCH_LAYER); // Leash starts off hidden, show it. t.show(leash); syncQueue.runInSync(transaction -> { transaction.merge(t); t.close(); }); } /** Releases the touch zone when it's no longer needed. */ void release() { if (mViewHost != null) { mViewHost.release(); } } /** * Listens for touch events. * TODO (b/349828130): Update for mouse click events as well, and possibly keyboard? */ private class OffscreenTouchListener implements View.OnTouchListener { @Override public boolean onTouch(View view, MotionEvent motionEvent) { if (motionEvent.getAction() == MotionEvent.ACTION_UP) { mOnClickRunnable.run(); return true; } return false; } } /** * Returns {@code true} if this touch zone represents an offscreen app on the top/left edge of * the display, {@code false} for bottom/right. */ public boolean isTopLeft() { return mIsTopLeft; } } libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java +13 −3 Original line number Diff line number Diff line Loading @@ -23,8 +23,8 @@ 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.SplitLayout.ANIMATING_BACK_APP_VEIL_LAYER; import static com.android.wm.shell.common.split.SplitLayout.ANIMATING_FRONT_APP_VEIL_LAYER; import static com.android.wm.shell.shared.split.SplitScreenConstants.FADE_DURATION; import static com.android.wm.shell.shared.split.SplitScreenConstants.VEIL_DELAY_DURATION; Loading Loading @@ -66,6 +66,13 @@ import java.util.function.Consumer; * Currently, we show a veil when: * a) Task is resizing down from a fullscreen window. * b) Task is being stretched past its original bounds. * <br> * Split root * / | \ * Stage root Divider Stage root * / \ * Task *this class* * */ public class SplitDecorManager extends WindowlessWindowManager { private static final String TAG = SplitDecorManager.class.getSimpleName(); Loading @@ -77,6 +84,7 @@ public class SplitDecorManager extends WindowlessWindowManager { private Drawable mIcon; private ImageView mVeilIconView; private SurfaceControlViewHost mViewHost; /** The parent surface that this is attached to. Should be the stage root. */ private SurfaceControl mHostLeash; private SurfaceControl mIconLeash; private SurfaceControl mBackgroundLeash; Loading Loading @@ -389,7 +397,9 @@ public class SplitDecorManager extends WindowlessWindowManager { mOffsetX = (int) iconOffsetX; mOffsetY = (int) iconOffsetY; t.setLayer(leash, isGoingBehind ? BEHIND_APP_VEIL_LAYER : FRONT_APP_VEIL_LAYER); t.setLayer(leash, isGoingBehind ? ANIMATING_BACK_APP_VEIL_LAYER : ANIMATING_FRONT_APP_VEIL_LAYER); if (!mShown) { if (mFadeAnimator != null && mFadeAnimator.isRunning()) { Loading libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +102 −26 Original line number Diff line number Diff line Loading @@ -33,6 +33,8 @@ import static com.android.wm.shell.shared.animation.Interpolators.LINEAR; import static com.android.wm.shell.shared.animation.Interpolators.SLOWDOWN_INTERPOLATOR; 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_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_45_45_10; import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_END_AND_DISMISS; import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_START_AND_DISMISS; import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; Loading Loading @@ -72,6 +74,7 @@ import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.protolog.ProtoLog; 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; Loading @@ -80,6 +83,7 @@ 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.DividerSnapAlgorithm.SnapTarget; import com.android.wm.shell.common.split.SplitWindowManager.ParentContainerCallbacks; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition; Loading @@ -88,6 +92,7 @@ import com.android.wm.shell.shared.split.SplitScreenConstants.SplitPosition; import com.android.wm.shell.splitscreen.StageTaskListener; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; Loading @@ -112,15 +117,19 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange 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. Note: This does not affect any other layer numbering systems because // the layer system in WindowManager is local within sibling groups. So, for example, each // "veil layer" defined here actually has two sub-layers; and *their* layer values, which we set // in SplitDecorManager, are only important relative to each other. public static final int DIVIDER_LAYER = 0; public static final int FRONT_APP_VEIL_LAYER = DIVIDER_LAYER + 20; public static final int FRONT_APP_LAYER = DIVIDER_LAYER + 10; public static final int BEHIND_APP_VEIL_LAYER = DIVIDER_LAYER - 10; public static final int BEHIND_APP_LAYER = DIVIDER_LAYER - 20; // layers stay in order. (During transitions, everything is reparented onto a transition root // and can be freely relayered.) public static final int ANIMATING_DIVIDER_LAYER = 0; public static final int ANIMATING_FRONT_APP_VEIL_LAYER = ANIMATING_DIVIDER_LAYER + 20; public static final int ANIMATING_FRONT_APP_LAYER = ANIMATING_DIVIDER_LAYER + 10; public static final int ANIMATING_BACK_APP_VEIL_LAYER = ANIMATING_DIVIDER_LAYER - 10; public static final int ANIMATING_BACK_APP_LAYER = ANIMATING_DIVIDER_LAYER - 20; // The divider is on the split root, and is sibling with the stage roots. We want to keep it // above the app stages. public static final int RESTING_DIVIDER_LAYER = Integer.MAX_VALUE; // The touch layer is on a stage root, and is sibling with things like the app activity itself // and the app veil. We want it to be above all those. public static final int RESTING_TOUCH_LAYER = Integer.MAX_VALUE; // Animation specs for the swap animation private static final int SWAP_ANIMATION_TOTAL_DURATION = 500; Loading Loading @@ -155,10 +164,16 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange // The temp bounds outside of display bounds for side stage when split screen inactive to avoid // flicker next time active split screen. private final Rect mInvisibleBounds = new Rect(); /** * Areas on the screen that the user can touch to shift the layout, bringing offscreen apps * onscreen. If n apps are offscreen, there should be n such areas. Empty otherwise. */ private final List<OffscreenTouchZone> mOffscreenTouchZones = new ArrayList<>(); private final SplitLayoutHandler mSplitLayoutHandler; private final SplitWindowManager mSplitWindowManager; private final DisplayController mDisplayController; private final DisplayImeController mDisplayImeController; private final ParentContainerCallbacks mParentContainerCallbacks; private final ImePositionProcessor mImePositionProcessor; private final ResizingEffectPolicy mSurfaceEffectPolicy; private final ShellTaskOrganizer mTaskOrganizer; Loading Loading @@ -199,6 +214,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mSplitLayoutHandler = splitLayoutHandler; mDisplayController = displayController; mDisplayImeController = displayImeController; mParentContainerCallbacks = parentContainerCallbacks; mSplitWindowManager = new SplitWindowManager(windowName, mContext, configuration, parentContainerCallbacks); mTaskOrganizer = taskOrganizer; Loading Loading @@ -269,18 +285,6 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange return new Rect(mRootBounds); } /** Gets bounds of divider window with screen based coordinate. */ public Rect getDividerBounds() { return new Rect(mDividerBounds); } /** Gets bounds of divider window with parent based coordinate. */ public Rect getRefDividerBounds() { final Rect outBounds = getDividerBounds(); outBounds.offset(-mRootBounds.left, -mRootBounds.top); return outBounds; } /** Copies the top/left bounds to the provided Rect (screen-based coordinates). */ public void copyTopLeftBounds(Rect rect) { rect.set(getTopLeftBounds()); Loading Loading @@ -319,12 +323,36 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange return mContentBounds.getLast(); } /** Gets bounds of divider window with screen based coordinate on the param Rect. */ /** * Gets the bounds of divider window, in screen-based coordinates. This is not the visible * bounds you see on screen, but the actual behind-the-scenes window bounds, which is larger. */ public Rect getDividerBounds() { return new Rect(mDividerBounds); } /** * Gets the bounds of divider window, in parent-based coordinates. This is not the visible * bounds you see on screen, but the actual behind-the-scenes window bounds, which is larger. */ public Rect getRefDividerBounds() { final Rect outBounds = getDividerBounds(); outBounds.offset(-mRootBounds.left, -mRootBounds.top); return outBounds; } /** * Gets the bounds of divider window, in screen-based coordinates. This is not the visible * bounds you see on screen, but the actual behind-the-scenes window bounds, which is larger. */ public void getDividerBounds(Rect rect) { rect.set(mDividerBounds); } /** Gets bounds of divider window with parent based coordinate on the param Rect. */ /** * Gets the bounds of divider window, in parent-based coordinates. This is not the visible * bounds you see on screen, but the actual behind-the-scenes window bounds, which is larger. */ public void getRefDividerBounds(Rect rect) { getDividerBounds(rect); rect.offset(-mRootBounds.left, -mRootBounds.top); Loading Loading @@ -372,6 +400,46 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mIsLeftRightSplit ? 0 : mRootBounds.bottom); } /** * (Re)calculates and activates any needed touch zones, so the user can tap them and retrieve * offscreen apps. */ public void populateTouchZones() { if (!Flags.enableFlexibleTwoAppSplit()) { return; } if (!mOffscreenTouchZones.isEmpty()) { removeTouchZones(); } int currentPosition = calculateCurrentSnapPosition(); switch (currentPosition) { case SNAP_TO_2_10_90: case SNAP_TO_3_10_45_45: mOffscreenTouchZones.add(new OffscreenTouchZone(true /* isTopLeft */, () -> flingDividerToOtherSide(currentPosition))); break; case SNAP_TO_2_90_10: case SNAP_TO_3_45_45_10: mOffscreenTouchZones.add(new OffscreenTouchZone(false /* isTopLeft */, () -> flingDividerToOtherSide(currentPosition))); break; } mOffscreenTouchZones.forEach(mParentContainerCallbacks::inflateOnStageRoot); } /** Removes all touch zones. */ public void removeTouchZones() { if (!Flags.enableFlexibleTwoAppSplit()) { return; } mOffscreenTouchZones.forEach(OffscreenTouchZone::release); mOffscreenTouchZones.clear(); } /** Applies new configuration, returns {@code false} if there's no effect to the layout. */ public boolean updateConfiguration(Configuration configuration) { // Update the split bounds when necessary. Besides root bounds changed, split bounds need to Loading Loading @@ -509,6 +577,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange if (mInitialized) return; mInitialized = true; mSplitWindowManager.init(this, mInsetsState, false /* isRestoring */); populateTouchZones(); mDisplayImeController.addPositionProcessor(mImePositionProcessor); } Loading @@ -517,6 +586,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange if (!mInitialized) return; mInitialized = false; mSplitWindowManager.release(t); removeTouchZones(); mDisplayImeController.removePositionProcessor(mImePositionProcessor); mImePositionProcessor.reset(); if (mDividerFlingAnimator != null) { Loading @@ -540,6 +610,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mImePositionProcessor.reset(); } mSplitWindowManager.init(this, mInsetsState, true /* isRestoring */); populateTouchZones(); // Update the surface positions again after recreating the divider in case nothing else // triggers it mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this); Loading Loading @@ -782,6 +853,9 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange * DividerSnapAlgorithm will need to be refactored, and this function will change as well. */ public void flingDividerToOtherSide(@PersistentSnapPosition int currentSnapPosition) { // If a fling animation is already running, just return. if (mDividerFlingAnimator != null) return; switch (currentSnapPosition) { case SNAP_TO_2_10_90 -> snapToTarget(mDividerPosition, mDividerSnapAlgorithm.getLastSplitTarget(), Loading Loading @@ -1018,9 +1092,11 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange // Set layers if (taskInfo != null) { t.setLayer(leash, isGoingBehind ? BEHIND_APP_LAYER : FRONT_APP_LAYER); t.setLayer(leash, isGoingBehind ? ANIMATING_BACK_APP_LAYER : ANIMATING_FRONT_APP_LAYER); } else { t.setLayer(leash, DIVIDER_LAYER); t.setLayer(leash, ANIMATING_DIVIDER_LAYER); } if (offsetX == 0 && offsetY == 0) { Loading Loading @@ -1079,7 +1155,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange getRefDividerBounds(mTempRect); t.setPosition(dividerLeash, mTempRect.left, mTempRect.top); // Resets layer of divider bar to make sure it is always on top. t.setLayer(dividerLeash, Integer.MAX_VALUE); t.setLayer(dividerLeash, RESTING_DIVIDER_LAYER); } copyTopLeftRefBounds(mTempRect); t.setPosition(leash1, mTempRect.left, mTempRect.top) Loading libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java +2 −0 Original line number Diff line number Diff line Loading @@ -67,6 +67,8 @@ public final class SplitWindowManager extends WindowlessWindowManager { public interface ParentContainerCallbacks { void attachToParentSurface(SurfaceControl.Builder b); void onLeashReady(SurfaceControl leash); /** Inflates the given touch zone on the appropriate stage root. */ void inflateOnStageRoot(OffscreenTouchZone touchZone); } public SplitWindowManager(String windowName, Context context, Configuration config, Loading Loading
libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java +2 −2 Original line number Diff line number Diff line Loading @@ -90,8 +90,8 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { private int mHandleRegionHeight; /** * Tracks divider bar visible bounds in screen-based coordination. Used to calculate with * insets. * This is not the visible bounds you see on screen, but the actual behind-the-scenes window * bounds, which is larger. */ private final Rect mDividerBounds = new Rect(); private final Rect mTempRect = new Rect(); Loading
libs/WindowManager/Shell/src/com/android/wm/shell/common/split/OffscreenTouchZone.java 0 → 100644 +148 −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.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; import static com.android.wm.shell.common.split.SplitLayout.RESTING_TOUCH_LAYER; import android.content.Context; import android.content.res.Configuration; import android.graphics.PixelFormat; import android.os.Binder; import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.view.WindowlessWindowManager; import com.android.wm.shell.common.SyncTransactionQueue; /** * Holds and manages a single touchable surface. These are used in offscreen split layouts, where * we use them as a signal that the user wants to bring an offscreen app back onscreen. * <br> * Split root * / | \ * Stage root Divider Stage root * / \ * Task *this class* * */ public class OffscreenTouchZone { private static final String TAG = "OffscreenTouchZone"; /** * Whether this touch zone is on the top/left or the bottom/right screen edge. */ private final boolean mIsTopLeft; /** The function that will be run when this zone is tapped. */ private final Runnable mOnClickRunnable; private SurfaceControlViewHost mViewHost; /** * @param isTopLeft Whether the desired touch zone will be on the top/left or the bottom/right * screen edge. * @param runnable The function to run when the touch zone is tapped. */ OffscreenTouchZone(boolean isTopLeft, Runnable runnable) { mIsTopLeft = isTopLeft; mOnClickRunnable = runnable; } /** Sets up a touch zone. */ public void inflate(Context context, Configuration config, SyncTransactionQueue syncQueue, SurfaceControl stageRoot) { View touchableView = new View(context); touchableView.setOnTouchListener(new OffscreenTouchListener()); // Set WM flags, tokens, and sizing on the touchable view. It will be the same size as its // parent, the stage root. // TODO (b/349828130): It's a bit wasteful to have the touch zone cover the whole app // surface, even extending offscreen (keeps buffer active in memory), so can trim it down // to the visible onscreen area in a future patch. WindowManager.LayoutParams lp = new WindowManager.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.TYPE_INPUT_CONSUMER, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT); lp.token = new Binder(); lp.setTitle(TAG); lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY; touchableView.setLayoutParams(lp); // Create a new leash under our stage leash. final SurfaceControl.Builder builder = new SurfaceControl.Builder() .setContainerLayer() .setName(TAG + (mIsTopLeft ? "TopLeft" : "BottomRight")) .setCallsite("OffscreenTouchZone::init"); builder.setParent(stageRoot); SurfaceControl leash = builder.build(); // Create a ViewHost that will hold our view. WindowlessWindowManager wwm = new WindowlessWindowManager(config, leash, null); mViewHost = new SurfaceControlViewHost(context, context.getDisplay(), wwm, "SplitTouchZones"); mViewHost.setView(touchableView, lp); // Create a transaction so that we can activate and reposition our surface. SurfaceControl.Transaction t = new SurfaceControl.Transaction(); // Set layer to maximum. We want this surface to be above the app layer, or else touches // will be blocked. t.setLayer(leash, RESTING_TOUCH_LAYER); // Leash starts off hidden, show it. t.show(leash); syncQueue.runInSync(transaction -> { transaction.merge(t); t.close(); }); } /** Releases the touch zone when it's no longer needed. */ void release() { if (mViewHost != null) { mViewHost.release(); } } /** * Listens for touch events. * TODO (b/349828130): Update for mouse click events as well, and possibly keyboard? */ private class OffscreenTouchListener implements View.OnTouchListener { @Override public boolean onTouch(View view, MotionEvent motionEvent) { if (motionEvent.getAction() == MotionEvent.ACTION_UP) { mOnClickRunnable.run(); return true; } return false; } } /** * Returns {@code true} if this touch zone represents an offscreen app on the top/left edge of * the display, {@code false} for bottom/right. */ public boolean isTopLeft() { return mIsTopLeft; } }
libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java +13 −3 Original line number Diff line number Diff line Loading @@ -23,8 +23,8 @@ 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.SplitLayout.ANIMATING_BACK_APP_VEIL_LAYER; import static com.android.wm.shell.common.split.SplitLayout.ANIMATING_FRONT_APP_VEIL_LAYER; import static com.android.wm.shell.shared.split.SplitScreenConstants.FADE_DURATION; import static com.android.wm.shell.shared.split.SplitScreenConstants.VEIL_DELAY_DURATION; Loading Loading @@ -66,6 +66,13 @@ import java.util.function.Consumer; * Currently, we show a veil when: * a) Task is resizing down from a fullscreen window. * b) Task is being stretched past its original bounds. * <br> * Split root * / | \ * Stage root Divider Stage root * / \ * Task *this class* * */ public class SplitDecorManager extends WindowlessWindowManager { private static final String TAG = SplitDecorManager.class.getSimpleName(); Loading @@ -77,6 +84,7 @@ public class SplitDecorManager extends WindowlessWindowManager { private Drawable mIcon; private ImageView mVeilIconView; private SurfaceControlViewHost mViewHost; /** The parent surface that this is attached to. Should be the stage root. */ private SurfaceControl mHostLeash; private SurfaceControl mIconLeash; private SurfaceControl mBackgroundLeash; Loading Loading @@ -389,7 +397,9 @@ public class SplitDecorManager extends WindowlessWindowManager { mOffsetX = (int) iconOffsetX; mOffsetY = (int) iconOffsetY; t.setLayer(leash, isGoingBehind ? BEHIND_APP_VEIL_LAYER : FRONT_APP_VEIL_LAYER); t.setLayer(leash, isGoingBehind ? ANIMATING_BACK_APP_VEIL_LAYER : ANIMATING_FRONT_APP_VEIL_LAYER); if (!mShown) { if (mFadeAnimator != null && mFadeAnimator.isRunning()) { Loading
libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +102 −26 Original line number Diff line number Diff line Loading @@ -33,6 +33,8 @@ import static com.android.wm.shell.shared.animation.Interpolators.LINEAR; import static com.android.wm.shell.shared.animation.Interpolators.SLOWDOWN_INTERPOLATOR; 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_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_45_45_10; import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_END_AND_DISMISS; import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_START_AND_DISMISS; import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; Loading Loading @@ -72,6 +74,7 @@ import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.protolog.ProtoLog; 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; Loading @@ -80,6 +83,7 @@ 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.DividerSnapAlgorithm.SnapTarget; import com.android.wm.shell.common.split.SplitWindowManager.ParentContainerCallbacks; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition; Loading @@ -88,6 +92,7 @@ import com.android.wm.shell.shared.split.SplitScreenConstants.SplitPosition; import com.android.wm.shell.splitscreen.StageTaskListener; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; Loading @@ -112,15 +117,19 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange 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. Note: This does not affect any other layer numbering systems because // the layer system in WindowManager is local within sibling groups. So, for example, each // "veil layer" defined here actually has two sub-layers; and *their* layer values, which we set // in SplitDecorManager, are only important relative to each other. public static final int DIVIDER_LAYER = 0; public static final int FRONT_APP_VEIL_LAYER = DIVIDER_LAYER + 20; public static final int FRONT_APP_LAYER = DIVIDER_LAYER + 10; public static final int BEHIND_APP_VEIL_LAYER = DIVIDER_LAYER - 10; public static final int BEHIND_APP_LAYER = DIVIDER_LAYER - 20; // layers stay in order. (During transitions, everything is reparented onto a transition root // and can be freely relayered.) public static final int ANIMATING_DIVIDER_LAYER = 0; public static final int ANIMATING_FRONT_APP_VEIL_LAYER = ANIMATING_DIVIDER_LAYER + 20; public static final int ANIMATING_FRONT_APP_LAYER = ANIMATING_DIVIDER_LAYER + 10; public static final int ANIMATING_BACK_APP_VEIL_LAYER = ANIMATING_DIVIDER_LAYER - 10; public static final int ANIMATING_BACK_APP_LAYER = ANIMATING_DIVIDER_LAYER - 20; // The divider is on the split root, and is sibling with the stage roots. We want to keep it // above the app stages. public static final int RESTING_DIVIDER_LAYER = Integer.MAX_VALUE; // The touch layer is on a stage root, and is sibling with things like the app activity itself // and the app veil. We want it to be above all those. public static final int RESTING_TOUCH_LAYER = Integer.MAX_VALUE; // Animation specs for the swap animation private static final int SWAP_ANIMATION_TOTAL_DURATION = 500; Loading Loading @@ -155,10 +164,16 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange // The temp bounds outside of display bounds for side stage when split screen inactive to avoid // flicker next time active split screen. private final Rect mInvisibleBounds = new Rect(); /** * Areas on the screen that the user can touch to shift the layout, bringing offscreen apps * onscreen. If n apps are offscreen, there should be n such areas. Empty otherwise. */ private final List<OffscreenTouchZone> mOffscreenTouchZones = new ArrayList<>(); private final SplitLayoutHandler mSplitLayoutHandler; private final SplitWindowManager mSplitWindowManager; private final DisplayController mDisplayController; private final DisplayImeController mDisplayImeController; private final ParentContainerCallbacks mParentContainerCallbacks; private final ImePositionProcessor mImePositionProcessor; private final ResizingEffectPolicy mSurfaceEffectPolicy; private final ShellTaskOrganizer mTaskOrganizer; Loading Loading @@ -199,6 +214,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mSplitLayoutHandler = splitLayoutHandler; mDisplayController = displayController; mDisplayImeController = displayImeController; mParentContainerCallbacks = parentContainerCallbacks; mSplitWindowManager = new SplitWindowManager(windowName, mContext, configuration, parentContainerCallbacks); mTaskOrganizer = taskOrganizer; Loading Loading @@ -269,18 +285,6 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange return new Rect(mRootBounds); } /** Gets bounds of divider window with screen based coordinate. */ public Rect getDividerBounds() { return new Rect(mDividerBounds); } /** Gets bounds of divider window with parent based coordinate. */ public Rect getRefDividerBounds() { final Rect outBounds = getDividerBounds(); outBounds.offset(-mRootBounds.left, -mRootBounds.top); return outBounds; } /** Copies the top/left bounds to the provided Rect (screen-based coordinates). */ public void copyTopLeftBounds(Rect rect) { rect.set(getTopLeftBounds()); Loading Loading @@ -319,12 +323,36 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange return mContentBounds.getLast(); } /** Gets bounds of divider window with screen based coordinate on the param Rect. */ /** * Gets the bounds of divider window, in screen-based coordinates. This is not the visible * bounds you see on screen, but the actual behind-the-scenes window bounds, which is larger. */ public Rect getDividerBounds() { return new Rect(mDividerBounds); } /** * Gets the bounds of divider window, in parent-based coordinates. This is not the visible * bounds you see on screen, but the actual behind-the-scenes window bounds, which is larger. */ public Rect getRefDividerBounds() { final Rect outBounds = getDividerBounds(); outBounds.offset(-mRootBounds.left, -mRootBounds.top); return outBounds; } /** * Gets the bounds of divider window, in screen-based coordinates. This is not the visible * bounds you see on screen, but the actual behind-the-scenes window bounds, which is larger. */ public void getDividerBounds(Rect rect) { rect.set(mDividerBounds); } /** Gets bounds of divider window with parent based coordinate on the param Rect. */ /** * Gets the bounds of divider window, in parent-based coordinates. This is not the visible * bounds you see on screen, but the actual behind-the-scenes window bounds, which is larger. */ public void getRefDividerBounds(Rect rect) { getDividerBounds(rect); rect.offset(-mRootBounds.left, -mRootBounds.top); Loading Loading @@ -372,6 +400,46 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mIsLeftRightSplit ? 0 : mRootBounds.bottom); } /** * (Re)calculates and activates any needed touch zones, so the user can tap them and retrieve * offscreen apps. */ public void populateTouchZones() { if (!Flags.enableFlexibleTwoAppSplit()) { return; } if (!mOffscreenTouchZones.isEmpty()) { removeTouchZones(); } int currentPosition = calculateCurrentSnapPosition(); switch (currentPosition) { case SNAP_TO_2_10_90: case SNAP_TO_3_10_45_45: mOffscreenTouchZones.add(new OffscreenTouchZone(true /* isTopLeft */, () -> flingDividerToOtherSide(currentPosition))); break; case SNAP_TO_2_90_10: case SNAP_TO_3_45_45_10: mOffscreenTouchZones.add(new OffscreenTouchZone(false /* isTopLeft */, () -> flingDividerToOtherSide(currentPosition))); break; } mOffscreenTouchZones.forEach(mParentContainerCallbacks::inflateOnStageRoot); } /** Removes all touch zones. */ public void removeTouchZones() { if (!Flags.enableFlexibleTwoAppSplit()) { return; } mOffscreenTouchZones.forEach(OffscreenTouchZone::release); mOffscreenTouchZones.clear(); } /** Applies new configuration, returns {@code false} if there's no effect to the layout. */ public boolean updateConfiguration(Configuration configuration) { // Update the split bounds when necessary. Besides root bounds changed, split bounds need to Loading Loading @@ -509,6 +577,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange if (mInitialized) return; mInitialized = true; mSplitWindowManager.init(this, mInsetsState, false /* isRestoring */); populateTouchZones(); mDisplayImeController.addPositionProcessor(mImePositionProcessor); } Loading @@ -517,6 +586,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange if (!mInitialized) return; mInitialized = false; mSplitWindowManager.release(t); removeTouchZones(); mDisplayImeController.removePositionProcessor(mImePositionProcessor); mImePositionProcessor.reset(); if (mDividerFlingAnimator != null) { Loading @@ -540,6 +610,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mImePositionProcessor.reset(); } mSplitWindowManager.init(this, mInsetsState, true /* isRestoring */); populateTouchZones(); // Update the surface positions again after recreating the divider in case nothing else // triggers it mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this); Loading Loading @@ -782,6 +853,9 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange * DividerSnapAlgorithm will need to be refactored, and this function will change as well. */ public void flingDividerToOtherSide(@PersistentSnapPosition int currentSnapPosition) { // If a fling animation is already running, just return. if (mDividerFlingAnimator != null) return; switch (currentSnapPosition) { case SNAP_TO_2_10_90 -> snapToTarget(mDividerPosition, mDividerSnapAlgorithm.getLastSplitTarget(), Loading Loading @@ -1018,9 +1092,11 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange // Set layers if (taskInfo != null) { t.setLayer(leash, isGoingBehind ? BEHIND_APP_LAYER : FRONT_APP_LAYER); t.setLayer(leash, isGoingBehind ? ANIMATING_BACK_APP_LAYER : ANIMATING_FRONT_APP_LAYER); } else { t.setLayer(leash, DIVIDER_LAYER); t.setLayer(leash, ANIMATING_DIVIDER_LAYER); } if (offsetX == 0 && offsetY == 0) { Loading Loading @@ -1079,7 +1155,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange getRefDividerBounds(mTempRect); t.setPosition(dividerLeash, mTempRect.left, mTempRect.top); // Resets layer of divider bar to make sure it is always on top. t.setLayer(dividerLeash, Integer.MAX_VALUE); t.setLayer(dividerLeash, RESTING_DIVIDER_LAYER); } copyTopLeftRefBounds(mTempRect); t.setPosition(leash1, mTempRect.left, mTempRect.top) Loading
libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java +2 −0 Original line number Diff line number Diff line Loading @@ -67,6 +67,8 @@ public final class SplitWindowManager extends WindowlessWindowManager { public interface ParentContainerCallbacks { void attachToParentSurface(SurfaceControl.Builder b); void onLeashReady(SurfaceControl leash); /** Inflates the given touch zone on the appropriate stage root. */ void inflateOnStageRoot(OffscreenTouchZone touchZone); } public SplitWindowManager(String windowName, Context context, Configuration config, Loading