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
17package android.support.design.widget;
18
19import android.content.res.ColorStateList;
20import android.graphics.Canvas;
21import android.graphics.ColorFilter;
22import android.graphics.LinearGradient;
23import android.graphics.Paint;
24import android.graphics.PixelFormat;
25import android.graphics.Rect;
26import android.graphics.RectF;
27import android.graphics.Shader;
28import android.graphics.drawable.Drawable;
29import android.support.v4.graphics.ColorUtils;
30
31/**
32 * A drawable which draws an oval 'border'.
33 */
34class 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
61    public CircularBorderDrawable() {
62        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
63        mPaint.setStyle(Paint.Style.STROKE);
64    }
65
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     */
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
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
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
120    public void setAlpha(int alpha) {
121        mPaint.setAlpha(alpha);
122        invalidateSelf();
123    }
124
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
135    public void setColorFilter(ColorFilter colorFilter) {
136        mPaint.setColorFilter(colorFilter);
137        invalidateSelf();
138    }
139
140    @Override
141    public int getOpacity() {
142        return mBorderWidth > 0 ? PixelFormat.TRANSLUCENT : PixelFormat.TRANSPARENT;
143    }
144
145    final void setRotation(float rotation) {
146        if (rotation != mRotation) {
147            mRotation = rotation;
148            invalidateSelf();
149        }
150    }
151
152    @Override
153    protected void onBoundsChange(Rect bounds) {
154        mInvalidateShader = true;
155    }
156
157    @Override
158    public boolean isStateful() {
159        return (mBorderTint != null && mBorderTint.isStateful()) || super.isStateful();
160    }
161
162    @Override
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     */
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