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 }