/* * Copyright (C) 2006 The Android Open Source Project * * Changes to accomodate repurposing for CircularProgressBarDrawable * Copyright (C) 2014 Chiller Labs * * 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 com.chillerlabs.circularprogressbar; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.SweepGradient; import android.graphics.drawable.Drawable; public class CircularProgressBarDrawable extends Drawable { private CircularProgressBarState mCircularProgressBarState; private final Paint mFillPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private ColorFilter mColorFilter; // optional, set by the caller private int mAlpha = 0xFF; // modified by the caller private final RectF mRect = new RectF(); private boolean mRectIsDirty; // internal state private boolean mMutated; private Path mRingPath; private boolean mPathIsDirty = true; private float startingAngle; public void setThicknessRatio(float thicknessRatio) { mCircularProgressBarState.mThicknessRatio = thicknessRatio; } public void setUseLevel(boolean useLevel) { mCircularProgressBarState.mUseLevel = useLevel; } public void setStartingAngle(float startingAngle) { this.startingAngle = startingAngle; } public CircularProgressBarDrawable() { this(new CircularProgressBarState((int[]) null)); setUseLevel(true); setThicknessRatio(8); mRectIsDirty = true; invalidateSelf(); setStartingAngle(90); } private int modulateAlpha(int alpha) { int scale = mAlpha + (mAlpha >> 7); return alpha * scale >> 8; } /** * <p>Sets the colors used to draw the gradient. Each color is specified as an * ARGB integer and the array must contain at least 2 colors.</p> * <p><strong>Note</strong>: changing orientation will affect all instances * of a drawable loaded from a resource. It is recommended to invoke * {@link #mutate()} before changing the orientation.</p> * * @param colors 2 or more ARGB colors * * @see #mutate() * @see #setColor(int) */ public void setColors(int[] colors) { mCircularProgressBarState.setColors(colors); mRectIsDirty = true; invalidateSelf(); } public void setColorResources(Resources resources, int... colorResources) { int[] colors = new int[colorResources.length]; for (int i = 0; i < colorResources.length; i++) { colors[i] = resources.getColor(colorResources[i]); } setColors(colors); } @Override public void draw(Canvas canvas) { if (!ensureValidRect()) { // nothing to draw return; } // remember the alpha values, in case we temporarily overwrite them // when we modulate them with mAlpha final int prevFillAlpha = mFillPaint.getAlpha(); // compute the modulate alpha values final int currFillAlpha = modulateAlpha(prevFillAlpha); final CircularProgressBarState st = mCircularProgressBarState; /* Drawing with a layer is slower than direct drawing, but it allows us to apply paint effects like alpha and colorfilter to the result of multiple separate draws. In our case, if the user asks for a non-opaque alpha value (via setAlpha), and we're stroking, then we need to apply the alpha AFTER we've drawn both the fill and the stroke. */ /* since we're not using a layer, apply the dither/filter to our individual paints */ mFillPaint.setAlpha(currFillAlpha); mFillPaint.setColorFilter(mColorFilter); if (mColorFilter != null && !mCircularProgressBarState.mHasSolidColor) { mFillPaint.setColor(mAlpha << 24); } Path path = buildRing(st); canvas.drawPath(path, mFillPaint); mFillPaint.setAlpha(prevFillAlpha); } private Path buildRing(CircularProgressBarState st) { if (mRingPath != null && (!st.mUseLevel || !mPathIsDirty)) return mRingPath; mPathIsDirty = false; float sweep = st.mUseLevel ? (360.0f * getLevel() / 10000.0f) : 360f; RectF bounds = new RectF(mRect); float x = bounds.width() / 2.0f; float y = bounds.height() / 2.0f; float thickness = bounds.width() / st.mThicknessRatio; float innerRadius = (bounds.width() / 2) - thickness; RectF innerBounds = new RectF(bounds); innerBounds.inset(x - innerRadius, y - innerRadius); bounds = new RectF(innerBounds); bounds.inset(-thickness, -thickness); if (mRingPath == null) { mRingPath = new Path(); } else { mRingPath.reset(); } final Path ringPath = mRingPath; // arcTo treats the sweep angle mod 360, so check for that, since we // think 360 means draw the entire oval if (Math.abs(sweep) < 360) { ringPath.setFillType(Path.FillType.EVEN_ODD); double startingAngleRadians = Math.toRadians(startingAngle); final float startingAngleCosine = (float) Math.cos(startingAngleRadians); final float startingAngleSine = (float) Math.sin(startingAngleRadians) * -1; // inner top final float innerX = startingAngleCosine * innerRadius + x; final float innerY = startingAngleSine * innerRadius + y; ringPath.moveTo(innerX, innerY); // outer top final float outerX = startingAngleCosine * (innerRadius + thickness) + x; final float outerY = startingAngleSine * (innerRadius + thickness) + y; ringPath.lineTo(outerX, outerY); // outer arc ringPath.arcTo(bounds, -startingAngle, -sweep, false); // inner arc ringPath.arcTo(innerBounds, -startingAngle - sweep, sweep, false); ringPath.close(); } else { // add the entire ovals ringPath.addOval(bounds, Path.Direction.CW); ringPath.addOval(innerBounds, Path.Direction.CCW); } return ringPath; } /** * <p>Changes this drawbale to use a single color instead of a gradient.</p> * <p><strong>Note</strong>: changing color will affect all instances * of a drawable loaded from a resource. It is recommended to invoke * {@link #mutate()} before changing the color.</p> * * @param argb The color used to fill the shape * * @see #mutate() * @see #setColors(int[]) */ public void setColor(int argb) { mCircularProgressBarState.setSolidColor(argb); mFillPaint.setColor(argb); invalidateSelf(); } public void setColorResource(Resources resources, int color) { setColor(resources.getColor(color)); } @Override public int getChangingConfigurations() { return super.getChangingConfigurations() | mCircularProgressBarState.mChangingConfigurations; } @Override public void setAlpha(int alpha) { if (alpha != mAlpha) { mAlpha = alpha; invalidateSelf(); } } @Override public int getAlpha() { return mAlpha; } @Override public void setColorFilter(ColorFilter cf) { if (cf != mColorFilter) { mColorFilter = cf; invalidateSelf(); } } @Override public int getOpacity() { return PixelFormat.TRANSLUCENT; } @Override protected void onBoundsChange(Rect r) { super.onBoundsChange(r); mRingPath = null; mPathIsDirty = true; mRectIsDirty = true; } @Override protected boolean onLevelChange(int level) { super.onLevelChange(level); mRectIsDirty = true; mPathIsDirty = true; invalidateSelf(); return true; } /** * This checks mRectIsDirty, and if it is true, recomputes both our drawing * rectangle (mRect) and the gradient itself, since it depends on our * rectangle too. * @return true if the resulting rectangle is not empty, false otherwise */ private boolean ensureValidRect() { if (mRectIsDirty) { mRectIsDirty = false; Rect bounds = getBounds(); float inset = 0; final CircularProgressBarState st = mCircularProgressBarState; mRect.set(bounds.left + inset, bounds.top + inset, bounds.right - inset, bounds.bottom - inset); final int[] colors = st.mColors; if (colors != null) { RectF r = mRect; float x0, y0; x0 = r.left + (r.right - r.left) * st.mCenterX; y0 = r.top + (r.bottom - r.top) * st.mCenterY; final SweepGradient sweepGradient = new SweepGradient(x0, y0, colors, null); Matrix flipMatrix = new Matrix(); flipMatrix.setScale(1, -1); flipMatrix.postTranslate(0, (r.bottom - r.top)); flipMatrix.postRotate(-startingAngle, x0, y0); sweepGradient.setLocalMatrix(flipMatrix); mFillPaint.setShader(sweepGradient); // If we don't have a solid color, the alpha channel must be // maxed out so that alpha modulation works correctly. if (!st.mHasSolidColor) { mFillPaint.setColor(Color.BLACK); } } } return !mRect.isEmpty(); } @Override public int getIntrinsicWidth() { return mCircularProgressBarState.mWidth; } @Override public int getIntrinsicHeight() { return mCircularProgressBarState.mHeight; } @Override public ConstantState getConstantState() { mCircularProgressBarState.mChangingConfigurations = getChangingConfigurations(); return mCircularProgressBarState; } @Override public Drawable mutate() { if (!mMutated && super.mutate() == this) { mCircularProgressBarState = new CircularProgressBarState(mCircularProgressBarState); initializeWithState(mCircularProgressBarState); mMutated = true; } return this; } public static class CircularProgressBarState extends ConstantState { public int mChangingConfigurations; public int[] mColors; public float[] mPositions; public boolean mHasSolidColor; public int mSolidColor; public Rect mPadding; public int mWidth = -1; public int mHeight = -1; public float mThicknessRatio; private float mCenterX = 0.5f; private float mCenterY = 0.5f; private float mGradientRadius = 0.5f; private boolean mUseLevel; CircularProgressBarState(int[] colors) { setColors(colors); } public CircularProgressBarState(CircularProgressBarState state) { mChangingConfigurations = state.mChangingConfigurations; if (state.mColors != null) { mColors = state.mColors.clone(); } if (state.mPositions != null) { mPositions = state.mPositions.clone(); } mHasSolidColor = state.mHasSolidColor; mSolidColor = state.mSolidColor; if (state.mPadding != null) { mPadding = new Rect(state.mPadding); } mWidth = state.mWidth; mHeight = state.mHeight; mThicknessRatio = state.mThicknessRatio; mCenterX = state.mCenterX; mCenterY = state.mCenterY; mGradientRadius = state.mGradientRadius; mUseLevel = state.mUseLevel; } @Override public Drawable newDrawable() { return new CircularProgressBarDrawable(this); } @Override public Drawable newDrawable(Resources res) { return new CircularProgressBarDrawable(this); } @Override public int getChangingConfigurations() { return mChangingConfigurations; } public void setColors(int[] colors) { mHasSolidColor = false; mColors = colors; } public void setSolidColor(int argb) { mHasSolidColor = true; mSolidColor = argb; mColors = null; } } private CircularProgressBarDrawable(CircularProgressBarState state) { mCircularProgressBarState = state; initializeWithState(state); mRectIsDirty = true; mMutated = false; } private void initializeWithState(CircularProgressBarState state) { if (state.mHasSolidColor) { mFillPaint.setColor(state.mSolidColor); } else if (state.mColors == null) { // If we don't have a solid color and we don't have a gradient, // the app is stroking the shape, set the color to the default // value of state.mSolidColor mFillPaint.setColor(0); } else { // Otherwise, make sure the fill alpha is maxed out. mFillPaint.setColor(Color.BLACK); } } }