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