1 /* 2 * Copyright (C) 2014 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 androidx.appcompat.graphics.drawable; 18 19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; 20 21 import android.content.Context; 22 import android.content.res.TypedArray; 23 import android.graphics.Canvas; 24 import android.graphics.ColorFilter; 25 import android.graphics.Paint; 26 import android.graphics.Path; 27 import android.graphics.PixelFormat; 28 import android.graphics.Rect; 29 import android.graphics.drawable.Drawable; 30 import android.view.View; 31 32 import androidx.annotation.ColorInt; 33 import androidx.annotation.FloatRange; 34 import androidx.annotation.IntDef; 35 import androidx.annotation.RestrictTo; 36 import androidx.appcompat.R; 37 import androidx.core.graphics.drawable.DrawableCompat; 38 import androidx.core.view.ViewCompat; 39 40 import org.jspecify.annotations.NonNull; 41 42 import java.lang.annotation.Retention; 43 import java.lang.annotation.RetentionPolicy; 44 45 /** 46 * A drawable that can draw a "Drawer hamburger" menu or an arrow and animate between them. 47 * <p> 48 * The progress between the two states is controlled via {@link #setProgress(float)}. 49 * </p> 50 */ 51 public class DrawerArrowDrawable extends Drawable { 52 53 /** 54 * Direction to make the arrow point towards the left. 55 * 56 * @see #setDirection(int) 57 * @see #getDirection() 58 */ 59 public static final int ARROW_DIRECTION_LEFT = 0; 60 61 /** 62 * Direction to make the arrow point towards the right. 63 * 64 * @see #setDirection(int) 65 * @see #getDirection() 66 */ 67 public static final int ARROW_DIRECTION_RIGHT = 1; 68 69 /** 70 * Direction to make the arrow point towards the start. 71 * 72 * <p>When used in a view with a {@link ViewCompat#LAYOUT_DIRECTION_RTL RTL} layout direction, 73 * this is the same as {@link #ARROW_DIRECTION_RIGHT}, otherwise it is the same as 74 * {@link #ARROW_DIRECTION_LEFT}.</p> 75 * 76 * @see #setDirection(int) 77 * @see #getDirection() 78 */ 79 public static final int ARROW_DIRECTION_START = 2; 80 81 /** 82 * Direction to make the arrow point to the end. 83 * 84 * <p>When used in a view with a {@link ViewCompat#LAYOUT_DIRECTION_RTL RTL} layout direction, 85 * this is the same as {@link #ARROW_DIRECTION_LEFT}, otherwise it is the same as 86 * {@link #ARROW_DIRECTION_RIGHT}.</p> 87 * 88 * @see #setDirection(int) 89 * @see #getDirection() 90 */ 91 public static final int ARROW_DIRECTION_END = 3; 92 93 @RestrictTo(LIBRARY_GROUP_PREFIX) 94 @IntDef({ARROW_DIRECTION_LEFT, ARROW_DIRECTION_RIGHT, 95 ARROW_DIRECTION_START, ARROW_DIRECTION_END}) 96 @Retention(RetentionPolicy.SOURCE) 97 public @interface ArrowDirection {} 98 99 private final Paint mPaint = new Paint(); 100 101 // The angle in degrees that the arrow head is inclined at. 102 private static final float ARROW_HEAD_ANGLE = (float) Math.toRadians(45); 103 // The length of top and bottom bars when they merge into an arrow 104 private float mArrowHeadLength; 105 // The length of middle bar 106 private float mBarLength; 107 // The length of the middle bar when arrow is shaped 108 private float mArrowShaftLength; 109 // The space between bars when they are parallel 110 private float mBarGap; 111 // Whether bars should spin or not during progress 112 private boolean mSpin; 113 // Use Path instead of canvas operations so that if color has transparency, overlapping sections 114 // wont look different 115 private final Path mPath = new Path(); 116 // The reported intrinsic size of the drawable. 117 private final int mSize; 118 // Whether we should mirror animation when animation is reversed. 119 private boolean mVerticalMirror = false; 120 // The interpolated version of the original progress 121 private float mProgress; 122 // the amount that overlaps w/ bar size when rotation is max 123 private float mMaxCutForBarSize; 124 // The arrow direction 125 private int mDirection = ARROW_DIRECTION_START; 126 127 /** 128 * @param context used to get the configuration for the drawable from 129 */ DrawerArrowDrawable(Context context)130 public DrawerArrowDrawable(Context context) { 131 mPaint.setStyle(Paint.Style.STROKE); 132 mPaint.setStrokeJoin(Paint.Join.MITER); 133 mPaint.setStrokeCap(Paint.Cap.BUTT); 134 mPaint.setAntiAlias(true); 135 136 final TypedArray a = context.getTheme().obtainStyledAttributes(null, 137 R.styleable.DrawerArrowToggle, R.attr.drawerArrowStyle, 138 R.style.Base_Widget_AppCompat_DrawerArrowToggle); 139 140 setColor(a.getColor(R.styleable.DrawerArrowToggle_color, 0)); 141 setBarThickness(a.getDimension(R.styleable.DrawerArrowToggle_thickness, 0)); 142 setSpinEnabled(a.getBoolean(R.styleable.DrawerArrowToggle_spinBars, true)); 143 // round this because having this floating may cause bad measurements 144 setGapSize(Math.round(a.getDimension(R.styleable.DrawerArrowToggle_gapBetweenBars, 0))); 145 146 mSize = a.getDimensionPixelSize(R.styleable.DrawerArrowToggle_drawableSize, 0); 147 // round this because having this floating may cause bad measurements 148 mBarLength = Math.round(a.getDimension(R.styleable.DrawerArrowToggle_barLength, 0)); 149 // round this because having this floating may cause bad measurements 150 mArrowHeadLength = Math.round(a.getDimension( 151 R.styleable.DrawerArrowToggle_arrowHeadLength, 0)); 152 mArrowShaftLength = a.getDimension(R.styleable.DrawerArrowToggle_arrowShaftLength, 0); 153 a.recycle(); 154 } 155 156 /** 157 * Sets the length of the arrow head (from tip to edge, perpendicular to the shaft). 158 * 159 * @param length the length in pixels 160 */ setArrowHeadLength(float length)161 public void setArrowHeadLength(float length) { 162 if (mArrowHeadLength != length) { 163 mArrowHeadLength = length; 164 invalidateSelf(); 165 } 166 } 167 168 /** 169 * Returns the length of the arrow head (from tip to edge, perpendicular to the shaft), 170 * in pixels. 171 */ getArrowHeadLength()172 public float getArrowHeadLength() { 173 return mArrowHeadLength; 174 } 175 176 /** 177 * Sets the arrow shaft length. 178 * 179 * @param length the length in pixels 180 */ setArrowShaftLength(float length)181 public void setArrowShaftLength(float length) { 182 if (mArrowShaftLength != length) { 183 mArrowShaftLength = length; 184 invalidateSelf(); 185 } 186 } 187 188 /** 189 * Returns the arrow shaft length in pixels. 190 */ getArrowShaftLength()191 public float getArrowShaftLength() { 192 return mArrowShaftLength; 193 } 194 195 /** 196 * The length of the bars when they are parallel to each other. 197 */ getBarLength()198 public float getBarLength() { 199 return mBarLength; 200 } 201 202 /** 203 * Sets the length of the bars when they are parallel to each other. 204 * 205 * @param length the length in pixels 206 */ setBarLength(float length)207 public void setBarLength(float length) { 208 if (mBarLength != length) { 209 mBarLength = length; 210 invalidateSelf(); 211 } 212 } 213 214 /** 215 * Sets the color of the drawable. 216 */ setColor(@olorInt int color)217 public void setColor(@ColorInt int color) { 218 if (color != mPaint.getColor()) { 219 mPaint.setColor(color); 220 invalidateSelf(); 221 } 222 } 223 224 /** 225 * Returns the color of the drawable. 226 */ 227 @ColorInt getColor()228 public int getColor() { 229 return mPaint.getColor(); 230 } 231 232 /** 233 * Sets the thickness (stroke size) for the bars. 234 * 235 * @param width stroke width in pixels 236 */ setBarThickness(float width)237 public void setBarThickness(float width) { 238 if (mPaint.getStrokeWidth() != width) { 239 mPaint.setStrokeWidth(width); 240 mMaxCutForBarSize = (float) (width / 2 * Math.cos(ARROW_HEAD_ANGLE)); 241 invalidateSelf(); 242 } 243 } 244 245 /** 246 * Returns the thickness (stroke width) of the bars. 247 */ getBarThickness()248 public float getBarThickness() { 249 return mPaint.getStrokeWidth(); 250 } 251 252 /** 253 * Returns the max gap between the bars when they are parallel to each other. 254 * 255 * @see #getGapSize() 256 */ getGapSize()257 public float getGapSize() { 258 return mBarGap; 259 } 260 261 /** 262 * Sets the max gap between the bars when they are parallel to each other. 263 * 264 * @param gap the gap in pixels 265 * 266 * @see #getGapSize() 267 */ setGapSize(float gap)268 public void setGapSize(float gap) { 269 if (gap != mBarGap) { 270 mBarGap = gap; 271 invalidateSelf(); 272 } 273 } 274 275 /** 276 * Set the arrow direction. 277 */ setDirection(@rrowDirection int direction)278 public void setDirection(@ArrowDirection int direction) { 279 if (direction != mDirection) { 280 mDirection = direction; 281 invalidateSelf(); 282 } 283 } 284 285 /** 286 * Returns whether the bars should rotate or not during the transition. 287 * 288 * @see #setSpinEnabled(boolean) 289 */ isSpinEnabled()290 public boolean isSpinEnabled() { 291 return mSpin; 292 } 293 294 /** 295 * Returns whether the bars should rotate or not during the transition. 296 * 297 * @param enabled true if the bars should rotate. 298 * 299 * @see #isSpinEnabled() 300 */ setSpinEnabled(boolean enabled)301 public void setSpinEnabled(boolean enabled) { 302 if (mSpin != enabled) { 303 mSpin = enabled; 304 invalidateSelf(); 305 } 306 } 307 308 /** 309 * Returns the arrow direction. 310 */ 311 @ArrowDirection getDirection()312 public int getDirection() { 313 return mDirection; 314 } 315 316 /** 317 * If set, canvas is flipped when progress reached to end and going back to start. 318 */ setVerticalMirror(boolean verticalMirror)319 public void setVerticalMirror(boolean verticalMirror) { 320 if (mVerticalMirror != verticalMirror) { 321 mVerticalMirror = verticalMirror; 322 invalidateSelf(); 323 } 324 } 325 326 @Override draw(@onNull Canvas canvas)327 public void draw(@NonNull Canvas canvas) { 328 Rect bounds = getBounds(); 329 330 final boolean flipToPointRight; 331 switch (mDirection) { 332 case ARROW_DIRECTION_LEFT: 333 flipToPointRight = false; 334 break; 335 case ARROW_DIRECTION_RIGHT: 336 flipToPointRight = true; 337 break; 338 case ARROW_DIRECTION_END: 339 flipToPointRight = DrawableCompat.getLayoutDirection(this) 340 == View.LAYOUT_DIRECTION_LTR; 341 break; 342 case ARROW_DIRECTION_START: 343 default: 344 flipToPointRight = DrawableCompat.getLayoutDirection(this) 345 == View.LAYOUT_DIRECTION_RTL; 346 break; 347 } 348 349 // Interpolated widths of arrow bars 350 351 float arrowHeadBarLength = (float) Math.sqrt(mArrowHeadLength * mArrowHeadLength * 2); 352 arrowHeadBarLength = lerp(mBarLength, arrowHeadBarLength, mProgress); 353 final float arrowShaftLength = lerp(mBarLength, mArrowShaftLength, mProgress); 354 // Interpolated size of middle bar 355 final float arrowShaftCut = Math.round(lerp(0, mMaxCutForBarSize, mProgress)); 356 // The rotation of the top and bottom bars (that make the arrow head) 357 final float rotation = lerp(0, ARROW_HEAD_ANGLE, mProgress); 358 359 // The whole canvas rotates as the transition happens 360 final float canvasRotate = lerp(flipToPointRight ? 0 : -180, 361 flipToPointRight ? 180 : 0, mProgress); 362 363 final float arrowWidth = Math.round(arrowHeadBarLength * Math.cos(rotation)); 364 final float arrowHeight = Math.round(arrowHeadBarLength * Math.sin(rotation)); 365 366 mPath.rewind(); 367 final float topBottomBarOffset = lerp(mBarGap + mPaint.getStrokeWidth(), -mMaxCutForBarSize, 368 mProgress); 369 370 final float arrowEdge = -arrowShaftLength / 2; 371 // draw middle bar 372 mPath.moveTo(arrowEdge + arrowShaftCut, 0); 373 mPath.rLineTo(arrowShaftLength - arrowShaftCut * 2, 0); 374 375 // bottom bar 376 mPath.moveTo(arrowEdge, topBottomBarOffset); 377 mPath.rLineTo(arrowWidth, arrowHeight); 378 379 // top bar 380 mPath.moveTo(arrowEdge, -topBottomBarOffset); 381 mPath.rLineTo(arrowWidth, -arrowHeight); 382 383 mPath.close(); 384 385 canvas.save(); 386 387 // Rotate the whole canvas if spinning, if not, rotate it 180 to get 388 // the arrow pointing the other way for RTL. 389 final float barThickness = mPaint.getStrokeWidth(); 390 final int remainingSpace = (int) (bounds.height() - barThickness * 3 - mBarGap * 2); 391 float yOffset = (remainingSpace / 4) * 2; // making sure it is a multiple of 2. 392 yOffset += barThickness * 1.5f + mBarGap; 393 394 canvas.translate(bounds.centerX(), yOffset); 395 if (mSpin) { 396 canvas.rotate(canvasRotate * ((mVerticalMirror ^ flipToPointRight) ? -1 : 1)); 397 } else if (flipToPointRight) { 398 canvas.rotate(180); 399 } 400 canvas.drawPath(mPath, mPaint); 401 402 canvas.restore(); 403 } 404 405 @Override setAlpha(int alpha)406 public void setAlpha(int alpha) { 407 if (alpha != mPaint.getAlpha()) { 408 mPaint.setAlpha(alpha); 409 invalidateSelf(); 410 } 411 } 412 413 @Override setColorFilter(ColorFilter colorFilter)414 public void setColorFilter(ColorFilter colorFilter) { 415 mPaint.setColorFilter(colorFilter); 416 invalidateSelf(); 417 } 418 419 @Override getIntrinsicHeight()420 public int getIntrinsicHeight() { 421 return mSize; 422 } 423 424 @Override getIntrinsicWidth()425 public int getIntrinsicWidth() { 426 return mSize; 427 } 428 429 @Override getOpacity()430 public int getOpacity() { 431 return PixelFormat.TRANSLUCENT; 432 } 433 434 /** 435 * Returns the current progress of the arrow. 436 */ 437 @FloatRange(from = 0.0, to = 1.0) getProgress()438 public float getProgress() { 439 return mProgress; 440 } 441 442 /** 443 * Set the progress of the arrow. 444 * 445 * <p>A value of {@code 0.0} indicates that the arrow should be drawn in its starting 446 * position. A value of {@code 1.0} indicates that the arrow should be drawn in its ending 447 * position.</p> 448 */ setProgress(@loatRangefrom = 0.0, to = 1.0) float progress)449 public void setProgress(@FloatRange(from = 0.0, to = 1.0) float progress) { 450 if (mProgress != progress) { 451 mProgress = progress; 452 invalidateSelf(); 453 } 454 } 455 456 /** 457 * Returns the paint instance used for all drawing. 458 */ getPaint()459 public final Paint getPaint() { 460 return mPaint; 461 } 462 463 /** 464 * Linear interpolate between a and b with parameter t. 465 */ lerp(float a, float b, float t)466 private static float lerp(float a, float b, float t) { 467 return a + (b - a) * t; 468 } 469 }