blob: 617a5010613152d4f1a91b08ee01fda81e186823 [file] [log] [blame]
/*
* 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);
}
}