• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.launcher3.popup;
18 
19 import static java.lang.Math.atan;
20 import static java.lang.Math.cos;
21 import static java.lang.Math.sin;
22 import static java.lang.Math.toDegrees;
23 
24 import android.graphics.Canvas;
25 import android.graphics.ColorFilter;
26 import android.graphics.Matrix;
27 import android.graphics.Outline;
28 import android.graphics.Paint;
29 import android.graphics.Path;
30 import android.graphics.PixelFormat;
31 import android.graphics.drawable.Drawable;
32 
33 /**
34  * A drawable for a very specific purpose. Used for the caret arrow on a rounded rectangle popup
35  * bubble.
36  * Draws a triangle with one rounded tip, the opposite edge is clipped by the body of the popup
37  * so there is no overlap when drawing them together.
38  */
39 public class RoundedArrowDrawable extends Drawable {
40 
41     private final Path mPath;
42     private final Paint mPaint;
43 
44     /**
45      * Default constructor.
46      *
47      * @param width of the arrow.
48      * @param height of the arrow.
49      * @param radius of the tip of the arrow.
50      * @param popupRadius of the rect to clip this by.
51      * @param popupWidth of the rect to clip this by.
52      * @param popupHeight of the rect to clip this by.
53      * @param arrowOffsetX from the edge of the popup to the arrow.
54      * @param arrowOffsetY how much the arrow will overlap the popup.
55      * @param isPointingUp or not.
56      * @param leftAligned or false for right aligned.
57      * @param color to draw the triangle.
58      */
RoundedArrowDrawable(float width, float height, float radius, float popupRadius, float popupWidth, float popupHeight, float arrowOffsetX, float arrowOffsetY, boolean isPointingUp, boolean leftAligned, int color)59     public RoundedArrowDrawable(float width, float height, float radius, float popupRadius,
60             float popupWidth, float popupHeight,
61             float arrowOffsetX, float arrowOffsetY, boolean isPointingUp, boolean leftAligned,
62             int color) {
63         mPath = new Path();
64         mPaint = new Paint();
65         mPaint.setColor(color);
66         mPaint.setStyle(Paint.Style.FILL);
67         mPaint.setAntiAlias(true);
68 
69         // Make the drawable with the triangle pointing down and positioned on the left..
70         addDownPointingRoundedTriangleToPath(width, height, radius, mPath);
71         clipPopupBodyFromPath(popupRadius, popupWidth, popupHeight, arrowOffsetX, arrowOffsetY,
72                 mPath);
73 
74         // ... then flip it horizontal or vertical based on where it will be used.
75         Matrix pathTransform = new Matrix();
76         pathTransform.setScale(
77                 leftAligned ? 1 : -1, isPointingUp ? -1 : 1, width * 0.5f, height * 0.5f);
78         mPath.transform(pathTransform);
79     }
80 
81     @Override
draw(Canvas canvas)82     public void draw(Canvas canvas) {
83         canvas.drawPath(mPath, mPaint);
84     }
85 
86     @Override
getOutline(Outline outline)87     public void getOutline(Outline outline) {
88         outline.setPath(mPath);
89     }
90 
91     @Override
getOpacity()92     public int getOpacity() {
93         return PixelFormat.TRANSLUCENT;
94     }
95 
96     @Override
setAlpha(int i)97     public void setAlpha(int i) {
98         mPaint.setAlpha(i);
99     }
100 
101     @Override
setColorFilter(ColorFilter colorFilter)102     public void setColorFilter(ColorFilter colorFilter) {
103         mPaint.setColorFilter(colorFilter);
104     }
105 
addDownPointingRoundedTriangleToPath(float width, float height, float radius, Path path)106     private static void addDownPointingRoundedTriangleToPath(float width, float height,
107             float radius, Path path) {
108         // Calculated for the arrow pointing down, will be flipped later if needed.
109 
110         // Theta is half of the angle inside the triangle tip
111         float tanTheta = width / (2.0f * height);
112         float theta = (float) atan(tanTheta);
113 
114         // Some trigonometry to find the center of the circle for the rounded tip
115         float roundedPointCenterY = (float) (height - (radius / sin(theta)));
116 
117         // p is the distance along the triangle side to the intersection with the point circle
118         float p = radius / tanTheta;
119         float lineRoundPointIntersectFromCenter = (float) (p * sin(theta));
120         float lineRoundPointIntersectFromTop = (float) (height - (p * cos(theta)));
121 
122         float centerX = width / 2.0f;
123         float thetaDeg = (float) toDegrees(theta);
124 
125         path.reset();
126         path.moveTo(0, 0);
127         // Draw the top
128         path.lineTo(width, 0);
129         // Draw the right side up to the circle intersection
130         path.lineTo(
131                 centerX + lineRoundPointIntersectFromCenter,
132                 lineRoundPointIntersectFromTop);
133         // Draw the rounded point
134         path.arcTo(
135                 centerX - radius,
136                 roundedPointCenterY - radius,
137                 centerX + radius,
138                 roundedPointCenterY + radius,
139                 thetaDeg,
140                 180 - (2 * thetaDeg),
141                 false);
142         // Draw the left edge to close
143         path.lineTo(0, 0);
144         path.close();
145     }
146 
clipPopupBodyFromPath(float popupRadius, float popupWidth, float popupHeight, float arrowOffsetX, float arrowOffsetY, Path path)147     private static void clipPopupBodyFromPath(float popupRadius, float popupWidth,
148             float popupHeight, float arrowOffsetX, float arrowOffsetY, Path path) {
149         // Make a path that is used to clip the triangle, this represents the body of the popup
150         Path clipPiece = new Path();
151         clipPiece.addRoundRect(
152                 0, 0, popupWidth, popupHeight,
153                 popupRadius, popupRadius, Path.Direction.CW);
154         // clipping is performed as if the arrow is pointing down and positioned on the left, the
155         // resulting path will be flipped as needed later.
156         // The extra 0.5 in the vertical offset is to close the gap between this anti-aliased object
157         // and the anti-aliased body of the popup.
158         clipPiece.offset(-arrowOffsetX, -popupHeight + arrowOffsetY - 0.5f);
159         path.op(clipPiece, Path.Op.DIFFERENCE);
160     }
161 }
162