• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 androidx.appcompat.graphics.drawable;
18 
19 import static androidx.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 
31 import androidx.annotation.ColorInt;
32 import androidx.annotation.FloatRange;
33 import androidx.annotation.IntDef;
34 import androidx.annotation.RestrictTo;
35 import androidx.appcompat.R;
36 import androidx.core.graphics.drawable.DrawableCompat;
37 import androidx.core.view.ViewCompat;
38 
39 import java.lang.annotation.Retention;
40 import 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  */
48 public 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      */
DrawerArrowDrawable(Context context)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      */
setArrowHeadLength(float length)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      */
getArrowHeadLength()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      */
setArrowShaftLength(float length)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      */
getArrowShaftLength()189     public float getArrowShaftLength() {
190         return mArrowShaftLength;
191     }
192 
193     /**
194      * The length of the bars when they are parallel to each other.
195      */
getBarLength()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      */
setBarLength(float length)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      */
setColor(@olorInt int color)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
getColor()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      */
setBarThickness(float width)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      */
getBarThickness()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      */
getGapSize()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      */
setGapSize(float gap)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      */
setDirection(@rrowDirection int direction)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      */
isSpinEnabled()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      */
setSpinEnabled(boolean enabled)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
getDirection()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      */
setVerticalMirror(boolean verticalMirror)317     public void setVerticalMirror(boolean verticalMirror) {
318         if (mVerticalMirror != verticalMirror) {
319             mVerticalMirror = verticalMirror;
320             invalidateSelf();
321         }
322     }
323 
324     @Override
draw(Canvas canvas)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
setAlpha(int alpha)404     public void setAlpha(int alpha) {
405         if (alpha != mPaint.getAlpha()) {
406             mPaint.setAlpha(alpha);
407             invalidateSelf();
408         }
409     }
410 
411     @Override
setColorFilter(ColorFilter colorFilter)412     public void setColorFilter(ColorFilter colorFilter) {
413         mPaint.setColorFilter(colorFilter);
414         invalidateSelf();
415     }
416 
417     @Override
getIntrinsicHeight()418     public int getIntrinsicHeight() {
419         return mSize;
420     }
421 
422     @Override
getIntrinsicWidth()423     public int getIntrinsicWidth() {
424         return mSize;
425     }
426 
427     @Override
getOpacity()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)
getProgress()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      */
setProgress(@loatRangefrom = 0.0, to = 1.0) float progress)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      */
getPaint()457     public final Paint getPaint() {
458         return mPaint;
459     }
460 
461     /**
462      * Linear interpolate between a and b with parameter t.
463      */
lerp(float a, float b, float t)464     private static float lerp(float a, float b, float t) {
465         return a + (b - a) * t;
466     }
467 }