1 /* 2 * Copyright (C) 2015 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.design.widget; 18 19 import android.content.res.ColorStateList; 20 import android.graphics.Canvas; 21 import android.graphics.ColorFilter; 22 import android.graphics.LinearGradient; 23 import android.graphics.Paint; 24 import android.graphics.PixelFormat; 25 import android.graphics.Rect; 26 import android.graphics.RectF; 27 import android.graphics.Shader; 28 import android.graphics.drawable.Drawable; 29 import android.support.v4.graphics.ColorUtils; 30 31 /** 32 * A drawable which draws an oval 'border'. 33 */ 34 class CircularBorderDrawable extends Drawable { 35 36 /** 37 * We actually draw the stroke wider than the border size given. This is to reduce any 38 * potential transparent space caused by anti-aliasing and padding rounding. 39 * This value defines the multiplier used to determine to draw stroke width. 40 */ 41 private static final float DRAW_STROKE_WIDTH_MULTIPLE = 1.3333f; 42 43 final Paint mPaint; 44 final Rect mRect = new Rect(); 45 final RectF mRectF = new RectF(); 46 47 float mBorderWidth; 48 49 private int mTopOuterStrokeColor; 50 private int mTopInnerStrokeColor; 51 private int mBottomOuterStrokeColor; 52 private int mBottomInnerStrokeColor; 53 54 private ColorStateList mBorderTint; 55 private int mCurrentBorderTintColor; 56 57 private boolean mInvalidateShader = true; 58 59 private float mRotation; 60 CircularBorderDrawable()61 public CircularBorderDrawable() { 62 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 63 mPaint.setStyle(Paint.Style.STROKE); 64 } 65 setGradientColors(int topOuterStrokeColor, int topInnerStrokeColor, int bottomOuterStrokeColor, int bottomInnerStrokeColor)66 void setGradientColors(int topOuterStrokeColor, int topInnerStrokeColor, 67 int bottomOuterStrokeColor, int bottomInnerStrokeColor) { 68 mTopOuterStrokeColor = topOuterStrokeColor; 69 mTopInnerStrokeColor = topInnerStrokeColor; 70 mBottomOuterStrokeColor = bottomOuterStrokeColor; 71 mBottomInnerStrokeColor = bottomInnerStrokeColor; 72 } 73 74 /** 75 * Set the border width 76 */ setBorderWidth(float width)77 void setBorderWidth(float width) { 78 if (mBorderWidth != width) { 79 mBorderWidth = width; 80 mPaint.setStrokeWidth(width * DRAW_STROKE_WIDTH_MULTIPLE); 81 mInvalidateShader = true; 82 invalidateSelf(); 83 } 84 } 85 86 @Override draw(Canvas canvas)87 public void draw(Canvas canvas) { 88 if (mInvalidateShader) { 89 mPaint.setShader(createGradientShader()); 90 mInvalidateShader = false; 91 } 92 93 final float halfBorderWidth = mPaint.getStrokeWidth() / 2f; 94 final RectF rectF = mRectF; 95 96 // We need to inset the oval bounds by half the border width. This is because stroke draws 97 // the center of the border on the dimension. Whereas we want the stroke on the inside. 98 copyBounds(mRect); 99 rectF.set(mRect); 100 rectF.left += halfBorderWidth; 101 rectF.top += halfBorderWidth; 102 rectF.right -= halfBorderWidth; 103 rectF.bottom -= halfBorderWidth; 104 105 canvas.save(); 106 canvas.rotate(mRotation, rectF.centerX(), rectF.centerY()); 107 // Draw the oval 108 canvas.drawOval(rectF, mPaint); 109 canvas.restore(); 110 } 111 112 @Override getPadding(Rect padding)113 public boolean getPadding(Rect padding) { 114 final int borderWidth = Math.round(mBorderWidth); 115 padding.set(borderWidth, borderWidth, borderWidth, borderWidth); 116 return true; 117 } 118 119 @Override setAlpha(int alpha)120 public void setAlpha(int alpha) { 121 mPaint.setAlpha(alpha); 122 invalidateSelf(); 123 } 124 setBorderTint(ColorStateList tint)125 void setBorderTint(ColorStateList tint) { 126 if (tint != null) { 127 mCurrentBorderTintColor = tint.getColorForState(getState(), mCurrentBorderTintColor); 128 } 129 mBorderTint = tint; 130 mInvalidateShader = true; 131 invalidateSelf(); 132 } 133 134 @Override setColorFilter(ColorFilter colorFilter)135 public void setColorFilter(ColorFilter colorFilter) { 136 mPaint.setColorFilter(colorFilter); 137 invalidateSelf(); 138 } 139 140 @Override getOpacity()141 public int getOpacity() { 142 return mBorderWidth > 0 ? PixelFormat.TRANSLUCENT : PixelFormat.TRANSPARENT; 143 } 144 setRotation(float rotation)145 final void setRotation(float rotation) { 146 if (rotation != mRotation) { 147 mRotation = rotation; 148 invalidateSelf(); 149 } 150 } 151 152 @Override onBoundsChange(Rect bounds)153 protected void onBoundsChange(Rect bounds) { 154 mInvalidateShader = true; 155 } 156 157 @Override isStateful()158 public boolean isStateful() { 159 return (mBorderTint != null && mBorderTint.isStateful()) || super.isStateful(); 160 } 161 162 @Override onStateChange(int[] state)163 protected boolean onStateChange(int[] state) { 164 if (mBorderTint != null) { 165 final int newColor = mBorderTint.getColorForState(state, mCurrentBorderTintColor); 166 if (newColor != mCurrentBorderTintColor) { 167 mInvalidateShader = true; 168 mCurrentBorderTintColor = newColor; 169 } 170 } 171 if (mInvalidateShader) { 172 invalidateSelf(); 173 } 174 return mInvalidateShader; 175 } 176 177 /** 178 * Creates a vertical {@link LinearGradient} 179 * @return 180 */ createGradientShader()181 private Shader createGradientShader() { 182 final Rect rect = mRect; 183 copyBounds(rect); 184 185 final float borderRatio = mBorderWidth / rect.height(); 186 187 final int[] colors = new int[6]; 188 colors[0] = ColorUtils.compositeColors(mTopOuterStrokeColor, mCurrentBorderTintColor); 189 colors[1] = ColorUtils.compositeColors(mTopInnerStrokeColor, mCurrentBorderTintColor); 190 colors[2] = ColorUtils.compositeColors( 191 ColorUtils.setAlphaComponent(mTopInnerStrokeColor, 0), mCurrentBorderTintColor); 192 colors[3] = ColorUtils.compositeColors( 193 ColorUtils.setAlphaComponent(mBottomInnerStrokeColor, 0), mCurrentBorderTintColor); 194 colors[4] = ColorUtils.compositeColors(mBottomInnerStrokeColor, mCurrentBorderTintColor); 195 colors[5] = ColorUtils.compositeColors(mBottomOuterStrokeColor, mCurrentBorderTintColor); 196 197 final float[] positions = new float[6]; 198 positions[0] = 0f; 199 positions[1] = borderRatio; 200 positions[2] = 0.5f; 201 positions[3] = 0.5f; 202 positions[4] = 1f - borderRatio; 203 positions[5] = 1f; 204 205 return new LinearGradient( 206 0, rect.top, 207 0, rect.bottom, 208 colors, positions, 209 Shader.TileMode.CLAMP); 210 } 211 } 212