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