/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.support.design.widget; import android.content.res.ColorStateList; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.LinearGradient; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Shader; import android.graphics.drawable.Drawable; import android.support.v4.graphics.ColorUtils; /** * A drawable which draws an oval 'border'. */ class CircularBorderDrawable extends Drawable { /** * We actually draw the stroke wider than the border size given. This is to reduce any * potential transparent space caused by anti-aliasing and padding rounding. * This value defines the multiplier used to determine to draw stroke width. */ private static final float DRAW_STROKE_WIDTH_MULTIPLE = 1.3333f; final Paint mPaint; final Rect mRect = new Rect(); final RectF mRectF = new RectF(); float mBorderWidth; private int mTopOuterStrokeColor; private int mTopInnerStrokeColor; private int mBottomOuterStrokeColor; private int mBottomInnerStrokeColor; private ColorStateList mBorderTint; private int mCurrentBorderTintColor; private boolean mInvalidateShader = true; private float mRotation; public CircularBorderDrawable() { mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setStyle(Paint.Style.STROKE); } void setGradientColors(int topOuterStrokeColor, int topInnerStrokeColor, int bottomOuterStrokeColor, int bottomInnerStrokeColor) { mTopOuterStrokeColor = topOuterStrokeColor; mTopInnerStrokeColor = topInnerStrokeColor; mBottomOuterStrokeColor = bottomOuterStrokeColor; mBottomInnerStrokeColor = bottomInnerStrokeColor; } /** * Set the border width */ void setBorderWidth(float width) { if (mBorderWidth != width) { mBorderWidth = width; mPaint.setStrokeWidth(width * DRAW_STROKE_WIDTH_MULTIPLE); mInvalidateShader = true; invalidateSelf(); } } @Override public void draw(Canvas canvas) { if (mInvalidateShader) { mPaint.setShader(createGradientShader()); mInvalidateShader = false; } final float halfBorderWidth = mPaint.getStrokeWidth() / 2f; final RectF rectF = mRectF; // We need to inset the oval bounds by half the border width. This is because stroke draws // the center of the border on the dimension. Whereas we want the stroke on the inside. copyBounds(mRect); rectF.set(mRect); rectF.left += halfBorderWidth; rectF.top += halfBorderWidth; rectF.right -= halfBorderWidth; rectF.bottom -= halfBorderWidth; canvas.save(); canvas.rotate(mRotation, rectF.centerX(), rectF.centerY()); // Draw the oval canvas.drawOval(rectF, mPaint); canvas.restore(); } @Override public boolean getPadding(Rect padding) { final int borderWidth = Math.round(mBorderWidth); padding.set(borderWidth, borderWidth, borderWidth, borderWidth); return true; } @Override public void setAlpha(int alpha) { mPaint.setAlpha(alpha); invalidateSelf(); } void setBorderTint(ColorStateList tint) { if (tint != null) { mCurrentBorderTintColor = tint.getColorForState(getState(), mCurrentBorderTintColor); } mBorderTint = tint; mInvalidateShader = true; invalidateSelf(); } @Override public void setColorFilter(ColorFilter colorFilter) { mPaint.setColorFilter(colorFilter); invalidateSelf(); } @Override public int getOpacity() { return mBorderWidth > 0 ? PixelFormat.TRANSLUCENT : PixelFormat.TRANSPARENT; } final void setRotation(float rotation) { if (rotation != mRotation) { mRotation = rotation; invalidateSelf(); } } @Override protected void onBoundsChange(Rect bounds) { mInvalidateShader = true; } @Override public boolean isStateful() { return (mBorderTint != null && mBorderTint.isStateful()) || super.isStateful(); } @Override protected boolean onStateChange(int[] state) { if (mBorderTint != null) { final int newColor = mBorderTint.getColorForState(state, mCurrentBorderTintColor); if (newColor != mCurrentBorderTintColor) { mInvalidateShader = true; mCurrentBorderTintColor = newColor; } } if (mInvalidateShader) { invalidateSelf(); } return mInvalidateShader; } /** * Creates a vertical {@link LinearGradient} * @return */ private Shader createGradientShader() { final Rect rect = mRect; copyBounds(rect); final float borderRatio = mBorderWidth / rect.height(); final int[] colors = new int[6]; colors[0] = ColorUtils.compositeColors(mTopOuterStrokeColor, mCurrentBorderTintColor); colors[1] = ColorUtils.compositeColors(mTopInnerStrokeColor, mCurrentBorderTintColor); colors[2] = ColorUtils.compositeColors( ColorUtils.setAlphaComponent(mTopInnerStrokeColor, 0), mCurrentBorderTintColor); colors[3] = ColorUtils.compositeColors( ColorUtils.setAlphaComponent(mBottomInnerStrokeColor, 0), mCurrentBorderTintColor); colors[4] = ColorUtils.compositeColors(mBottomInnerStrokeColor, mCurrentBorderTintColor); colors[5] = ColorUtils.compositeColors(mBottomOuterStrokeColor, mCurrentBorderTintColor); final float[] positions = new float[6]; positions[0] = 0f; positions[1] = borderRatio; positions[2] = 0.5f; positions[3] = 0.5f; positions[4] = 1f - borderRatio; positions[5] = 1f; return new LinearGradient( 0, rect.top, 0, rect.bottom, colors, positions, Shader.TileMode.CLAMP); } }