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