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 }