Loading libs/WindowManager/Shell/res/values/ids.xml +2 −0 Original line number Diff line number Diff line Loading @@ -21,9 +21,11 @@ <!-- Accessibility actions for the docked stack divider --> <item type="id" name="action_move_tl_full" /> <item type="id" name="action_move_tl_90" /> <item type="id" name="action_move_tl_70" /> <item type="id" name="action_move_tl_50" /> <item type="id" name="action_move_tl_30" /> <item type="id" name="action_move_tl_10" /> <item type="id" name="action_move_rb_full" /> <item type="id" name="action_swap_apps" /> Loading libs/WindowManager/Shell/res/values/strings.xml +16 −12 Original line number Diff line number Diff line Loading @@ -83,29 +83,33 @@ <string name="divider_title">Split screen divider</string> <!-- Accessibility action for moving docked stack divider to make the left screen full screen [CHAR LIMIT=NONE] --> <string name="accessibility_action_divider_left_full">Left full screen</string> <string name="accessibility_action_divider_left_full">Make left app full screen</string> <!-- Accessibility action for moving docked stack divider to make the left screen 70% [CHAR LIMIT=NONE] --> <string name="accessibility_action_divider_left_70">Left 70%</string> <string formatted="false" name="accessibility_action_divider_left_10">10% left, 90% right</string> <string formatted="false" name="accessibility_action_divider_left_30">30% left, 70% right</string> <!-- Accessibility action for moving docked stack divider to make the left screen 50% [CHAR LIMIT=NONE] --> <string name="accessibility_action_divider_left_50">Left 50%</string> <string formatted="false" name="accessibility_action_divider_left_50">50% left, 50% right</string> <!-- Accessibility action for moving docked stack divider to make the left screen 30% [CHAR LIMIT=NONE] --> <string name="accessibility_action_divider_left_30">Left 30%</string> <string formatted="false" name="accessibility_action_divider_left_70">70% left, 30% right</string> <string formatted="false" name="accessibility_action_divider_left_90">90% left, 10% right</string> <!-- Accessibility action for moving docked stack divider to make the right screen full screen [CHAR LIMIT=NONE] --> <string name="accessibility_action_divider_right_full">Right full screen</string> <string name="accessibility_action_divider_right_full">Make right app full screen</string> <!-- Accessibility action for swapping the apps around the divider (double tap action) [CHAR LIMIT=NONE] --> <string name="accessibility_action_divider_swap_vertical">Swap top app with bottom</string> <string name="accessibility_action_divider_swap_horizontal">Swap left app with right</string> <string name="accessibility_action_divider_swap_vertical">Swap top app with bottom app, keep current split</string> <string name="accessibility_action_divider_swap_horizontal">Swap left app with right app, keep current split</string> <!-- Accessibility action for moving docked stack divider to make the top screen full screen [CHAR LIMIT=NONE] --> <string name="accessibility_action_divider_top_full">Top full screen</string> <string name="accessibility_action_divider_top_full">Make top app full screen</string> <!-- Accessibility action for moving docked stack divider to make the top screen 70% [CHAR LIMIT=NONE] --> <string name="accessibility_action_divider_top_70">Top 70%</string> <string formatted="false" name="accessibility_action_divider_top_10">10% top, 90% bottom</string> <string formatted="false" name="accessibility_action_divider_top_30">30% top, 70% bottom</string> <!-- Accessibility action for moving docked stack divider to make the top screen 50% [CHAR LIMIT=NONE] --> <string name="accessibility_action_divider_top_50">Top 50%</string> <string formatted="false" name="accessibility_action_divider_top_50">50% top, 50% bottom</string> <!-- Accessibility action for moving docked stack divider to make the top screen 30% [CHAR LIMIT=NONE] --> <string name="accessibility_action_divider_top_30">Top 30%</string> <string formatted="false" name="accessibility_action_divider_top_70">70% top, 30% bottom</string> <string formatted="false" name="accessibility_action_divider_top_90">90% top, 10% bottom</string> <!-- Accessibility action for moving docked stack divider to make the bottom screen full screen [CHAR LIMIT=NONE] --> <string name="accessibility_action_divider_bottom_full">Bottom full screen</string> <string name="accessibility_action_divider_bottom_full">Make bottom app full screen</string> <!-- Accessibility label for splitting to the left drop zone [CHAR LIMIT=NONE] --> <string name="accessibility_split_left">Split left</string> Loading libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java +23 −23 Original line number Diff line number Diff line Loading @@ -100,6 +100,7 @@ public class DividerSnapAlgorithm { private final boolean mIsLeftRightSplit; /** In SNAP_MODE_MINIMIZED, the side of the screen on which an app will "dock" when minimized */ private final int mDockSide; private final SplitTargetProvider mSplitTargetProvider; /** * The first, and usually only, snap target between the left/top screen edge and center. Loading @@ -126,14 +127,16 @@ public class DividerSnapAlgorithm { private final MotionSpec mMotionSpec; public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize, boolean isLeftRightSplit, Rect insets, Rect pinnedTaskbarInsets, int dockSide) { boolean isLeftRightSplit, Rect insets, Rect pinnedTaskbarInsets, int dockSide, SplitTargetProvider splitTargetProvider) { this(res, displayWidth, displayHeight, dividerSize, isLeftRightSplit, insets, pinnedTaskbarInsets, dockSide, false /* minimized */, true /* resizable */); pinnedTaskbarInsets, dockSide, true /* resizable */, splitTargetProvider); } public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize, boolean isLeftRightSplit, Rect insets, Rect pinnedTaskbarInsets, int dockSide, boolean isMinimizedMode, boolean isHomeResizable) { boolean isHomeResizable, SplitTargetProvider splitTargetProvider) { mMinFlingVelocityPxPerSecond = MIN_FLING_VELOCITY_DP_PER_SECOND * res.getDisplayMetrics().density; mMinDismissVelocityPxPerSecond = Loading @@ -143,17 +146,10 @@ public class DividerSnapAlgorithm { mDisplayHeight = displayHeight; mIsLeftRightSplit = isLeftRightSplit; mDockSide = dockSide; mSplitTargetProvider = splitTargetProvider; mInsets.set(insets); mPinnedTaskbarInsets.set(pinnedTaskbarInsets); if (Flags.enableFlexibleTwoAppSplit()) { mSnapMode = SNAP_FLEXIBLE_HYBRID; } else { // Set SNAP_MODE_MINIMIZED, SNAP_MODE_16_9, or SNAP_FIXED_RATIO depending on config mSnapMode = isMinimizedMode ? SNAP_MODE_MINIMIZED : res.getInteger( com.android.internal.R.integer.config_dockedStackDividerSnapMode); } mSnapMode = mSplitTargetProvider.getSnapMode(); mFreeSnapMode = res.getBoolean( com.android.internal.R.bool.config_dockedStackDividerFreeSnapMode); mFixedRatio = res.getFraction( Loading Loading @@ -358,8 +354,10 @@ public class DividerSnapAlgorithm { */ private void addNonDismissingTargets(List<Integer> positions, int dividerMax) { // Get the desired layout for our snap mode. List<Integer> targetSpec = SplitSpec.getSnapTargetLayout(mSnapMode, areOffscreenRatiosSupported()); List<Integer> targetSpec = mSplitTargetProvider .getTargets(false /*includeDismissal*/) .stream().map(SplitTargetProvider.SplitTarget::getSnapPosition) .toList(); if (positions.size() != targetSpec.size()) { throw new IllegalStateException("unexpected number of snap positions"); Loading @@ -372,12 +370,18 @@ public class DividerSnapAlgorithm { int position = positions.get(i); if (!midpointPassed) { maybeAddTarget(position, position - getStartInset(), target); } else if (target == SNAP_TO_2_50_50) { if (target == SNAP_TO_2_50_50) { // midpoint mTargets.add(new SnapTarget(position, target)); midpointPassed = true; } else { maybeAddTarget(position, dividerMax - getEndInset() - (position + mDividerSize), // before midpoint maybeAddTarget(position, position - getStartInset(), target); } } else { // after midpoint maybeAddTarget(position, dividerMax - getEndInset() - (position + mDividerSize), target); } } Loading Loading @@ -552,10 +556,6 @@ public class DividerSnapAlgorithm { return mMotionSpec; } public int getSnapMode() { return mSnapMode; } /** * An object, calculated at boot time, representing a legal position for the split screen * divider (i.e. the divider can be dragged to this spot). Loading libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java +20 −52 Original line number Diff line number Diff line Loading @@ -66,6 +66,7 @@ import com.android.wm.shell.shared.desktopmode.DesktopState; import com.google.android.msdl.data.model.MSDLToken; import java.util.List; import java.util.Objects; /** Loading Loading @@ -116,6 +117,8 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { */ private final Rect mDividerBounds = new Rect(); private final Rect mTempRect = new Rect(); private SplitTargetProvider mSplitTargetProvider; private FrameLayout mDividerBar; static final Property<DividerView, Integer> DIVIDER_HEIGHT_PROPERTY = Loading Loading @@ -147,48 +150,20 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { }; final AccessibilityDelegate mHandleDelegate = new AccessibilityDelegate() { private List<SplitTargetProvider.SplitTarget> mTargets; @Override public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(host, info); final DividerSnapAlgorithm snapAlgorithm = mSplitLayout.mDividerSnapAlgorithm; mTargets = mSplitTargetProvider.getTargets(true /*includeDismissal*/); mTargets.forEach(target -> info.addAction(new AccessibilityAction(target.getA11yActionId(), mContext.getString(target.getA11yActionString())))); if (mSplitLayout.isLeftRightSplit()) { info.addAction(new AccessibilityAction(R.id.action_move_tl_full, mContext.getString(R.string.accessibility_action_divider_left_full))); if (snapAlgorithm.isFirstSplitTargetAvailable()) { info.addAction(new AccessibilityAction(R.id.action_move_tl_70, mContext.getString(R.string.accessibility_action_divider_left_70))); } if (snapAlgorithm.showMiddleSplitTargetForAccessibility()) { // Only show the middle target if there are more than 1 split target info.addAction(new AccessibilityAction(R.id.action_move_tl_50, mContext.getString(R.string.accessibility_action_divider_left_50))); } if (snapAlgorithm.isLastSplitTargetAvailable()) { info.addAction(new AccessibilityAction(R.id.action_move_tl_30, mContext.getString(R.string.accessibility_action_divider_left_30))); } info.addAction(new AccessibilityAction(R.id.action_move_rb_full, mContext.getString(R.string.accessibility_action_divider_right_full))); info.addAction(new AccessibilityAction(R.id.action_swap_apps, mContext.getString(R.string.accessibility_action_divider_swap_horizontal))); } else { info.addAction(new AccessibilityAction(R.id.action_move_tl_full, mContext.getString(R.string.accessibility_action_divider_top_full))); if (snapAlgorithm.isFirstSplitTargetAvailable()) { info.addAction(new AccessibilityAction(R.id.action_move_tl_70, mContext.getString(R.string.accessibility_action_divider_top_70))); } if (snapAlgorithm.showMiddleSplitTargetForAccessibility()) { // Only show the middle target if there are more than 1 split target info.addAction(new AccessibilityAction(R.id.action_move_tl_50, mContext.getString(R.string.accessibility_action_divider_top_50))); } if (snapAlgorithm.isLastSplitTargetAvailable()) { info.addAction(new AccessibilityAction(R.id.action_move_tl_30, mContext.getString(R.string.accessibility_action_divider_top_30))); } info.addAction(new AccessibilityAction(R.id.action_move_rb_full, mContext.getString(R.string.accessibility_action_divider_bottom_full))); info.addAction(new AccessibilityAction(R.id.action_swap_apps, mContext.getString(R.string.accessibility_action_divider_swap_vertical))); } Loading @@ -202,21 +177,12 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { return true; } SnapTarget nextTarget = null; DividerSnapAlgorithm snapAlgorithm = mSplitLayout.mDividerSnapAlgorithm; if (action == R.id.action_move_tl_full) { nextTarget = snapAlgorithm.getDismissEndTarget(); } else if (action == R.id.action_move_tl_70) { nextTarget = snapAlgorithm.getLastSplitTarget(); } else if (action == R.id.action_move_tl_50) { nextTarget = snapAlgorithm.getMiddleTarget(); } else if (action == R.id.action_move_tl_30) { nextTarget = snapAlgorithm.getFirstSplitTarget(); } else if (action == R.id.action_move_rb_full) { nextTarget = snapAlgorithm.getDismissStartTarget(); } if (nextTarget != null) { mSplitLayout.snapToTarget(mSplitLayout.getDividerPosition(), nextTarget); SplitTargetProvider.SplitTarget selectedTarget = mTargets.stream() .filter(target -> target.getA11yActionId() == action) .findFirst() .orElse(null); if (selectedTarget != null) { mSplitLayout.snapToTarget(selectedTarget.getSnapPosition()); return true; } return super.performAccessibilityAction(host, action, args); Loading Loading @@ -244,7 +210,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { /** Sets up essential dependencies of the divider bar. */ public void setup(SplitLayout layout, SplitWindowManager splitWindowManager, SurfaceControlViewHost viewHost, InsetsState insetsState, DesktopState desktopState) { DesktopState desktopState, SplitTargetProvider splitTargetProvider) { mSplitLayout = layout; mSplitWindowManager = splitWindowManager; mViewHost = viewHost; Loading @@ -264,6 +230,9 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { : desktopState.canEnterDesktopMode() ? R.dimen.desktop_mode_portrait_split_divider_handle_region_height : R.dimen.split_divider_handle_region_height); mSplitTargetProvider = splitTargetProvider; mHandle.setAccessibilityDelegate(mHandleDelegate); } void onInsetsChanged(InsetsState insetsState, boolean animate) { Loading Loading @@ -312,7 +281,6 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { mInteractive = true; mHideHandle = false; setOnTouchListener(this); mHandle.setAccessibilityDelegate(mHandleDelegate); setWillNotDraw(false); mPaint.setColor(getResources().getColor(R.color.split_divider_background, null)); mPaint.setAntiAlias(true); Loading libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SnapToTargetConverter.kt 0 → 100644 +207 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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 android.content.res.Resources import com.android.wm.shell.Flags import com.android.wm.shell.R import com.android.wm.shell.common.split.SplitTargetProvider.SplitTarget import com.android.wm.shell.shared.split.SplitScreenConstants /** * For a given snap mode, based on current feature flags and resource config values, generates list * of viable snap targets which should be accessed via the [SplitTargetProvider] interface */ class SnapToTargetConverter(val res: Resources, isMinimizedMode: Boolean) : SplitTargetProvider { private var snapMode: Int = 0 init { snapMode = if (Flags.enableFlexibleTwoAppSplit()) { DividerSnapAlgorithm.SNAP_FLEXIBLE_HYBRID } else { if (isMinimizedMode) DividerSnapAlgorithm.SNAP_MODE_MINIMIZED else res.getInteger(com.android.internal.R.integer.config_dockedStackDividerSnapMode) } } /** See [SplitScreenUtils.isLeftRightSplit] */ private fun isLeftRightSplit() : Boolean { val mAllowLeftRightSplitInPortrait = SplitScreenUtils.allowLeftRightSplitInPortrait(res) return SplitScreenUtils.isLeftRightSplit( mAllowLeftRightSplitInPortrait, res.configuration ) } /** * @param includeDismissal if true then the returned list will include the start and end * dismiss targets at the start and end of the list, respectively. * @return A [List] of [SplitTarget] for the device's current [snapMode], by ordering of left * to right based on the different snap positions */ override fun getTargets(includeDismissal: Boolean): List<SplitTarget> { val targets: MutableList<SplitTarget> = ArrayList() val areOffscreenRatiosSupported = SplitScreenUtils.allowOffscreenRatios(res) val isLeftRightSplit = isLeftRightSplit() if (includeDismissal) { targets.add( SplitTarget( SplitScreenConstants.SNAP_TO_START_AND_DISMISS, if (isLeftRightSplit) R.string.accessibility_action_divider_right_full else R.string.accessibility_action_divider_bottom_full, R.id.action_move_tl_full ) ) } when (snapMode) { DividerSnapAlgorithm.SNAP_ONLY_1_1 -> targets.add( SplitTarget( SplitScreenConstants.SNAP_TO_2_50_50, if (isLeftRightSplit) R.string.accessibility_action_divider_left_50 else R.string.accessibility_action_divider_top_50, R.id.action_move_tl_50 ) ) DividerSnapAlgorithm.SNAP_MODE_MINIMIZED -> targets.add( SplitTarget( SplitScreenConstants.SNAP_TO_MINIMIZE, if (isLeftRightSplit) R.string.accessibility_action_divider_left_full else R.string.accessibility_action_divider_top_full, R.id.action_move_tl_full ) ) DividerSnapAlgorithm.SNAP_MODE_16_9, DividerSnapAlgorithm.SNAP_FIXED_RATIO -> targets.addAll( getOnscreenTargets(isLeftRightSplit) ) DividerSnapAlgorithm.SNAP_FLEXIBLE_SPLIT -> { if (areOffscreenRatiosSupported) { targets.add( SplitTarget( SplitScreenConstants.SNAP_TO_2_10_90, if (isLeftRightSplit) R.string.accessibility_action_divider_left_10 else R.string.accessibility_action_divider_top_10, R.id.action_move_tl_90 ) ) targets.add( SplitTarget( SplitScreenConstants.SNAP_TO_2_50_50, if (isLeftRightSplit) R.string.accessibility_action_divider_left_50 else R.string.accessibility_action_divider_top_50, R.id.action_move_tl_50 ) ) targets.add( SplitTarget( SplitScreenConstants.SNAP_TO_2_90_10, if (isLeftRightSplit) R.string.accessibility_action_divider_left_90 else R.string.accessibility_action_divider_top_90, R.id.action_move_tl_10 ) ) } else { targets.addAll(getOnscreenTargets(isLeftRightSplit)) } } DividerSnapAlgorithm.SNAP_FLEXIBLE_HYBRID -> { if (areOffscreenRatiosSupported) { targets.add( SplitTarget( SplitScreenConstants.SNAP_TO_2_10_90, if (isLeftRightSplit) R.string.accessibility_action_divider_left_10 else R.string.accessibility_action_divider_top_10, R.id.action_move_tl_90 ) ) targets.addAll(getOnscreenTargets(isLeftRightSplit)) targets.add( SplitTarget( SplitScreenConstants.SNAP_TO_2_90_10, if (isLeftRightSplit) R.string.accessibility_action_divider_left_90 else R.string.accessibility_action_divider_top_90, R.id.action_move_tl_10 ) ) } else { targets.addAll(getOnscreenTargets(isLeftRightSplit)) } } else -> throw IllegalStateException("unrecognized snap mode") } if (includeDismissal) { targets.add( SplitTarget( SplitScreenConstants.SNAP_TO_END_AND_DISMISS, if (isLeftRightSplit) R.string.accessibility_action_divider_left_full else R.string.accessibility_action_divider_top_full, R.id.action_move_rb_full ) ) } return targets } /** * Current snap mode of the device. You really probably shouldn't be using this. Exposed only * for the few use cases that actually need it. */ override fun getSnapMode(): Int { return snapMode } /** * @return Default, 3 non-flex split targets. 30/50/70, in that specific order. */ private fun getOnscreenTargets(isLeftRightSplit: Boolean): List<SplitTarget> { val targets: MutableList<SplitTarget> = java.util.ArrayList(3) targets.add( SplitTarget( SplitScreenConstants.SNAP_TO_2_33_66, if (isLeftRightSplit) R.string.accessibility_action_divider_left_30 else R.string.accessibility_action_divider_top_30, R.id.action_move_tl_30 ) ) targets.add( SplitTarget( SplitScreenConstants.SNAP_TO_2_50_50, if (isLeftRightSplit) R.string.accessibility_action_divider_left_50 else R.string.accessibility_action_divider_top_50, R.id.action_move_tl_50 ) ) targets.add( SplitTarget( SplitScreenConstants.SNAP_TO_2_66_33, if (isLeftRightSplit) R.string.accessibility_action_divider_left_70 else R.string.accessibility_action_divider_top_70, R.id.action_move_tl_70 ) ) return targets } } No newline at end of file Loading
libs/WindowManager/Shell/res/values/ids.xml +2 −0 Original line number Diff line number Diff line Loading @@ -21,9 +21,11 @@ <!-- Accessibility actions for the docked stack divider --> <item type="id" name="action_move_tl_full" /> <item type="id" name="action_move_tl_90" /> <item type="id" name="action_move_tl_70" /> <item type="id" name="action_move_tl_50" /> <item type="id" name="action_move_tl_30" /> <item type="id" name="action_move_tl_10" /> <item type="id" name="action_move_rb_full" /> <item type="id" name="action_swap_apps" /> Loading
libs/WindowManager/Shell/res/values/strings.xml +16 −12 Original line number Diff line number Diff line Loading @@ -83,29 +83,33 @@ <string name="divider_title">Split screen divider</string> <!-- Accessibility action for moving docked stack divider to make the left screen full screen [CHAR LIMIT=NONE] --> <string name="accessibility_action_divider_left_full">Left full screen</string> <string name="accessibility_action_divider_left_full">Make left app full screen</string> <!-- Accessibility action for moving docked stack divider to make the left screen 70% [CHAR LIMIT=NONE] --> <string name="accessibility_action_divider_left_70">Left 70%</string> <string formatted="false" name="accessibility_action_divider_left_10">10% left, 90% right</string> <string formatted="false" name="accessibility_action_divider_left_30">30% left, 70% right</string> <!-- Accessibility action for moving docked stack divider to make the left screen 50% [CHAR LIMIT=NONE] --> <string name="accessibility_action_divider_left_50">Left 50%</string> <string formatted="false" name="accessibility_action_divider_left_50">50% left, 50% right</string> <!-- Accessibility action for moving docked stack divider to make the left screen 30% [CHAR LIMIT=NONE] --> <string name="accessibility_action_divider_left_30">Left 30%</string> <string formatted="false" name="accessibility_action_divider_left_70">70% left, 30% right</string> <string formatted="false" name="accessibility_action_divider_left_90">90% left, 10% right</string> <!-- Accessibility action for moving docked stack divider to make the right screen full screen [CHAR LIMIT=NONE] --> <string name="accessibility_action_divider_right_full">Right full screen</string> <string name="accessibility_action_divider_right_full">Make right app full screen</string> <!-- Accessibility action for swapping the apps around the divider (double tap action) [CHAR LIMIT=NONE] --> <string name="accessibility_action_divider_swap_vertical">Swap top app with bottom</string> <string name="accessibility_action_divider_swap_horizontal">Swap left app with right</string> <string name="accessibility_action_divider_swap_vertical">Swap top app with bottom app, keep current split</string> <string name="accessibility_action_divider_swap_horizontal">Swap left app with right app, keep current split</string> <!-- Accessibility action for moving docked stack divider to make the top screen full screen [CHAR LIMIT=NONE] --> <string name="accessibility_action_divider_top_full">Top full screen</string> <string name="accessibility_action_divider_top_full">Make top app full screen</string> <!-- Accessibility action for moving docked stack divider to make the top screen 70% [CHAR LIMIT=NONE] --> <string name="accessibility_action_divider_top_70">Top 70%</string> <string formatted="false" name="accessibility_action_divider_top_10">10% top, 90% bottom</string> <string formatted="false" name="accessibility_action_divider_top_30">30% top, 70% bottom</string> <!-- Accessibility action for moving docked stack divider to make the top screen 50% [CHAR LIMIT=NONE] --> <string name="accessibility_action_divider_top_50">Top 50%</string> <string formatted="false" name="accessibility_action_divider_top_50">50% top, 50% bottom</string> <!-- Accessibility action for moving docked stack divider to make the top screen 30% [CHAR LIMIT=NONE] --> <string name="accessibility_action_divider_top_30">Top 30%</string> <string formatted="false" name="accessibility_action_divider_top_70">70% top, 30% bottom</string> <string formatted="false" name="accessibility_action_divider_top_90">90% top, 10% bottom</string> <!-- Accessibility action for moving docked stack divider to make the bottom screen full screen [CHAR LIMIT=NONE] --> <string name="accessibility_action_divider_bottom_full">Bottom full screen</string> <string name="accessibility_action_divider_bottom_full">Make bottom app full screen</string> <!-- Accessibility label for splitting to the left drop zone [CHAR LIMIT=NONE] --> <string name="accessibility_split_left">Split left</string> Loading
libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java +23 −23 Original line number Diff line number Diff line Loading @@ -100,6 +100,7 @@ public class DividerSnapAlgorithm { private final boolean mIsLeftRightSplit; /** In SNAP_MODE_MINIMIZED, the side of the screen on which an app will "dock" when minimized */ private final int mDockSide; private final SplitTargetProvider mSplitTargetProvider; /** * The first, and usually only, snap target between the left/top screen edge and center. Loading @@ -126,14 +127,16 @@ public class DividerSnapAlgorithm { private final MotionSpec mMotionSpec; public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize, boolean isLeftRightSplit, Rect insets, Rect pinnedTaskbarInsets, int dockSide) { boolean isLeftRightSplit, Rect insets, Rect pinnedTaskbarInsets, int dockSide, SplitTargetProvider splitTargetProvider) { this(res, displayWidth, displayHeight, dividerSize, isLeftRightSplit, insets, pinnedTaskbarInsets, dockSide, false /* minimized */, true /* resizable */); pinnedTaskbarInsets, dockSide, true /* resizable */, splitTargetProvider); } public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize, boolean isLeftRightSplit, Rect insets, Rect pinnedTaskbarInsets, int dockSide, boolean isMinimizedMode, boolean isHomeResizable) { boolean isHomeResizable, SplitTargetProvider splitTargetProvider) { mMinFlingVelocityPxPerSecond = MIN_FLING_VELOCITY_DP_PER_SECOND * res.getDisplayMetrics().density; mMinDismissVelocityPxPerSecond = Loading @@ -143,17 +146,10 @@ public class DividerSnapAlgorithm { mDisplayHeight = displayHeight; mIsLeftRightSplit = isLeftRightSplit; mDockSide = dockSide; mSplitTargetProvider = splitTargetProvider; mInsets.set(insets); mPinnedTaskbarInsets.set(pinnedTaskbarInsets); if (Flags.enableFlexibleTwoAppSplit()) { mSnapMode = SNAP_FLEXIBLE_HYBRID; } else { // Set SNAP_MODE_MINIMIZED, SNAP_MODE_16_9, or SNAP_FIXED_RATIO depending on config mSnapMode = isMinimizedMode ? SNAP_MODE_MINIMIZED : res.getInteger( com.android.internal.R.integer.config_dockedStackDividerSnapMode); } mSnapMode = mSplitTargetProvider.getSnapMode(); mFreeSnapMode = res.getBoolean( com.android.internal.R.bool.config_dockedStackDividerFreeSnapMode); mFixedRatio = res.getFraction( Loading Loading @@ -358,8 +354,10 @@ public class DividerSnapAlgorithm { */ private void addNonDismissingTargets(List<Integer> positions, int dividerMax) { // Get the desired layout for our snap mode. List<Integer> targetSpec = SplitSpec.getSnapTargetLayout(mSnapMode, areOffscreenRatiosSupported()); List<Integer> targetSpec = mSplitTargetProvider .getTargets(false /*includeDismissal*/) .stream().map(SplitTargetProvider.SplitTarget::getSnapPosition) .toList(); if (positions.size() != targetSpec.size()) { throw new IllegalStateException("unexpected number of snap positions"); Loading @@ -372,12 +370,18 @@ public class DividerSnapAlgorithm { int position = positions.get(i); if (!midpointPassed) { maybeAddTarget(position, position - getStartInset(), target); } else if (target == SNAP_TO_2_50_50) { if (target == SNAP_TO_2_50_50) { // midpoint mTargets.add(new SnapTarget(position, target)); midpointPassed = true; } else { maybeAddTarget(position, dividerMax - getEndInset() - (position + mDividerSize), // before midpoint maybeAddTarget(position, position - getStartInset(), target); } } else { // after midpoint maybeAddTarget(position, dividerMax - getEndInset() - (position + mDividerSize), target); } } Loading Loading @@ -552,10 +556,6 @@ public class DividerSnapAlgorithm { return mMotionSpec; } public int getSnapMode() { return mSnapMode; } /** * An object, calculated at boot time, representing a legal position for the split screen * divider (i.e. the divider can be dragged to this spot). Loading
libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java +20 −52 Original line number Diff line number Diff line Loading @@ -66,6 +66,7 @@ import com.android.wm.shell.shared.desktopmode.DesktopState; import com.google.android.msdl.data.model.MSDLToken; import java.util.List; import java.util.Objects; /** Loading Loading @@ -116,6 +117,8 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { */ private final Rect mDividerBounds = new Rect(); private final Rect mTempRect = new Rect(); private SplitTargetProvider mSplitTargetProvider; private FrameLayout mDividerBar; static final Property<DividerView, Integer> DIVIDER_HEIGHT_PROPERTY = Loading Loading @@ -147,48 +150,20 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { }; final AccessibilityDelegate mHandleDelegate = new AccessibilityDelegate() { private List<SplitTargetProvider.SplitTarget> mTargets; @Override public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(host, info); final DividerSnapAlgorithm snapAlgorithm = mSplitLayout.mDividerSnapAlgorithm; mTargets = mSplitTargetProvider.getTargets(true /*includeDismissal*/); mTargets.forEach(target -> info.addAction(new AccessibilityAction(target.getA11yActionId(), mContext.getString(target.getA11yActionString())))); if (mSplitLayout.isLeftRightSplit()) { info.addAction(new AccessibilityAction(R.id.action_move_tl_full, mContext.getString(R.string.accessibility_action_divider_left_full))); if (snapAlgorithm.isFirstSplitTargetAvailable()) { info.addAction(new AccessibilityAction(R.id.action_move_tl_70, mContext.getString(R.string.accessibility_action_divider_left_70))); } if (snapAlgorithm.showMiddleSplitTargetForAccessibility()) { // Only show the middle target if there are more than 1 split target info.addAction(new AccessibilityAction(R.id.action_move_tl_50, mContext.getString(R.string.accessibility_action_divider_left_50))); } if (snapAlgorithm.isLastSplitTargetAvailable()) { info.addAction(new AccessibilityAction(R.id.action_move_tl_30, mContext.getString(R.string.accessibility_action_divider_left_30))); } info.addAction(new AccessibilityAction(R.id.action_move_rb_full, mContext.getString(R.string.accessibility_action_divider_right_full))); info.addAction(new AccessibilityAction(R.id.action_swap_apps, mContext.getString(R.string.accessibility_action_divider_swap_horizontal))); } else { info.addAction(new AccessibilityAction(R.id.action_move_tl_full, mContext.getString(R.string.accessibility_action_divider_top_full))); if (snapAlgorithm.isFirstSplitTargetAvailable()) { info.addAction(new AccessibilityAction(R.id.action_move_tl_70, mContext.getString(R.string.accessibility_action_divider_top_70))); } if (snapAlgorithm.showMiddleSplitTargetForAccessibility()) { // Only show the middle target if there are more than 1 split target info.addAction(new AccessibilityAction(R.id.action_move_tl_50, mContext.getString(R.string.accessibility_action_divider_top_50))); } if (snapAlgorithm.isLastSplitTargetAvailable()) { info.addAction(new AccessibilityAction(R.id.action_move_tl_30, mContext.getString(R.string.accessibility_action_divider_top_30))); } info.addAction(new AccessibilityAction(R.id.action_move_rb_full, mContext.getString(R.string.accessibility_action_divider_bottom_full))); info.addAction(new AccessibilityAction(R.id.action_swap_apps, mContext.getString(R.string.accessibility_action_divider_swap_vertical))); } Loading @@ -202,21 +177,12 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { return true; } SnapTarget nextTarget = null; DividerSnapAlgorithm snapAlgorithm = mSplitLayout.mDividerSnapAlgorithm; if (action == R.id.action_move_tl_full) { nextTarget = snapAlgorithm.getDismissEndTarget(); } else if (action == R.id.action_move_tl_70) { nextTarget = snapAlgorithm.getLastSplitTarget(); } else if (action == R.id.action_move_tl_50) { nextTarget = snapAlgorithm.getMiddleTarget(); } else if (action == R.id.action_move_tl_30) { nextTarget = snapAlgorithm.getFirstSplitTarget(); } else if (action == R.id.action_move_rb_full) { nextTarget = snapAlgorithm.getDismissStartTarget(); } if (nextTarget != null) { mSplitLayout.snapToTarget(mSplitLayout.getDividerPosition(), nextTarget); SplitTargetProvider.SplitTarget selectedTarget = mTargets.stream() .filter(target -> target.getA11yActionId() == action) .findFirst() .orElse(null); if (selectedTarget != null) { mSplitLayout.snapToTarget(selectedTarget.getSnapPosition()); return true; } return super.performAccessibilityAction(host, action, args); Loading Loading @@ -244,7 +210,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { /** Sets up essential dependencies of the divider bar. */ public void setup(SplitLayout layout, SplitWindowManager splitWindowManager, SurfaceControlViewHost viewHost, InsetsState insetsState, DesktopState desktopState) { DesktopState desktopState, SplitTargetProvider splitTargetProvider) { mSplitLayout = layout; mSplitWindowManager = splitWindowManager; mViewHost = viewHost; Loading @@ -264,6 +230,9 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { : desktopState.canEnterDesktopMode() ? R.dimen.desktop_mode_portrait_split_divider_handle_region_height : R.dimen.split_divider_handle_region_height); mSplitTargetProvider = splitTargetProvider; mHandle.setAccessibilityDelegate(mHandleDelegate); } void onInsetsChanged(InsetsState insetsState, boolean animate) { Loading Loading @@ -312,7 +281,6 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { mInteractive = true; mHideHandle = false; setOnTouchListener(this); mHandle.setAccessibilityDelegate(mHandleDelegate); setWillNotDraw(false); mPaint.setColor(getResources().getColor(R.color.split_divider_background, null)); mPaint.setAntiAlias(true); Loading
libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SnapToTargetConverter.kt 0 → 100644 +207 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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 android.content.res.Resources import com.android.wm.shell.Flags import com.android.wm.shell.R import com.android.wm.shell.common.split.SplitTargetProvider.SplitTarget import com.android.wm.shell.shared.split.SplitScreenConstants /** * For a given snap mode, based on current feature flags and resource config values, generates list * of viable snap targets which should be accessed via the [SplitTargetProvider] interface */ class SnapToTargetConverter(val res: Resources, isMinimizedMode: Boolean) : SplitTargetProvider { private var snapMode: Int = 0 init { snapMode = if (Flags.enableFlexibleTwoAppSplit()) { DividerSnapAlgorithm.SNAP_FLEXIBLE_HYBRID } else { if (isMinimizedMode) DividerSnapAlgorithm.SNAP_MODE_MINIMIZED else res.getInteger(com.android.internal.R.integer.config_dockedStackDividerSnapMode) } } /** See [SplitScreenUtils.isLeftRightSplit] */ private fun isLeftRightSplit() : Boolean { val mAllowLeftRightSplitInPortrait = SplitScreenUtils.allowLeftRightSplitInPortrait(res) return SplitScreenUtils.isLeftRightSplit( mAllowLeftRightSplitInPortrait, res.configuration ) } /** * @param includeDismissal if true then the returned list will include the start and end * dismiss targets at the start and end of the list, respectively. * @return A [List] of [SplitTarget] for the device's current [snapMode], by ordering of left * to right based on the different snap positions */ override fun getTargets(includeDismissal: Boolean): List<SplitTarget> { val targets: MutableList<SplitTarget> = ArrayList() val areOffscreenRatiosSupported = SplitScreenUtils.allowOffscreenRatios(res) val isLeftRightSplit = isLeftRightSplit() if (includeDismissal) { targets.add( SplitTarget( SplitScreenConstants.SNAP_TO_START_AND_DISMISS, if (isLeftRightSplit) R.string.accessibility_action_divider_right_full else R.string.accessibility_action_divider_bottom_full, R.id.action_move_tl_full ) ) } when (snapMode) { DividerSnapAlgorithm.SNAP_ONLY_1_1 -> targets.add( SplitTarget( SplitScreenConstants.SNAP_TO_2_50_50, if (isLeftRightSplit) R.string.accessibility_action_divider_left_50 else R.string.accessibility_action_divider_top_50, R.id.action_move_tl_50 ) ) DividerSnapAlgorithm.SNAP_MODE_MINIMIZED -> targets.add( SplitTarget( SplitScreenConstants.SNAP_TO_MINIMIZE, if (isLeftRightSplit) R.string.accessibility_action_divider_left_full else R.string.accessibility_action_divider_top_full, R.id.action_move_tl_full ) ) DividerSnapAlgorithm.SNAP_MODE_16_9, DividerSnapAlgorithm.SNAP_FIXED_RATIO -> targets.addAll( getOnscreenTargets(isLeftRightSplit) ) DividerSnapAlgorithm.SNAP_FLEXIBLE_SPLIT -> { if (areOffscreenRatiosSupported) { targets.add( SplitTarget( SplitScreenConstants.SNAP_TO_2_10_90, if (isLeftRightSplit) R.string.accessibility_action_divider_left_10 else R.string.accessibility_action_divider_top_10, R.id.action_move_tl_90 ) ) targets.add( SplitTarget( SplitScreenConstants.SNAP_TO_2_50_50, if (isLeftRightSplit) R.string.accessibility_action_divider_left_50 else R.string.accessibility_action_divider_top_50, R.id.action_move_tl_50 ) ) targets.add( SplitTarget( SplitScreenConstants.SNAP_TO_2_90_10, if (isLeftRightSplit) R.string.accessibility_action_divider_left_90 else R.string.accessibility_action_divider_top_90, R.id.action_move_tl_10 ) ) } else { targets.addAll(getOnscreenTargets(isLeftRightSplit)) } } DividerSnapAlgorithm.SNAP_FLEXIBLE_HYBRID -> { if (areOffscreenRatiosSupported) { targets.add( SplitTarget( SplitScreenConstants.SNAP_TO_2_10_90, if (isLeftRightSplit) R.string.accessibility_action_divider_left_10 else R.string.accessibility_action_divider_top_10, R.id.action_move_tl_90 ) ) targets.addAll(getOnscreenTargets(isLeftRightSplit)) targets.add( SplitTarget( SplitScreenConstants.SNAP_TO_2_90_10, if (isLeftRightSplit) R.string.accessibility_action_divider_left_90 else R.string.accessibility_action_divider_top_90, R.id.action_move_tl_10 ) ) } else { targets.addAll(getOnscreenTargets(isLeftRightSplit)) } } else -> throw IllegalStateException("unrecognized snap mode") } if (includeDismissal) { targets.add( SplitTarget( SplitScreenConstants.SNAP_TO_END_AND_DISMISS, if (isLeftRightSplit) R.string.accessibility_action_divider_left_full else R.string.accessibility_action_divider_top_full, R.id.action_move_rb_full ) ) } return targets } /** * Current snap mode of the device. You really probably shouldn't be using this. Exposed only * for the few use cases that actually need it. */ override fun getSnapMode(): Int { return snapMode } /** * @return Default, 3 non-flex split targets. 30/50/70, in that specific order. */ private fun getOnscreenTargets(isLeftRightSplit: Boolean): List<SplitTarget> { val targets: MutableList<SplitTarget> = java.util.ArrayList(3) targets.add( SplitTarget( SplitScreenConstants.SNAP_TO_2_33_66, if (isLeftRightSplit) R.string.accessibility_action_divider_left_30 else R.string.accessibility_action_divider_top_30, R.id.action_move_tl_30 ) ) targets.add( SplitTarget( SplitScreenConstants.SNAP_TO_2_50_50, if (isLeftRightSplit) R.string.accessibility_action_divider_left_50 else R.string.accessibility_action_divider_top_50, R.id.action_move_tl_50 ) ) targets.add( SplitTarget( SplitScreenConstants.SNAP_TO_2_66_33, if (isLeftRightSplit) R.string.accessibility_action_divider_left_70 else R.string.accessibility_action_divider_top_70, R.id.action_move_tl_70 ) ) return targets } } No newline at end of file