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}