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

Commit ba53344a authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Implement tooltip for split divider drag" into main

parents 28bb0c9d a02f98ec
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -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"
+15 −1
Original line number Diff line number Diff line
@@ -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";
@@ -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
+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
    }
}
+54 −3
Original line number Diff line number Diff line
@@ -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;
@@ -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();
@@ -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;
@@ -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;

    /**
@@ -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
@@ -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());
@@ -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();
                                }
                            }
                        });
                    }
                }
@@ -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 */);
@@ -470,6 +519,8 @@ public class DividerView extends FrameLayout implements View.OnTouchListener {
        mDistanceGestureContext = null;
        mViewMotionValue = null;
        mLastHoveredOverSnapPosition = null;
        mDragStartingSnapPosition = null;
        hideTooltip();
    }

    private void setTouching() {