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