Loading libs/WindowManager/Shell/res/layout/split_divider.xml +8 −0 Original line number Diff line number Diff line Loading @@ -24,6 +24,14 @@ android:layout_width="match_parent" android:layout_height="match_parent"> <com.android.wm.shell.common.split.DividerTooltip android:id="@+id/docked_divider_tooltip" android:layout_height="wrap_content" android:layout_width="wrap_content" android:layout_gravity="center" android:gravity="center" android:visibility="gone" /> <com.android.wm.shell.common.split.DividerHandleView android:id="@+id/docked_divider_handle" android:layout_height="match_parent" Loading libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java +15 −1 Original line number Diff line number Diff line Loading @@ -199,7 +199,7 @@ public class SplitScreenConstants { }) public @interface SplitScreenState {} /** Converts a {@link SplitScreenState} to a human-readable string. */ /** Converts a {@link SplitScreenState} to a human-readable string, for debug use. */ public static String stateToString(@SplitScreenState int state) { return switch (state) { case NOT_IN_SPLIT -> "NOT_IN_SPLIT"; Loading @@ -217,6 +217,20 @@ public class SplitScreenConstants { }; } /** Converts a {@link SnapPosition} to a string, for UI use. */ public static String snapPositionToUIString(@SnapPosition int snapPosition) { return switch (snapPosition) { case SNAP_TO_START_AND_DISMISS -> "\u2715"; case SNAP_TO_END_AND_DISMISS -> "\u2715"; case SNAP_TO_2_33_66 -> "30:70"; case SNAP_TO_2_50_50 -> "50:50"; case SNAP_TO_2_66_33 -> "70:30"; case SNAP_TO_2_90_10 -> "90:10"; case SNAP_TO_2_10_90 -> "10:90"; default -> "Split"; }; } /** * Convenience method to convert between the IntDef's to avoid some errors * @return {@code -1} if splitScreenState does not have a valid/corresponding Loading libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerTooltip.kt 0 → 100644 +105 −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.Context import android.graphics.Color import android.graphics.Rect import android.util.AttributeSet import android.util.Log import android.widget.FrameLayout import androidx.appcompat.widget.AppCompatTextView import com.android.wm.shell.R import com.android.wm.shell.common.pip.PipUtils.dpToPx import com.android.wm.shell.shared.TypefaceUtils import com.android.wm.shell.shared.TypefaceUtils.Companion.setTypeface import androidx.core.graphics.toColorInt /** * A small tooltip bubble that educates the user about split screen breakpoints. */ class DividerTooltip(context: Context, attrs: AttributeSet?) : AppCompatTextView(context, attrs) { /** The length of the split divider handle, along its long edge, in px. */ private val mDividerHandleLengthPx = resources.getDimensionPixelSize(R.dimen.split_divider_handle_width) private var mIsLeftRightSplit = false init { alpha = 0f setTextColor(TOOLTIP_TEXT_COLOR) setBackgroundColor(TOOLTIP_BG_COLOR) setTypeface(this, TOOLTIP_FONT) } /** * Called in DividerView.setup() to determine orientation. Expected to always be called on * divider initialization. */ fun setIsLeftRightSplit(isLeftRightSplit: Boolean) { mIsLeftRightSplit = isLeftRightSplit } /** Converts dp to px. */ private fun dpToPx(dpValue: Int): Int { return dpToPx(dpValue.toFloat(), mContext.resources.displayMetrics) } /** * Resizes the tooltip to fit the current text, and adjusts margins to put it in the correct * place on the screen. */ override fun onTextChanged( text: CharSequence, start: Int, lengthBefore: Int, lengthAfter: Int) { if (layoutParams == null) { return } // Get a Rect representing the raw size of the current text string. val textBounds = Rect() paint.getTextBounds(text.toString(), 0, text.length, textBounds) val lp = layoutParams as FrameLayout.LayoutParams lp.height = textBounds.height() + (dpToPx(TOOLTIP_PADDING_DP) * 2) lp.width = textBounds.width() + (dpToPx(TOOLTIP_PADDING_DP) * 2) if (mIsLeftRightSplit) { lp.bottomMargin = ((mDividerHandleLengthPx / 2) + (lp.height / 2) + dpToPx(TOOLTIP_DISTANCE_FROM_HANDLE_DP)) lp.rightMargin = 0 } else { lp.bottomMargin = 0 lp.rightMargin = ((mDividerHandleLengthPx / 2) + (lp.width / 2) + dpToPx(TOOLTIP_DISTANCE_FROM_HANDLE_DP)) } layoutParams = lp } companion object { private val TOOLTIP_TEXT_COLOR = "#4A3F08".toColorInt() private val TOOLTIP_BG_COLOR = "#F5E29D".toColorInt() private val TOOLTIP_FONT = TypefaceUtils.FontFamily.GSF_BODY_MEDIUM_EMPHASIZED /** The padding between the tooltip's text and its outer border, on all four sides, in dp. */ private const val TOOLTIP_PADDING_DP = 12 /** The distance between the tooltip's border and the (full-sized) divider handle, in dp. */ private const val TOOLTIP_DISTANCE_FROM_HANDLE_DP = 16 } } libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java +54 −3 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ import static android.view.PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW; import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY; import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CURSOR_HOVER_STATES_ENABLED; import static com.android.wm.shell.shared.split.SplitScreenConstants.snapPositionToUIString; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; Loading Loading @@ -75,6 +76,7 @@ import java.util.Objects; public class DividerView extends FrameLayout implements View.OnTouchListener { public static final long TOUCH_ANIMATION_DURATION = 150; public static final long TOUCH_RELEASE_ANIMATION_DURATION = 200; private static final boolean SHOW_DRAG_TOOLTIP = true; private final Paint mPaint = new Paint(); private final Rect mBackgroundRect = new Rect(); Loading @@ -85,6 +87,8 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { private SurfaceControlViewHost mViewHost; private DividerHandleView mHandle; private DividerRoundedCorner mCorners; /** A tooltip view that appears to educate users about split screen breakpoints. */ private DividerTooltip mTooltip; private int mTouchElevation; private VelocityTracker mVelocityTracker; Loading @@ -101,6 +105,11 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { // Calculation classes for "magnetic snap" user-controlled movement private DistanceGestureContext mDistanceGestureContext; private ViewMotionValue mViewMotionValue; /** * The @SnapPosition where the user started dragging from. Used for mid-drag calculations, null * otherwise. */ @Nullable private Integer mDragStartingSnapPosition; @Nullable private Integer mLastHoveredOverSnapPosition; /** Loading Loading @@ -247,6 +256,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { final boolean isLeftRightSplit = mSplitLayout.isLeftRightSplit(); mHandle.setIsLeftRightSplit(isLeftRightSplit); mCorners.setIsLeftRightSplit(isLeftRightSplit); mTooltip.setIsLeftRightSplit(isLeftRightSplit); mHandleRegionWidth = getResources().getDimensionPixelSize(isLeftRightSplit ? R.dimen.split_divider_handle_region_height Loading Loading @@ -297,6 +307,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { mDividerBar = findViewById(R.id.divider_bar); mHandle = findViewById(R.id.docked_divider_handle); mCorners = findViewById(R.id.docked_divider_rounded_corner); mTooltip = findViewById(R.id.docked_divider_tooltip); mTouchElevation = getResources().getDimensionPixelSize( R.dimen.docked_stack_divider_lift_elevation); mDoubleTapDetector = new GestureDetector(getContext(), new DoubleTapListener()); Loading Loading @@ -382,19 +393,40 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { mSplitLayout.mDividerSnapAlgorithm.getMotionSpec(), "dividerView::pos" /* label */); mLastHoveredOverSnapPosition = mSplitLayout.calculateCurrentSnapPosition(); mDragStartingSnapPosition = mSplitLayout.calculateCurrentSnapPosition(); mViewMotionValue.addUpdateCallback(viewMotionValue -> { int snappedPosition = (int) viewMotionValue.getOutput(); // Whenever MotionValue updates (from user moving the divider): // - Place divider in its new position placeDivider((int) viewMotionValue.getOutput()); placeDivider(snappedPosition); // - Play a haptic if entering a magnetic zone Integer currentlyHoveredOverSnapZone = viewMotionValue.get( MagneticDividerUtils.getSNAP_POSITION_KEY()); if (currentlyHoveredOverSnapZone != null && !Objects.equals( currentlyHoveredOverSnapZone, mLastHoveredOverSnapPosition)) { boolean changedSnapPosition = !Objects.equals( currentlyHoveredOverSnapZone, mLastHoveredOverSnapPosition); if (currentlyHoveredOverSnapZone != null && changedSnapPosition) { playHapticClick(); } // - Update the last-hovered-over snap zone mLastHoveredOverSnapPosition = currentlyHoveredOverSnapZone; // - Update tooltip state if needed if (SHOW_DRAG_TOOLTIP && changedSnapPosition) { // - Update internal state for closest snap position (i.e. where the // user will end up if drag is released) final float velocity = isLeftRightSplit ? mVelocityTracker.getXVelocity() : mVelocityTracker.getYVelocity(); int closestSnapPosition = mSplitLayout .findSnapTarget(snappedPosition, velocity, false /* hardDismiss */) .snapPosition; if (closestSnapPosition != mDragStartingSnapPosition) { showTooltip(snapPositionToUIString(closestSnapPosition)); } else { hideTooltip(); } } }); } } Loading Loading @@ -443,6 +475,23 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { mSplitLayout.getHapticPlayer().playToken(MSDLToken.SWIPE_THRESHOLD_INDICATOR, null); } private void showTooltip(String tooltipText) { mTooltip.setText(tooltipText); if (mTooltip.getVisibility() == VISIBLE) { return; } mTooltip.setVisibility(VISIBLE); mTooltip.setAlpha(1f); } private void hideTooltip() { if (mTooltip.getVisibility() == GONE) { return; } mTooltip.setAlpha(0f); mTooltip.setVisibility(GONE); } /** Updates the position of the divider. */ private void placeDivider(int position) { mSplitLayout.updateDividerBounds(position, true /* shouldUseParallaxEffect */); Loading Loading @@ -470,6 +519,8 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { mDistanceGestureContext = null; mViewMotionValue = null; mLastHoveredOverSnapPosition = null; mDragStartingSnapPosition = null; hideTooltip(); } private void setTouching() { Loading Loading
libs/WindowManager/Shell/res/layout/split_divider.xml +8 −0 Original line number Diff line number Diff line Loading @@ -24,6 +24,14 @@ android:layout_width="match_parent" android:layout_height="match_parent"> <com.android.wm.shell.common.split.DividerTooltip android:id="@+id/docked_divider_tooltip" android:layout_height="wrap_content" android:layout_width="wrap_content" android:layout_gravity="center" android:gravity="center" android:visibility="gone" /> <com.android.wm.shell.common.split.DividerHandleView android:id="@+id/docked_divider_handle" android:layout_height="match_parent" Loading
libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java +15 −1 Original line number Diff line number Diff line Loading @@ -199,7 +199,7 @@ public class SplitScreenConstants { }) public @interface SplitScreenState {} /** Converts a {@link SplitScreenState} to a human-readable string. */ /** Converts a {@link SplitScreenState} to a human-readable string, for debug use. */ public static String stateToString(@SplitScreenState int state) { return switch (state) { case NOT_IN_SPLIT -> "NOT_IN_SPLIT"; Loading @@ -217,6 +217,20 @@ public class SplitScreenConstants { }; } /** Converts a {@link SnapPosition} to a string, for UI use. */ public static String snapPositionToUIString(@SnapPosition int snapPosition) { return switch (snapPosition) { case SNAP_TO_START_AND_DISMISS -> "\u2715"; case SNAP_TO_END_AND_DISMISS -> "\u2715"; case SNAP_TO_2_33_66 -> "30:70"; case SNAP_TO_2_50_50 -> "50:50"; case SNAP_TO_2_66_33 -> "70:30"; case SNAP_TO_2_90_10 -> "90:10"; case SNAP_TO_2_10_90 -> "10:90"; default -> "Split"; }; } /** * Convenience method to convert between the IntDef's to avoid some errors * @return {@code -1} if splitScreenState does not have a valid/corresponding Loading
libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerTooltip.kt 0 → 100644 +105 −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.Context import android.graphics.Color import android.graphics.Rect import android.util.AttributeSet import android.util.Log import android.widget.FrameLayout import androidx.appcompat.widget.AppCompatTextView import com.android.wm.shell.R import com.android.wm.shell.common.pip.PipUtils.dpToPx import com.android.wm.shell.shared.TypefaceUtils import com.android.wm.shell.shared.TypefaceUtils.Companion.setTypeface import androidx.core.graphics.toColorInt /** * A small tooltip bubble that educates the user about split screen breakpoints. */ class DividerTooltip(context: Context, attrs: AttributeSet?) : AppCompatTextView(context, attrs) { /** The length of the split divider handle, along its long edge, in px. */ private val mDividerHandleLengthPx = resources.getDimensionPixelSize(R.dimen.split_divider_handle_width) private var mIsLeftRightSplit = false init { alpha = 0f setTextColor(TOOLTIP_TEXT_COLOR) setBackgroundColor(TOOLTIP_BG_COLOR) setTypeface(this, TOOLTIP_FONT) } /** * Called in DividerView.setup() to determine orientation. Expected to always be called on * divider initialization. */ fun setIsLeftRightSplit(isLeftRightSplit: Boolean) { mIsLeftRightSplit = isLeftRightSplit } /** Converts dp to px. */ private fun dpToPx(dpValue: Int): Int { return dpToPx(dpValue.toFloat(), mContext.resources.displayMetrics) } /** * Resizes the tooltip to fit the current text, and adjusts margins to put it in the correct * place on the screen. */ override fun onTextChanged( text: CharSequence, start: Int, lengthBefore: Int, lengthAfter: Int) { if (layoutParams == null) { return } // Get a Rect representing the raw size of the current text string. val textBounds = Rect() paint.getTextBounds(text.toString(), 0, text.length, textBounds) val lp = layoutParams as FrameLayout.LayoutParams lp.height = textBounds.height() + (dpToPx(TOOLTIP_PADDING_DP) * 2) lp.width = textBounds.width() + (dpToPx(TOOLTIP_PADDING_DP) * 2) if (mIsLeftRightSplit) { lp.bottomMargin = ((mDividerHandleLengthPx / 2) + (lp.height / 2) + dpToPx(TOOLTIP_DISTANCE_FROM_HANDLE_DP)) lp.rightMargin = 0 } else { lp.bottomMargin = 0 lp.rightMargin = ((mDividerHandleLengthPx / 2) + (lp.width / 2) + dpToPx(TOOLTIP_DISTANCE_FROM_HANDLE_DP)) } layoutParams = lp } companion object { private val TOOLTIP_TEXT_COLOR = "#4A3F08".toColorInt() private val TOOLTIP_BG_COLOR = "#F5E29D".toColorInt() private val TOOLTIP_FONT = TypefaceUtils.FontFamily.GSF_BODY_MEDIUM_EMPHASIZED /** The padding between the tooltip's text and its outer border, on all four sides, in dp. */ private const val TOOLTIP_PADDING_DP = 12 /** The distance between the tooltip's border and the (full-sized) divider handle, in dp. */ private const val TOOLTIP_DISTANCE_FROM_HANDLE_DP = 16 } }
libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java +54 −3 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ import static android.view.PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW; import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY; import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CURSOR_HOVER_STATES_ENABLED; import static com.android.wm.shell.shared.split.SplitScreenConstants.snapPositionToUIString; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; Loading Loading @@ -75,6 +76,7 @@ import java.util.Objects; public class DividerView extends FrameLayout implements View.OnTouchListener { public static final long TOUCH_ANIMATION_DURATION = 150; public static final long TOUCH_RELEASE_ANIMATION_DURATION = 200; private static final boolean SHOW_DRAG_TOOLTIP = true; private final Paint mPaint = new Paint(); private final Rect mBackgroundRect = new Rect(); Loading @@ -85,6 +87,8 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { private SurfaceControlViewHost mViewHost; private DividerHandleView mHandle; private DividerRoundedCorner mCorners; /** A tooltip view that appears to educate users about split screen breakpoints. */ private DividerTooltip mTooltip; private int mTouchElevation; private VelocityTracker mVelocityTracker; Loading @@ -101,6 +105,11 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { // Calculation classes for "magnetic snap" user-controlled movement private DistanceGestureContext mDistanceGestureContext; private ViewMotionValue mViewMotionValue; /** * The @SnapPosition where the user started dragging from. Used for mid-drag calculations, null * otherwise. */ @Nullable private Integer mDragStartingSnapPosition; @Nullable private Integer mLastHoveredOverSnapPosition; /** Loading Loading @@ -247,6 +256,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { final boolean isLeftRightSplit = mSplitLayout.isLeftRightSplit(); mHandle.setIsLeftRightSplit(isLeftRightSplit); mCorners.setIsLeftRightSplit(isLeftRightSplit); mTooltip.setIsLeftRightSplit(isLeftRightSplit); mHandleRegionWidth = getResources().getDimensionPixelSize(isLeftRightSplit ? R.dimen.split_divider_handle_region_height Loading Loading @@ -297,6 +307,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { mDividerBar = findViewById(R.id.divider_bar); mHandle = findViewById(R.id.docked_divider_handle); mCorners = findViewById(R.id.docked_divider_rounded_corner); mTooltip = findViewById(R.id.docked_divider_tooltip); mTouchElevation = getResources().getDimensionPixelSize( R.dimen.docked_stack_divider_lift_elevation); mDoubleTapDetector = new GestureDetector(getContext(), new DoubleTapListener()); Loading Loading @@ -382,19 +393,40 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { mSplitLayout.mDividerSnapAlgorithm.getMotionSpec(), "dividerView::pos" /* label */); mLastHoveredOverSnapPosition = mSplitLayout.calculateCurrentSnapPosition(); mDragStartingSnapPosition = mSplitLayout.calculateCurrentSnapPosition(); mViewMotionValue.addUpdateCallback(viewMotionValue -> { int snappedPosition = (int) viewMotionValue.getOutput(); // Whenever MotionValue updates (from user moving the divider): // - Place divider in its new position placeDivider((int) viewMotionValue.getOutput()); placeDivider(snappedPosition); // - Play a haptic if entering a magnetic zone Integer currentlyHoveredOverSnapZone = viewMotionValue.get( MagneticDividerUtils.getSNAP_POSITION_KEY()); if (currentlyHoveredOverSnapZone != null && !Objects.equals( currentlyHoveredOverSnapZone, mLastHoveredOverSnapPosition)) { boolean changedSnapPosition = !Objects.equals( currentlyHoveredOverSnapZone, mLastHoveredOverSnapPosition); if (currentlyHoveredOverSnapZone != null && changedSnapPosition) { playHapticClick(); } // - Update the last-hovered-over snap zone mLastHoveredOverSnapPosition = currentlyHoveredOverSnapZone; // - Update tooltip state if needed if (SHOW_DRAG_TOOLTIP && changedSnapPosition) { // - Update internal state for closest snap position (i.e. where the // user will end up if drag is released) final float velocity = isLeftRightSplit ? mVelocityTracker.getXVelocity() : mVelocityTracker.getYVelocity(); int closestSnapPosition = mSplitLayout .findSnapTarget(snappedPosition, velocity, false /* hardDismiss */) .snapPosition; if (closestSnapPosition != mDragStartingSnapPosition) { showTooltip(snapPositionToUIString(closestSnapPosition)); } else { hideTooltip(); } } }); } } Loading Loading @@ -443,6 +475,23 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { mSplitLayout.getHapticPlayer().playToken(MSDLToken.SWIPE_THRESHOLD_INDICATOR, null); } private void showTooltip(String tooltipText) { mTooltip.setText(tooltipText); if (mTooltip.getVisibility() == VISIBLE) { return; } mTooltip.setVisibility(VISIBLE); mTooltip.setAlpha(1f); } private void hideTooltip() { if (mTooltip.getVisibility() == GONE) { return; } mTooltip.setAlpha(0f); mTooltip.setVisibility(GONE); } /** Updates the position of the divider. */ private void placeDivider(int position) { mSplitLayout.updateDividerBounds(position, true /* shouldUseParallaxEffect */); Loading Loading @@ -470,6 +519,8 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { mDistanceGestureContext = null; mViewMotionValue = null; mLastHoveredOverSnapPosition = null; mDragStartingSnapPosition = null; hideTooltip(); } private void setTouching() { Loading