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

Commit 3be39aa0 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Update ArrowPopup to accommodate more rounded dialogs." into sc-dev

parents e9fe7f27 452005a1
Loading
Loading
Loading
Loading
+4 −6
Original line number Diff line number Diff line
@@ -183,13 +183,11 @@
    <dimen name="popup_padding_start">10dp</dimen>
    <dimen name="popup_padding_end">16dp</dimen>
    <dimen name="popup_vertical_padding">4dp</dimen>
    <dimen name="popup_arrow_width">10dp</dimen>
    <dimen name="popup_arrow_height">8dp</dimen>
    <dimen name="popup_arrow_vertical_offset">-2dp</dimen>
    <dimen name="popup_arrow_width">12dp</dimen>
    <dimen name="popup_arrow_height">10dp</dimen>
    <dimen name="popup_arrow_vertical_offset">-1dp</dimen>
    <!-- popup_padding_start + deep_shortcut_icon_size / 2 -->
    <dimen name="popup_arrow_horizontal_center_start">28dp</dimen>
    <!-- popup_padding_end + deep_shortcut_drag_handle_size / 2 -->
    <dimen name="popup_arrow_horizontal_center_end">24dp</dimen>
    <dimen name="popup_arrow_horizontal_center_offset">28dp</dimen>
    <dimen name="popup_arrow_corner_radius">2dp</dimen>
    <!-- popup_padding_start + icon_size + 10dp -->
    <dimen name="deep_shortcuts_text_padding_start">56dp</dimen>
+40 −73
Original line number Diff line number Diff line
@@ -26,11 +26,8 @@ import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.CornerPathEffect;
import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.ShapeDrawable;
import android.util.AttributeSet;
import android.util.Pair;
import android.view.Gravity;
@@ -51,7 +48,6 @@ import com.android.launcher3.Utilities;
import com.android.launcher3.anim.RevealOutlineAnimation;
import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
import com.android.launcher3.dragndrop.DragLayer;
import com.android.launcher3.graphics.TriangleShape;
import com.android.launcher3.util.Themes;
import com.android.launcher3.views.BaseDragLayer;

@@ -72,7 +68,11 @@ public abstract class ArrowPopup<T extends BaseDraggingActivity> extends Abstrac
    protected final T mLauncher;
    protected final boolean mIsRtl;

    private final int mArrowOffset;
    private final int mArrowOffsetVertical;
    private final int mArrowOffsetHorizontal;
    private final int mArrowWidth;
    private final int mArrowHeight;
    private final int mArrowPointRadius;
    private final View mArrow;

    protected boolean mIsLeftAligned;
@@ -103,11 +103,14 @@ public abstract class ArrowPopup<T extends BaseDraggingActivity> extends Abstrac

        // Initialize arrow view
        final Resources resources = getResources();
        final int arrowWidth = resources.getDimensionPixelSize(R.dimen.popup_arrow_width);
        final int arrowHeight = resources.getDimensionPixelSize(R.dimen.popup_arrow_height);
        mArrowWidth = resources.getDimensionPixelSize(R.dimen.popup_arrow_width);
        mArrowHeight = resources.getDimensionPixelSize(R.dimen.popup_arrow_height);
        mArrow = new View(context);
        mArrow.setLayoutParams(new DragLayer.LayoutParams(arrowWidth, arrowHeight));
        mArrowOffset = resources.getDimensionPixelSize(R.dimen.popup_arrow_vertical_offset);
        mArrow.setLayoutParams(new DragLayer.LayoutParams(mArrowWidth, mArrowHeight));
        mArrowOffsetVertical = resources.getDimensionPixelSize(R.dimen.popup_arrow_vertical_offset);
        mArrowOffsetHorizontal = resources.getDimensionPixelSize(
                R.dimen.popup_arrow_horizontal_center_offset) - (mArrowWidth / 2);
        mArrowPointRadius = resources.getDimensionPixelSize(R.dimen.popup_arrow_corner_radius);
    }

    public ArrowPopup(Context context, AttributeSet attrs) {
@@ -200,48 +203,33 @@ public abstract class ArrowPopup<T extends BaseDraggingActivity> extends Abstrac
        orientAboutObject();
    }

    private void addArrow() {
        final Resources res = getResources();
        final int arrowCenterOffset = res.getDimensionPixelSize(isAlignedWithStart()
                ? R.dimen.popup_arrow_horizontal_center_start
                : R.dimen.popup_arrow_horizontal_center_end);
        final int halfArrowWidth = res.getDimensionPixelSize(R.dimen.popup_arrow_width) / 2;
        getPopupContainer().addView(mArrow);
        DragLayer.LayoutParams arrowLp = (DragLayer.LayoutParams) mArrow.getLayoutParams();
    private int getArrowLeft() {
        if (mIsLeftAligned) {
            mArrow.setX(getX() + arrowCenterOffset - halfArrowWidth);
        } else {
            mArrow.setX(getX() + getMeasuredWidth() - arrowCenterOffset - halfArrowWidth);
            return mArrowOffsetHorizontal;
        }
        return getMeasuredWidth() - mArrowOffsetHorizontal - mArrowWidth;
    }

    private void addArrow() {
        getPopupContainer().addView(mArrow);
        mArrow.setX(getX() + getArrowLeft());

        if (Gravity.isVertical(mGravity)) {
            // This is only true if there wasn't room for the container next to the icon,
            // so we centered it instead. In that case we don't want to showDefaultOptions the arrow.
            mArrow.setVisibility(INVISIBLE);
        } else {
            ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
                    arrowLp.width, arrowLp.height, !mIsAboveIcon));
            Paint arrowPaint = arrowDrawable.getPaint();
            arrowPaint.setColor(Themes.getAttrColor(getContext(), R.attr.popupColorPrimary));
            // The corner path effect won't be reflected in the shadow, but shouldn't be noticeable.
            int radius = getResources().getDimensionPixelSize(R.dimen.popup_arrow_corner_radius);
            arrowPaint.setPathEffect(new CornerPathEffect(radius));
            mArrow.setBackground(arrowDrawable);
            // Clip off the part of the arrow that is underneath the popup.
            if (mIsAboveIcon) {
                mArrow.setClipBounds(new Rect(0, -mArrowOffset, arrowLp.width, arrowLp.height));
            } else {
                mArrow.setClipBounds(new Rect(0, 0, arrowLp.width, arrowLp.height + mArrowOffset));
            }
            mArrow.setBackground(new RoundedArrowDrawable(
                    mArrowWidth, mArrowHeight, mArrowPointRadius,
                    mOutlineRadius, getMeasuredWidth(), getMeasuredHeight(),
                    mArrowOffsetHorizontal, -mArrowOffsetVertical,
                    !mIsAboveIcon, mIsLeftAligned,
                    Themes.getAttrColor(getContext(), R.attr.popupColorPrimary)));
            mArrow.setElevation(getElevation());
        }

        mArrow.setPivotX(arrowLp.width / 2);
        mArrow.setPivotY(mIsAboveIcon ? arrowLp.height : 0);
    }

    protected boolean isAlignedWithStart() {
        return mIsLeftAligned && !mIsRtl || !mIsLeftAligned && mIsRtl;
        mArrow.setPivotX(mArrowWidth / 2.0f);
        mArrow.setPivotY(mIsAboveIcon ? mArrowHeight : 0);
    }

    /**
@@ -274,8 +262,9 @@ public abstract class ArrowPopup<T extends BaseDraggingActivity> extends Abstrac
     */
    private void orientAboutObject(boolean allowAlignLeft, boolean allowAlignRight) {
        measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);

        int width = getMeasuredWidth();
        int extraVerticalSpace = mArrow.getLayoutParams().height + mArrowOffset
        int extraVerticalSpace = mArrowHeight + mArrowOffsetVertical
                + getResources().getDimensionPixelSize(R.dimen.popup_vertical_padding);
        int height = getMeasuredHeight() + extraVerticalSpace;

@@ -291,22 +280,7 @@ public abstract class ArrowPopup<T extends BaseDraggingActivity> extends Abstrac

        // Offset x so that the arrow and shortcut icons are center-aligned with the original icon.
        int iconWidth = mTempRect.width();
        Resources resources = getResources();
        int xOffset;
        if (isAlignedWithStart()) {
            // Aligning with the shortcut icon.
            int shortcutIconWidth = resources.getDimensionPixelSize(R.dimen.deep_shortcut_icon_size);
            int shortcutPaddingStart = resources.getDimensionPixelSize(
                    R.dimen.popup_padding_start);
            xOffset = iconWidth / 2 - shortcutIconWidth / 2 - shortcutPaddingStart;
        } else {
            // Aligning with the drag handle.
            int shortcutDragHandleWidth = resources.getDimensionPixelSize(
                    R.dimen.deep_shortcut_drag_handle_size);
            int shortcutPaddingEnd = resources.getDimensionPixelSize(
                    R.dimen.popup_padding_end);
            xOffset = iconWidth / 2 - shortcutDragHandleWidth / 2 - shortcutPaddingEnd;
        }
        int xOffset = iconWidth / 2 - mArrowOffsetHorizontal - mArrowWidth / 2;
        x += mIsLeftAligned ? xOffset : -xOffset;

        // Check whether we can still align as we originally wanted, now that we've calculated x.
@@ -375,12 +349,14 @@ public abstract class ArrowPopup<T extends BaseDraggingActivity> extends Abstrac
        FrameLayout.LayoutParams arrowLp = (FrameLayout.LayoutParams) mArrow.getLayoutParams();
        if (mIsAboveIcon) {
            arrowLp.gravity = lp.gravity = Gravity.BOTTOM;
            lp.bottomMargin = getPopupContainer().getHeight() - y - getMeasuredHeight() - insets.top;
            arrowLp.bottomMargin = lp.bottomMargin - arrowLp.height - mArrowOffset - insets.bottom;
            lp.bottomMargin =
                    getPopupContainer().getHeight() - y - getMeasuredHeight() - insets.top;
            arrowLp.bottomMargin =
                    lp.bottomMargin - arrowLp.height - mArrowOffsetVertical - insets.bottom;
        } else {
            arrowLp.gravity = lp.gravity = Gravity.TOP;
            lp.topMargin = y + insets.top;
            arrowLp.topMargin = lp.topMargin - insets.top - arrowLp.height - mArrowOffset;
            arrowLp.topMargin = lp.topMargin - insets.top - arrowLp.height - mArrowOffsetVertical;
        }
    }

@@ -529,22 +505,13 @@ public abstract class ArrowPopup<T extends BaseDraggingActivity> extends Abstrac
    protected void onCreateCloseAnimation(AnimatorSet anim) { }

    private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() {
        Resources res = getResources();
        int arrowCenterX = res.getDimensionPixelSize(mIsLeftAligned ^ mIsRtl ?
                R.dimen.popup_arrow_horizontal_center_start:
                R.dimen.popup_arrow_horizontal_center_end);
        int halfArrowWidth = res.getDimensionPixelSize(R.dimen.popup_arrow_width) / 2;
        float arrowCornerRadius = res.getDimension(R.dimen.popup_arrow_corner_radius);
        if (!mIsLeftAligned) {
            arrowCenterX = getMeasuredWidth() - arrowCenterX;
        }
        int arrowLeft = getArrowLeft();
        int arrowCenterY = mIsAboveIcon ? getMeasuredHeight() : 0;

        mStartRect.set(arrowCenterX - halfArrowWidth, arrowCenterY, arrowCenterX + halfArrowWidth,
                arrowCenterY);
        mStartRect.set(arrowLeft, arrowCenterY, arrowLeft + mArrowWidth, arrowCenterY);

        return new RoundedRectRevealOutlineProvider
                (arrowCornerRadius, mOutlineRadius, mStartRect, mEndRect);
        return new RoundedRectRevealOutlineProvider(
                mArrowPointRadius, mOutlineRadius, mStartRect, mEndRect);
    }

    /**
+161 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.launcher3.popup;

import static java.lang.Math.atan;
import static java.lang.Math.cos;
import static java.lang.Math.sin;
import static java.lang.Math.toDegrees;

import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Matrix;
import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.drawable.Drawable;

/**
 * A drawable for a very specific purpose. Used for the caret arrow on a rounded rectangle popup
 * bubble.
 * Draws a triangle with one rounded tip, the opposite edge is clipped by the body of the popup
 * so there is no overlap when drawing them together.
 */
public class RoundedArrowDrawable extends Drawable {

    private final Path mPath;
    private final Paint mPaint;

    /**
     * Default constructor.
     *
     * @param width of the arrow.
     * @param height of the arrow.
     * @param radius of the tip of the arrow.
     * @param popupRadius of the rect to clip this by.
     * @param popupWidth of the rect to clip this by.
     * @param popupHeight of the rect to clip this by.
     * @param arrowOffsetX from the edge of the popup to the arrow.
     * @param arrowOffsetY how much the arrow will overlap the popup.
     * @param isPointingUp or not.
     * @param leftAligned or false for right aligned.
     * @param color to draw the triangle.
     */
    public RoundedArrowDrawable(float width, float height, float radius, float popupRadius,
            float popupWidth, float popupHeight,
            float arrowOffsetX, float arrowOffsetY, boolean isPointingUp, boolean leftAligned,
            int color) {
        mPath = new Path();
        mPaint = new Paint();
        mPaint.setColor(color);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setAntiAlias(true);

        // Make the drawable with the triangle pointing down and positioned on the left..
        addDownPointingRoundedTriangleToPath(width, height, radius, mPath);
        clipPopupBodyFromPath(popupRadius, popupWidth, popupHeight, arrowOffsetX, arrowOffsetY,
                mPath);

        // ... then flip it horizontal or vertical based on where it will be used.
        Matrix pathTransform = new Matrix();
        pathTransform.setScale(
                leftAligned ? 1 : -1, isPointingUp ? -1 : 1, width * 0.5f, height * 0.5f);
        mPath.transform(pathTransform);
    }

    @Override
    public void draw(Canvas canvas) {
        canvas.drawPath(mPath, mPaint);
    }

    @Override
    public void getOutline(Outline outline) {
        outline.setPath(mPath);
    }

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }

    @Override
    public void setAlpha(int i) {
        mPaint.setAlpha(i);
    }

    @Override
    public void setColorFilter(ColorFilter colorFilter) {
        mPaint.setColorFilter(colorFilter);
    }

    private static void addDownPointingRoundedTriangleToPath(float width, float height,
            float radius, Path path) {
        // Calculated for the arrow pointing down, will be flipped later if needed.

        // Theta is half of the angle inside the triangle tip
        float tanTheta = width / (2.0f * height);
        float theta = (float) atan(tanTheta);

        // Some trigonometry to find the center of the circle for the rounded tip
        float roundedPointCenterY = (float) (height - (radius / sin(theta)));

        // p is the distance along the triangle side to the intersection with the point circle
        float p = radius / tanTheta;
        float lineRoundPointIntersectFromCenter = (float) (p * sin(theta));
        float lineRoundPointIntersectFromTop = (float) (height - (p * cos(theta)));

        float centerX = width / 2.0f;
        float thetaDeg = (float) toDegrees(theta);

        path.reset();
        path.moveTo(0, 0);
        // Draw the top
        path.lineTo(width, 0);
        // Draw the right side up to the circle intersection
        path.lineTo(
                centerX + lineRoundPointIntersectFromCenter,
                lineRoundPointIntersectFromTop);
        // Draw the rounded point
        path.arcTo(
                centerX - radius,
                roundedPointCenterY - radius,
                centerX + radius,
                roundedPointCenterY + radius,
                thetaDeg,
                180 - (2 * thetaDeg),
                false);
        // Draw the left edge to close
        path.lineTo(0, 0);
        path.close();
    }

    private static void clipPopupBodyFromPath(float popupRadius, float popupWidth,
            float popupHeight, float arrowOffsetX, float arrowOffsetY, Path path) {
        // Make a path that is used to clip the triangle, this represents the body of the popup
        Path clipPiece = new Path();
        clipPiece.addRoundRect(
                0, 0, popupWidth, popupHeight,
                popupRadius, popupRadius, Path.Direction.CW);
        // clipping is performed as if the arrow is pointing down and positioned on the left, the
        // resulting path will be flipped as needed later.
        // The extra 0.5 in the vertical offset is to close the gap between this anti-aliased object
        // and the anti-aliased body of the popup.
        clipPiece.offset(-arrowOffsetX, -popupHeight + arrowOffsetY - 0.5f);
        path.op(clipPiece, Path.Op.DIFFERENCE);
    }
}