• 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     /**
82      * Constructor for an arrow that points to the left or right.
83      *
84      * @param width        of the arrow.
85      * @param height       of the arrow.
86      * @param radius       of the tip of the arrow.
87      * @param isPointingLeft or not.
88      * @param color        to draw the triangle.
89      */
RoundedArrowDrawable(float width, float height, float radius, boolean isPointingLeft, int color)90     public RoundedArrowDrawable(float width, float height, float radius, boolean isPointingLeft,
91             int color) {
92         mPath = new Path();
93         mPaint = new Paint();
94         mPaint.setColor(color);
95         mPaint.setStyle(Paint.Style.FILL);
96         mPaint.setAntiAlias(true);
97 
98         // Make the drawable with the triangle pointing down...
99         addDownPointingRoundedTriangleToPath(width, height, radius, mPath);
100 
101         // ... then rotate it to the side it needs to point.
102         Matrix pathTransform = new Matrix();
103         pathTransform.setRotate(isPointingLeft ? 90 : -90, width * 0.5f, height * 0.5f);
104         mPath.transform(pathTransform);
105     }
106 
107     @Override
draw(Canvas canvas)108     public void draw(Canvas canvas) {
109         canvas.drawPath(mPath, mPaint);
110     }
111 
112     @Override
getOutline(Outline outline)113     public void getOutline(Outline outline) {
114         outline.setPath(mPath);
115     }
116 
117     @Override
getOpacity()118     public int getOpacity() {
119         return PixelFormat.TRANSLUCENT;
120     }
121 
122     @Override
setAlpha(int i)123     public void setAlpha(int i) {
124         mPaint.setAlpha(i);
125     }
126 
127     @Override
setColorFilter(ColorFilter colorFilter)128     public void setColorFilter(ColorFilter colorFilter) {
129         mPaint.setColorFilter(colorFilter);
130     }
131 
addDownPointingRoundedTriangleToPath(float width, float height, float radius, Path path)132     private static void addDownPointingRoundedTriangleToPath(float width, float height,
133             float radius, Path path) {
134         // Calculated for the arrow pointing down, will be flipped later if needed.
135 
136         // Theta is half of the angle inside the triangle tip
137         float tanTheta = width / (2.0f * height);
138         float theta = (float) atan(tanTheta);
139 
140         // Some trigonometry to find the center of the circle for the rounded tip
141         float roundedPointCenterY = (float) (height - (radius / sin(theta)));
142 
143         // p is the distance along the triangle side to the intersection with the point circle
144         float p = radius / tanTheta;
145         float lineRoundPointIntersectFromCenter = (float) (p * sin(theta));
146         float lineRoundPointIntersectFromTop = (float) (height - (p * cos(theta)));
147 
148         float centerX = width / 2.0f;
149         float thetaDeg = (float) toDegrees(theta);
150 
151         path.reset();
152         path.moveTo(0, 0);
153         // Draw the top
154         path.lineTo(width, 0);
155         // Draw the right side up to the circle intersection
156         path.lineTo(
157                 centerX + lineRoundPointIntersectFromCenter,
158                 lineRoundPointIntersectFromTop);
159         // Draw the rounded point
160         path.arcTo(
161                 centerX - radius,
162                 roundedPointCenterY - radius,
163                 centerX + radius,
164                 roundedPointCenterY + radius,
165                 thetaDeg,
166                 180 - (2 * thetaDeg),
167                 false);
168         // Draw the left edge to close
169         path.lineTo(0, 0);
170         path.close();
171     }
172 
clipPopupBodyFromPath(float popupRadius, float popupWidth, float popupHeight, float arrowOffsetX, float arrowOffsetY, Path path)173     private static void clipPopupBodyFromPath(float popupRadius, float popupWidth,
174             float popupHeight, float arrowOffsetX, float arrowOffsetY, Path path) {
175         // Make a path that is used to clip the triangle, this represents the body of the popup
176         Path clipPiece = new Path();
177         clipPiece.addRoundRect(
178                 0, 0, popupWidth, popupHeight,
179                 popupRadius, popupRadius, Path.Direction.CW);
180         // clipping is performed as if the arrow is pointing down and positioned on the left, the
181         // resulting path will be flipped as needed later.
182         // The extra 0.5 in the vertical offset is to close the gap between this anti-aliased object
183         // and the anti-aliased body of the popup.
184         clipPiece.offset(-arrowOffsetX, -popupHeight + arrowOffsetY - 0.5f);
185         path.op(clipPiece, Path.Op.DIFFERENCE);
186     }
187 }
188