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 isHorizontal or not. 88 * @param isLeftOrTop or not. 89 * @param color to draw the triangle. 90 */ RoundedArrowDrawable(float width, float height, float radius, boolean isHorizontal, boolean isLeftOrTop, int color)91 private RoundedArrowDrawable(float width, float height, float radius, boolean isHorizontal, 92 boolean isLeftOrTop, int color) { 93 mPath = new Path(); 94 mPaint = new Paint(); 95 mPaint.setColor(color); 96 mPaint.setStyle(Paint.Style.FILL); 97 mPaint.setAntiAlias(true); 98 99 // Make the drawable with the triangle pointing down... 100 addDownPointingRoundedTriangleToPath(width, height, radius, mPath); 101 102 if (isHorizontal || isLeftOrTop) { 103 // ... then rotate it to the side it needs to point. 104 Matrix pathTransform = new Matrix(); 105 int rotationAngle; 106 if (isHorizontal) { 107 rotationAngle = isLeftOrTop ? 90 : -90; 108 } else { 109 // it could only be vertical arrow pointing up 110 rotationAngle = 180; 111 } 112 pathTransform.setRotate(rotationAngle, width * 0.5f, height * 0.5f); 113 mPath.transform(pathTransform); 114 } 115 } 116 117 /** 118 * factory method for an arrow that points to the left or right. 119 * 120 * @param width of the arrow. 121 * @param height of the arrow. 122 * @param radius of the tip of the arrow. 123 * @param isPointingLeft or not. 124 * @param color to draw the triangle. 125 */ createHorizontalRoundedArrow(float width, float height, float radius, boolean isPointingLeft, int color)126 public static RoundedArrowDrawable createHorizontalRoundedArrow(float width, float height, 127 float radius, boolean isPointingLeft, int color) { 128 return new RoundedArrowDrawable(width, height, radius, true, isPointingLeft, color); 129 } 130 131 /** 132 * factory method for an arrow that points to the left or right. 133 * 134 * @param width of the arrow. 135 * @param height of the arrow. 136 * @param radius of the tip of the arrow. 137 * @param isPointingUp or not. 138 * @param color to draw the triangle. 139 */ createVerticalRoundedArrow(float width, float height, float radius, boolean isPointingUp, int color)140 public static RoundedArrowDrawable createVerticalRoundedArrow(float width, float height, 141 float radius, boolean isPointingUp, int color) { 142 return new RoundedArrowDrawable(width, height, radius, false, isPointingUp, color); 143 } 144 145 @Override draw(Canvas canvas)146 public void draw(Canvas canvas) { 147 canvas.drawPath(mPath, mPaint); 148 } 149 150 @Override getOutline(Outline outline)151 public void getOutline(Outline outline) { 152 outline.setPath(mPath); 153 } 154 155 @Override getOpacity()156 public int getOpacity() { 157 return PixelFormat.TRANSLUCENT; 158 } 159 160 @Override setAlpha(int i)161 public void setAlpha(int i) { 162 mPaint.setAlpha(i); 163 } 164 165 @Override setColorFilter(ColorFilter colorFilter)166 public void setColorFilter(ColorFilter colorFilter) { 167 mPaint.setColorFilter(colorFilter); 168 } 169 170 /** 171 * Set shadow layer to internal {@link Paint#setShadowLayer(float, float, float, int) paint} 172 * object 173 */ setShadowLayer(float shadowBlur, float dx, float dy, int shadowColor)174 public void setShadowLayer(float shadowBlur, float dx, float dy, int shadowColor) { 175 mPaint.setShadowLayer(shadowBlur, dx, dy, shadowColor); 176 } 177 178 /** 179 * Adds rounded triangle pointing down to the provided {@link Path path} argument 180 */ addDownPointingRoundedTriangleToPath(float width, float height, float radius, Path path)181 public static void addDownPointingRoundedTriangleToPath(float width, float height, 182 float radius, Path path) { 183 // Calculated for the arrow pointing down, will be flipped later if needed. 184 185 // Theta is half of the angle inside the triangle tip 186 float tanTheta = width / (2.0f * height); 187 float theta = (float) atan(tanTheta); 188 189 // Some trigonometry to find the center of the circle for the rounded tip 190 float roundedPointCenterY = (float) (height - (radius / sin(theta))); 191 192 // p is the distance along the triangle side to the intersection with the point circle 193 float p = radius / tanTheta; 194 float lineRoundPointIntersectFromCenter = (float) (p * sin(theta)); 195 float lineRoundPointIntersectFromTop = (float) (height - (p * cos(theta))); 196 197 float centerX = width / 2.0f; 198 float thetaDeg = (float) toDegrees(theta); 199 200 path.reset(); 201 path.moveTo(0, 0); 202 // Draw the top 203 path.lineTo(width, 0); 204 // Draw the right side up to the circle intersection 205 path.lineTo( 206 centerX + lineRoundPointIntersectFromCenter, 207 lineRoundPointIntersectFromTop); 208 // Draw the rounded point 209 path.arcTo( 210 centerX - radius, 211 roundedPointCenterY - radius, 212 centerX + radius, 213 roundedPointCenterY + radius, 214 thetaDeg, 215 180 - (2 * thetaDeg), 216 false); 217 // Draw the left edge to close 218 path.lineTo(0, 0); 219 path.close(); 220 } 221 clipPopupBodyFromPath(float popupRadius, float popupWidth, float popupHeight, float arrowOffsetX, float arrowOffsetY, Path path)222 private static void clipPopupBodyFromPath(float popupRadius, float popupWidth, 223 float popupHeight, float arrowOffsetX, float arrowOffsetY, Path path) { 224 // Make a path that is used to clip the triangle, this represents the body of the popup 225 Path clipPiece = new Path(); 226 clipPiece.addRoundRect( 227 0, 0, popupWidth, popupHeight, 228 popupRadius, popupRadius, Path.Direction.CW); 229 // clipping is performed as if the arrow is pointing down and positioned on the left, the 230 // resulting path will be flipped as needed later. 231 // The extra 0.5 in the vertical offset is to close the gap between this anti-aliased object 232 // and the anti-aliased body of the popup. 233 clipPiece.offset(-arrowOffsetX, -popupHeight + arrowOffsetY - 0.5f); 234 path.op(clipPiece, Path.Op.DIFFERENCE); 235 } 236 } 237