| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.support.v7.graphics.drawable; |
| |
| import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; |
| |
| import android.content.Context; |
| import android.content.res.TypedArray; |
| import android.graphics.Canvas; |
| import android.graphics.ColorFilter; |
| import android.graphics.Paint; |
| import android.graphics.Path; |
| import android.graphics.PixelFormat; |
| import android.graphics.Rect; |
| import android.graphics.drawable.Drawable; |
| import android.support.annotation.ColorInt; |
| import android.support.annotation.FloatRange; |
| import android.support.annotation.IntDef; |
| import android.support.annotation.RestrictTo; |
| import android.support.v4.graphics.drawable.DrawableCompat; |
| import android.support.v4.view.ViewCompat; |
| import android.support.v7.appcompat.R; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| |
| /** |
| * A drawable that can draw a "Drawer hamburger" menu or an arrow and animate between them. |
| * <p> |
| * The progress between the two states is controlled via {@link #setProgress(float)}. |
| * </p> |
| */ |
| public class DrawerArrowDrawable extends Drawable { |
| |
| /** |
| * Direction to make the arrow point towards the left. |
| * |
| * @see #setDirection(int) |
| * @see #getDirection() |
| */ |
| public static final int ARROW_DIRECTION_LEFT = 0; |
| |
| /** |
| * Direction to make the arrow point towards the right. |
| * |
| * @see #setDirection(int) |
| * @see #getDirection() |
| */ |
| public static final int ARROW_DIRECTION_RIGHT = 1; |
| |
| /** |
| * Direction to make the arrow point towards the start. |
| * |
| * <p>When used in a view with a {@link ViewCompat#LAYOUT_DIRECTION_RTL RTL} layout direction, |
| * this is the same as {@link #ARROW_DIRECTION_RIGHT}, otherwise it is the same as |
| * {@link #ARROW_DIRECTION_LEFT}.</p> |
| * |
| * @see #setDirection(int) |
| * @see #getDirection() |
| */ |
| public static final int ARROW_DIRECTION_START = 2; |
| |
| /** |
| * Direction to make the arrow point to the end. |
| * |
| * <p>When used in a view with a {@link ViewCompat#LAYOUT_DIRECTION_RTL RTL} layout direction, |
| * this is the same as {@link #ARROW_DIRECTION_LEFT}, otherwise it is the same as |
| * {@link #ARROW_DIRECTION_RIGHT}.</p> |
| * |
| * @see #setDirection(int) |
| * @see #getDirection() |
| */ |
| public static final int ARROW_DIRECTION_END = 3; |
| |
| /** @hide */ |
| @RestrictTo(LIBRARY_GROUP) |
| @IntDef({ARROW_DIRECTION_LEFT, ARROW_DIRECTION_RIGHT, |
| ARROW_DIRECTION_START, ARROW_DIRECTION_END}) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface ArrowDirection {} |
| |
| private final Paint mPaint = new Paint(); |
| |
| // The angle in degrees that the arrow head is inclined at. |
| private static final float ARROW_HEAD_ANGLE = (float) Math.toRadians(45); |
| // The length of top and bottom bars when they merge into an arrow |
| private float mArrowHeadLength; |
| // The length of middle bar |
| private float mBarLength; |
| // The length of the middle bar when arrow is shaped |
| private float mArrowShaftLength; |
| // The space between bars when they are parallel |
| private float mBarGap; |
| // Whether bars should spin or not during progress |
| private boolean mSpin; |
| // Use Path instead of canvas operations so that if color has transparency, overlapping sections |
| // wont look different |
| private final Path mPath = new Path(); |
| // The reported intrinsic size of the drawable. |
| private final int mSize; |
| // Whether we should mirror animation when animation is reversed. |
| private boolean mVerticalMirror = false; |
| // The interpolated version of the original progress |
| private float mProgress; |
| // the amount that overlaps w/ bar size when rotation is max |
| private float mMaxCutForBarSize; |
| // The arrow direction |
| private int mDirection = ARROW_DIRECTION_START; |
| |
| /** |
| * @param context used to get the configuration for the drawable from |
| */ |
| public DrawerArrowDrawable(Context context) { |
| mPaint.setStyle(Paint.Style.STROKE); |
| mPaint.setStrokeJoin(Paint.Join.MITER); |
| mPaint.setStrokeCap(Paint.Cap.BUTT); |
| mPaint.setAntiAlias(true); |
| |
| final TypedArray a = context.getTheme().obtainStyledAttributes(null, |
| R.styleable.DrawerArrowToggle, R.attr.drawerArrowStyle, |
| R.style.Base_Widget_AppCompat_DrawerArrowToggle); |
| |
| setColor(a.getColor(R.styleable.DrawerArrowToggle_color, 0)); |
| setBarThickness(a.getDimension(R.styleable.DrawerArrowToggle_thickness, 0)); |
| setSpinEnabled(a.getBoolean(R.styleable.DrawerArrowToggle_spinBars, true)); |
| // round this because having this floating may cause bad measurements |
| setGapSize(Math.round(a.getDimension(R.styleable.DrawerArrowToggle_gapBetweenBars, 0))); |
| |
| mSize = a.getDimensionPixelSize(R.styleable.DrawerArrowToggle_drawableSize, 0); |
| // round this because having this floating may cause bad measurements |
| mBarLength = Math.round(a.getDimension(R.styleable.DrawerArrowToggle_barLength, 0)); |
| // round this because having this floating may cause bad measurements |
| mArrowHeadLength = Math.round(a.getDimension( |
| R.styleable.DrawerArrowToggle_arrowHeadLength, 0)); |
| mArrowShaftLength = a.getDimension(R.styleable.DrawerArrowToggle_arrowShaftLength, 0); |
| a.recycle(); |
| } |
| |
| /** |
| * Sets the length of the arrow head (from tip to edge, perpendicular to the shaft). |
| * |
| * @param length the length in pixels |
| */ |
| public void setArrowHeadLength(float length) { |
| if (mArrowHeadLength != length) { |
| mArrowHeadLength = length; |
| invalidateSelf(); |
| } |
| } |
| |
| /** |
| * Returns the length of the arrow head (from tip to edge, perpendicular to the shaft), |
| * in pixels. |
| */ |
| public float getArrowHeadLength() { |
| return mArrowHeadLength; |
| } |
| |
| /** |
| * Sets the arrow shaft length. |
| * |
| * @param length the length in pixels |
| */ |
| public void setArrowShaftLength(float length) { |
| if (mArrowShaftLength != length) { |
| mArrowShaftLength = length; |
| invalidateSelf(); |
| } |
| } |
| |
| /** |
| * Returns the arrow shaft length in pixels. |
| */ |
| public float getArrowShaftLength() { |
| return mArrowShaftLength; |
| } |
| |
| /** |
| * The length of the bars when they are parallel to each other. |
| */ |
| public float getBarLength() { |
| return mBarLength; |
| } |
| |
| /** |
| * Sets the length of the bars when they are parallel to each other. |
| * |
| * @param length the length in pixels |
| */ |
| public void setBarLength(float length) { |
| if (mBarLength != length) { |
| mBarLength = length; |
| invalidateSelf(); |
| } |
| } |
| |
| /** |
| * Sets the color of the drawable. |
| */ |
| public void setColor(@ColorInt int color) { |
| if (color != mPaint.getColor()) { |
| mPaint.setColor(color); |
| invalidateSelf(); |
| } |
| } |
| |
| /** |
| * Returns the color of the drawable. |
| */ |
| @ColorInt |
| public int getColor() { |
| return mPaint.getColor(); |
| } |
| |
| /** |
| * Sets the thickness (stroke size) for the bars. |
| * |
| * @param width stroke width in pixels |
| */ |
| public void setBarThickness(float width) { |
| if (mPaint.getStrokeWidth() != width) { |
| mPaint.setStrokeWidth(width); |
| mMaxCutForBarSize = (float) (width / 2 * Math.cos(ARROW_HEAD_ANGLE)); |
| invalidateSelf(); |
| } |
| } |
| |
| /** |
| * Returns the thickness (stroke width) of the bars. |
| */ |
| public float getBarThickness() { |
| return mPaint.getStrokeWidth(); |
| } |
| |
| /** |
| * Returns the max gap between the bars when they are parallel to each other. |
| * |
| * @see #getGapSize() |
| */ |
| public float getGapSize() { |
| return mBarGap; |
| } |
| |
| /** |
| * Sets the max gap between the bars when they are parallel to each other. |
| * |
| * @param gap the gap in pixels |
| * |
| * @see #getGapSize() |
| */ |
| public void setGapSize(float gap) { |
| if (gap != mBarGap) { |
| mBarGap = gap; |
| invalidateSelf(); |
| } |
| } |
| |
| /** |
| * Set the arrow direction. |
| */ |
| public void setDirection(@ArrowDirection int direction) { |
| if (direction != mDirection) { |
| mDirection = direction; |
| invalidateSelf(); |
| } |
| } |
| |
| /** |
| * Returns whether the bars should rotate or not during the transition. |
| * |
| * @see #setSpinEnabled(boolean) |
| */ |
| public boolean isSpinEnabled() { |
| return mSpin; |
| } |
| |
| /** |
| * Returns whether the bars should rotate or not during the transition. |
| * |
| * @param enabled true if the bars should rotate. |
| * |
| * @see #isSpinEnabled() |
| */ |
| public void setSpinEnabled(boolean enabled) { |
| if (mSpin != enabled) { |
| mSpin = enabled; |
| invalidateSelf(); |
| } |
| } |
| |
| /** |
| * Returns the arrow direction. |
| */ |
| @ArrowDirection |
| public int getDirection() { |
| return mDirection; |
| } |
| |
| /** |
| * If set, canvas is flipped when progress reached to end and going back to start. |
| */ |
| public void setVerticalMirror(boolean verticalMirror) { |
| if (mVerticalMirror != verticalMirror) { |
| mVerticalMirror = verticalMirror; |
| invalidateSelf(); |
| } |
| } |
| |
| @Override |
| public void draw(Canvas canvas) { |
| Rect bounds = getBounds(); |
| |
| final boolean flipToPointRight; |
| switch (mDirection) { |
| case ARROW_DIRECTION_LEFT: |
| flipToPointRight = false; |
| break; |
| case ARROW_DIRECTION_RIGHT: |
| flipToPointRight = true; |
| break; |
| case ARROW_DIRECTION_END: |
| flipToPointRight = DrawableCompat.getLayoutDirection(this) |
| == ViewCompat.LAYOUT_DIRECTION_LTR; |
| break; |
| case ARROW_DIRECTION_START: |
| default: |
| flipToPointRight = DrawableCompat.getLayoutDirection(this) |
| == ViewCompat.LAYOUT_DIRECTION_RTL; |
| break; |
| } |
| |
| // Interpolated widths of arrow bars |
| |
| float arrowHeadBarLength = (float) Math.sqrt(mArrowHeadLength * mArrowHeadLength * 2); |
| arrowHeadBarLength = lerp(mBarLength, arrowHeadBarLength, mProgress); |
| final float arrowShaftLength = lerp(mBarLength, mArrowShaftLength, mProgress); |
| // Interpolated size of middle bar |
| final float arrowShaftCut = Math.round(lerp(0, mMaxCutForBarSize, mProgress)); |
| // The rotation of the top and bottom bars (that make the arrow head) |
| final float rotation = lerp(0, ARROW_HEAD_ANGLE, mProgress); |
| |
| // The whole canvas rotates as the transition happens |
| final float canvasRotate = lerp(flipToPointRight ? 0 : -180, |
| flipToPointRight ? 180 : 0, mProgress); |
| |
| final float arrowWidth = Math.round(arrowHeadBarLength * Math.cos(rotation)); |
| final float arrowHeight = Math.round(arrowHeadBarLength * Math.sin(rotation)); |
| |
| mPath.rewind(); |
| final float topBottomBarOffset = lerp(mBarGap + mPaint.getStrokeWidth(), -mMaxCutForBarSize, |
| mProgress); |
| |
| final float arrowEdge = -arrowShaftLength / 2; |
| // draw middle bar |
| mPath.moveTo(arrowEdge + arrowShaftCut, 0); |
| mPath.rLineTo(arrowShaftLength - arrowShaftCut * 2, 0); |
| |
| // bottom bar |
| mPath.moveTo(arrowEdge, topBottomBarOffset); |
| mPath.rLineTo(arrowWidth, arrowHeight); |
| |
| // top bar |
| mPath.moveTo(arrowEdge, -topBottomBarOffset); |
| mPath.rLineTo(arrowWidth, -arrowHeight); |
| |
| mPath.close(); |
| |
| canvas.save(); |
| |
| // Rotate the whole canvas if spinning, if not, rotate it 180 to get |
| // the arrow pointing the other way for RTL. |
| final float barThickness = mPaint.getStrokeWidth(); |
| final int remainingSpace = (int) (bounds.height() - barThickness * 3 - mBarGap * 2); |
| float yOffset = (remainingSpace / 4) * 2; // making sure it is a multiple of 2. |
| yOffset += barThickness * 1.5 + mBarGap; |
| |
| canvas.translate(bounds.centerX(), yOffset); |
| if (mSpin) { |
| canvas.rotate(canvasRotate * ((mVerticalMirror ^ flipToPointRight) ? -1 : 1)); |
| } else if (flipToPointRight) { |
| canvas.rotate(180); |
| } |
| canvas.drawPath(mPath, mPaint); |
| |
| canvas.restore(); |
| } |
| |
| @Override |
| public void setAlpha(int alpha) { |
| if (alpha != mPaint.getAlpha()) { |
| mPaint.setAlpha(alpha); |
| invalidateSelf(); |
| } |
| } |
| |
| @Override |
| public void setColorFilter(ColorFilter colorFilter) { |
| mPaint.setColorFilter(colorFilter); |
| invalidateSelf(); |
| } |
| |
| @Override |
| public int getIntrinsicHeight() { |
| return mSize; |
| } |
| |
| @Override |
| public int getIntrinsicWidth() { |
| return mSize; |
| } |
| |
| @Override |
| public int getOpacity() { |
| return PixelFormat.TRANSLUCENT; |
| } |
| |
| /** |
| * Returns the current progress of the arrow. |
| */ |
| @FloatRange(from = 0.0, to = 1.0) |
| public float getProgress() { |
| return mProgress; |
| } |
| |
| /** |
| * Set the progress of the arrow. |
| * |
| * <p>A value of {@code 0.0} indicates that the arrow should be drawn in it's starting |
| * position. A value of {@code 1.0} indicates that the arrow should be drawn in it's ending |
| * position.</p> |
| */ |
| public void setProgress(@FloatRange(from = 0.0, to = 1.0) float progress) { |
| if (mProgress != progress) { |
| mProgress = progress; |
| invalidateSelf(); |
| } |
| } |
| |
| /** |
| * Returns the paint instance used for all drawing. |
| */ |
| public final Paint getPaint() { |
| return mPaint; |
| } |
| |
| /** |
| * Linear interpolate between a and b with parameter t. |
| */ |
| private static float lerp(float a, float b, float t) { |
| return a + (b - a) * t; |
| } |
| } |