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 */
16package android.support.v7.app;
17
18
19import android.content.Context;
20import android.content.res.TypedArray;
21import android.graphics.Canvas;
22import android.graphics.Color;
23import android.graphics.ColorFilter;
24import android.graphics.Paint;
25import android.graphics.Path;
26import android.graphics.PixelFormat;
27import android.graphics.Rect;
28import android.graphics.drawable.Drawable;
29import android.support.v7.appcompat.R;
30
31/**
32 * A drawable that can draw a "Drawer hamburger" menu or an Arrow and animate between them.
33 */
34abstract class DrawerArrowDrawable extends Drawable {
35
36    private final Paint mPaint = new Paint();
37
38    // The angle in degress that the arrow head is inclined at.
39    private static final float ARROW_HEAD_ANGLE = (float) Math.toRadians(45);
40    private final float mBarThickness;
41    // The length of top and bottom bars when they merge into an arrow
42    private final float mTopBottomArrowSize;
43    // The length of middle bar
44    private final float mBarSize;
45    // The length of the middle bar when arrow is shaped
46    private final float mMiddleArrowSize;
47    // The space between bars when they are parallel
48    private final float mBarGap;
49    // Whether bars should spin or not during progress
50    private final boolean mSpin;
51    // Use Path instead of canvas operations so that if color has transparency, overlapping sections
52    // wont look different
53    private final Path mPath = new Path();
54    // The reported intrinsic size of the drawable.
55    private final int mSize;
56    // Whether we should mirror animation when animation is reversed.
57    private boolean mVerticalMirror = false;
58    // The interpolated version of the original progress
59    private float mProgress;
60    // the amount that overlaps w/ bar size when rotation is max
61    private float mMaxCutForBarSize;
62    // The distance of arrow's center from top when horizontal
63    private float mCenterOffset;
64
65    /**
66     * @param context used to get the configuration for the drawable from
67     */
68    DrawerArrowDrawable(Context context) {
69        final TypedArray typedArray = context.getTheme()
70                .obtainStyledAttributes(null, R.styleable.DrawerArrowToggle,
71                        R.attr.drawerArrowStyle,
72                        R.style.Base_Widget_AppCompat_DrawerArrowToggle);
73        mPaint.setAntiAlias(true);
74        mPaint.setColor(typedArray.getColor(R.styleable.DrawerArrowToggle_color, 0));
75        mSize = typedArray.getDimensionPixelSize(R.styleable.DrawerArrowToggle_drawableSize, 0);
76        // round this because having this floating may cause bad measurements
77        mBarSize = Math.round(typedArray.getDimension(R.styleable.DrawerArrowToggle_barSize, 0));
78        // round this because having this floating may cause bad measurements
79        mTopBottomArrowSize = Math.round(typedArray.getDimension(
80                R.styleable.DrawerArrowToggle_topBottomBarArrowSize, 0));
81        mBarThickness = typedArray.getDimension(R.styleable.DrawerArrowToggle_thickness, 0);
82        // round this because having this floating may cause bad measurements
83        mBarGap = Math.round(typedArray.getDimension(
84                R.styleable.DrawerArrowToggle_gapBetweenBars, 0));
85        mSpin = typedArray.getBoolean(R.styleable.DrawerArrowToggle_spinBars, true);
86        mMiddleArrowSize = typedArray
87                .getDimension(R.styleable.DrawerArrowToggle_middleBarArrowSize, 0);
88        final int remainingSpace = (int) (mSize - mBarThickness * 3 - mBarGap * 2);
89        mCenterOffset = (remainingSpace / 4) * 2; //making sure it is a multiple of 2.
90        mCenterOffset += mBarThickness * 1.5 + mBarGap;
91        typedArray.recycle();
92
93        mPaint.setStyle(Paint.Style.STROKE);
94        mPaint.setStrokeJoin(Paint.Join.MITER);
95        mPaint.setStrokeCap(Paint.Cap.BUTT);
96        mPaint.setStrokeWidth(mBarThickness);
97
98        mMaxCutForBarSize = (float) (mBarThickness / 2 * Math.cos(ARROW_HEAD_ANGLE));
99    }
100
101    abstract boolean isLayoutRtl();
102
103    /**
104     * If set, canvas is flipped when progress reached to end and going back to start.
105     */
106    protected void setVerticalMirror(boolean verticalMirror) {
107        mVerticalMirror = verticalMirror;
108    }
109
110    @Override
111    public void draw(Canvas canvas) {
112        Rect bounds = getBounds();
113        final boolean isRtl = isLayoutRtl();
114        // Interpolated widths of arrow bars
115        final float arrowSize = lerp(mBarSize, mTopBottomArrowSize, mProgress);
116        final float middleBarSize = lerp(mBarSize, mMiddleArrowSize, mProgress);
117        // Interpolated size of middle bar
118        final float middleBarCut = Math.round(lerp(0, mMaxCutForBarSize, mProgress));
119        // The rotation of the top and bottom bars (that make the arrow head)
120        final float rotation = lerp(0, ARROW_HEAD_ANGLE, mProgress);
121
122        // The whole canvas rotates as the transition happens
123        final float canvasRotate = lerp(isRtl ? 0 : -180, isRtl ? 180 : 0, mProgress);
124        final float arrowWidth = Math.round(arrowSize * Math.cos(rotation));
125        final float arrowHeight = Math.round(arrowSize * Math.sin(rotation));
126
127
128        mPath.rewind();
129        final float topBottomBarOffset = lerp(mBarGap + mBarThickness, -mMaxCutForBarSize,
130                mProgress);
131
132        final float arrowEdge = -middleBarSize / 2;
133        // draw middle bar
134        mPath.moveTo(arrowEdge + middleBarCut, 0);
135        mPath.rLineTo(middleBarSize - middleBarCut * 2, 0);
136
137        // bottom bar
138        mPath.moveTo(arrowEdge, topBottomBarOffset);
139        mPath.rLineTo(arrowWidth, arrowHeight);
140
141        // top bar
142        mPath.moveTo(arrowEdge, -topBottomBarOffset);
143        mPath.rLineTo(arrowWidth, -arrowHeight);
144
145        mPath.close();
146
147        canvas.save();
148        // Rotate the whole canvas if spinning, if not, rotate it 180 to get
149        // the arrow pointing the other way for RTL.
150        canvas.translate(bounds.centerX(), mCenterOffset);
151        if (mSpin) {
152            canvas.rotate(canvasRotate * ((mVerticalMirror ^ isRtl) ? -1 : 1));
153        } else if (isRtl) {
154            canvas.rotate(180);
155        }
156        canvas.drawPath(mPath, mPaint);
157
158        canvas.restore();
159    }
160
161    @Override
162    public void setAlpha(int i) {
163        mPaint.setAlpha(i);
164    }
165
166    // override
167    public boolean isAutoMirrored() {
168        // Draws rotated 180 degrees in RTL mode.
169        return true;
170    }
171
172    @Override
173    public void setColorFilter(ColorFilter colorFilter) {
174        mPaint.setColorFilter(colorFilter);
175    }
176
177    @Override
178    public int getIntrinsicHeight() {
179        return mSize;
180    }
181
182    @Override
183    public int getIntrinsicWidth() {
184        return mSize;
185    }
186
187    @Override
188    public int getOpacity() {
189        return PixelFormat.TRANSLUCENT;
190    }
191
192    public float getProgress() {
193        return mProgress;
194    }
195
196    public void setProgress(float progress) {
197        mProgress = progress;
198        invalidateSelf();
199    }
200
201    /**
202     * Linear interpolate between a and b with parameter t.
203     */
204    private static float lerp(float a, float b, float t) {
205        return a + (b - a) * t;
206    }
207}