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