一个自定义的圆形颜色渐变进度条
首先,先上效果图,有图有真相
自定义基础的知识就不讲了,各路大神都有说明,下面直接说思路和核心的代码
-
思路
观察view,都是需要canvas去画。大概分为4部分
- 内圆
- 圆弧背景
- 圆弧
- 发光小圆
上代码,每个view都用一个Paint去画,定义4个Paint,初始化Paint属性,属性不明白的童鞋可以去百度。主要就是设置颜色,画笔宽度,画笔风格,是否抗锯齿。
/**
* 初始化画笔
*/
private void initPaint() {
// 内圆
mCirclePaint = new Paint();
mCirclePaint.setAntiAlias(true);
mCirclePaint.setColor(Color.BLUE);
mCirclePaint.setStyle(Paint.Style.FILL);
// 圆弧背景
mBgArcPaint = new Paint();
mBgArcPaint.setAntiAlias(true);
mBgArcPaint.setColor(Color.WHITE);
mBgArcPaint.setStyle(Paint.Style.STROKE);
mBgArcPaint.setStrokeWidth(mBgArcWidth);
// 圆弧
mArcPaint = new Paint();
mArcPaint.setAntiAlias(true);
mArcPaint.setStyle(Paint.Style.STROKE);
mArcPaint.setStrokeWidth(mArcWidth);
mArcPaint.setStrokeCap(Paint.Cap.ROUND);
// 发光小圆
mSmallCirclePaint = new Paint();
mSmallCirclePaint.setAntiAlias(true);
mSmallCirclePaint.setColor(getResources().getColor(R.color.color_7cffff));
mSmallCirclePaint.setStyle(Paint.Style.FILL);
mSmallCirclePaint.setMaskFilter(new BlurMaskFilter(mSmallRadius / 2, BlurMaskFilter.Blur.SOLID));
}
-
onSizeChange()
在onSizeChange() 中,需要计算所画view的一些属性,例如圆心,半径和弧的边界等,
画圆,需要有圆心坐标,半径,画弧需要圆心坐标,所画弧的角度,弧边界,我们这个例子就是用的这种方法。
废话少说,上代码解析
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//1.求圆弧和背景圆弧的最大宽度
float maxArcWidth = Math.max(mArcWidth, mBgArcWidth);
//2.求最小值作为实际值,这是直径
int minSize = Math.min(w - getPaddingLeft() - getPaddingRight() - 2 * (int) maxArcWidth,
h - getPaddingTop() - getPaddingBottom() - 2 * (int) maxArcWidth);
//3.内圆半径: 进度圆环内半径 - 中间空余距离 = 内圆半径
mRadius = minSize / 2 - 28;
//4.获取圆的相关参数
mCenterPoint.x = w / 2;
mCenterPoint.y = h / 2;
//5.绘制进度圆弧的边界
mRectF.left = mCenterPoint.x - mRadius - maxArcWidth / 2;
mRectF.top = mCenterPoint.y - mRadius - maxArcWidth / 2;
mRectF.right = mCenterPoint.x + mRadius + maxArcWidth / 2;
mRectF.bottom = mCenterPoint.y + mRadius + maxArcWidth / 2;
//6.绘制背景圆弧的边界
mBgRectF.left = mCenterPoint.x - mRadius - (mArcWidth - mBgArcWidth) - mBgArcWidth / 2;
mBgRectF.top = mCenterPoint.y - mRadius - (mArcWidth - mBgArcWidth) - mBgArcWidth / 2;
mBgRectF.right = mCenterPoint.x + mRadius + (mArcWidth - mBgArcWidth) + mBgArcWidth / 2;
mBgRectF.bottom = mCenterPoint.y + mRadius + (mArcWidth - mBgArcWidth) + mBgArcWidth / 2;
// 7.设置渐变
updateArcPaint();
}
- 由于圆弧背景和进度圆弧的宽度不一样,所以求出最大值,用来计算对应圆弧的边界。
- 根据onSizeChange()得到view的宽高,然后算出我们可利用的view宽度最大值。
- minSize / 2 得到的是最大半径,由于内圆不会占满view,所以减去内圆与view宽度的空隙,得到内圆半径。
- view是个正方形,将中心作为我们的圆心,也是画弧的圆心。
- mRectF作为圆弧的边界时,left,top,right,bottom所指的是圆弧宽度的中心点,由于圆弧有一定的宽度,所以,需要将mRectF的值加上或者减去圆弧宽度的一半,我们这里用圆心计算,减去内圆半径,再减(加)上进度弧的宽度。
- mArcWidth - mBgArcWidth 为进度圆弧和背景圆弧中间的空白间隙,减去这个间隙,然后再减去(加)背景圆弧的一半,即可得到背景圆弧的边界。
- 用来设置画笔的渐变颜色
上渐变代码
private void updateArcPaint() {
// 设置渐变
int[] mGradientColors = {getResources().getColor(R.color.color_start), getResources().getColor(R.color.color_end)};
float[] positions = {0f, 0.5f};
mSweepGradient = new SweepGradient(mCenterPoint.x, mCenterPoint.y, mGradientColors, positions);
}
就一个圆形渐变的一个构造方法,设置颜色的int[], 设置颜色变化位置,值为从0f-1.0f , positions可以为null,默认均分渐变。
-
onDraw()
好了,重头戏已经结束了,剩下的就是用canvas进行绘制了,调用对应的方法即可
对了,还有一个需要注意的,由于画布的0°,是从3点钟方向开始的,不是从12点开始,所以需要旋转画布。
旋转94度,是因为画笔是头尾都是圆弧状,如果从90开始,看起来会有点歪。
// 逆时针旋转94度
canvas.rotate(mStartAngle, mCenterPoint.x, mCenterPoint.y);
画内圆
// 画内圆
canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, mRadius, mCirclePaint);
画背景弧
// 圆环背景
canvas.drawArc(mBgRectF, 0, 360, false, mBgArcPaint);
画进度弧
// 设置渐变
mArcPaint.setShader(mSweepGradient);
float currentAngle = finalCurrentValue * 1.0f / mMaxValue * 360;
// +4 是因为绘制的时候出现了圆弧起点有尾巴的问题
canvas.drawArc(mRectF, 4, currentAngle, false, mArcPaint);
最后一个画发光小圆需要注意一下,这里的算法是根据当前旋转角度和半径来计算圆上一点的坐标的,这个不知道如何计算的,应该是知识都还给数学老师了,童鞋们可以某度一下计算圆上一点坐标的公式。知道坐标之后,计算出与圆心的距离,然后画出。
// 设置发光的圆
setLayerType(LAYER_TYPE_SOFTWARE, null);
// 计算小圆距离中心点的距离
float tempRadius = mRadius + mArcWidth / 2 + 1;
// 根据求圆上一点的方式,求出圆上的点相对于圆心的距离
if (currentAngle >= 360) currentAngle = 358;
float y1 = tempRadius * (float) Math.sin((currentAngle + 4) * Math.PI / 180);
float x1 = tempRadius * (float) Math.cos((currentAngle + 4) * Math.PI / 180);
// 算出小圆圆心坐标,根据此坐标,画出小圆
canvas.drawCircle(mCenterPoint.x + x1, mCenterPoint.y + y1, mSmallRadius, mSmallCirclePaint);
- 动态画弧
最后再讲一下动态画弧吧
正常思维下,一般有两种,一种是开子线程,不断改变需要绘制的圆弧,然后延时,达到动态变化的目的。
另一种是利用差值器,系统计算出不同的值,然后来自己改变,自己无需开子线程,只需要设置持续时间和最大值即可。
private void startAnimator() {
ValueAnimator animator = ValueAnimator.ofInt(0, mCurrentValue);
animator.setDuration(1000);
final int finalTempCurrentValue = mCurrentValue;
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
finalCurrentValue = (int) (animation.getAnimatedValue());
isRun = finalCurrentValue != finalTempCurrentValue;
if (finalCurrentValue > tempValue) {
tempValue = finalCurrentValue;
invalidate();
}
}
});
animator.start();
}
finalCurrentValue即为产生的值,作为圆弧进度值,不断绘制view。其他的代码就是一些健壮性的判断啦,比如动画持续中不能重新开始,每次设定需要重新开始等等。
Finally,附上view完整代码,最后啰嗦一句,为了简单省事,这里并没有自定义属性,如果有需要的朋友可以自定义属性,需要自己设定背景弧和进度弧宽度,渐变颜色等属性的朋友,可以自己添加getset方法。
初次创作,如有错误,请多指教。
工程下载地址:https://download.csdn.net/download/u010017719/10741645
CircleProgressView完整代码
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.BlurMaskFilter;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.RectF;
import android.graphics.SweepGradient;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
/**
* Created by schema on 2018/3/12.
*/
public class CircleProgressView extends View {
private static final String TAG = "CircleProgressView";
/**
* 内圆半径
*/
private int mRadius;
/**
* 圆弧宽度
*/
private float mArcWidth = 40;
/**
* 背景弧宽度
*/
private float mBgArcWidth = 20;
/**
* 小圆半径
*/
private float mSmallRadius = 30;
/**
* 圆心点坐标
*/
private Point mCenterPoint = new Point();
/**
* 圆弧边界
*/
private RectF mRectF = new RectF();
private RectF mBgRectF = new RectF();
/**
* 圆弧一周最大值
*/
private int mMaxValue;
/**
* 开始角度
*/
private int mStartAngle = -94;
/**
* 当前值
*/
private int mCurrentValue;
/**
* 圆弧背景画笔
*/
private Paint mBgArcPaint;
/**
* 圆弧画笔
*/
private Paint mArcPaint;
/**
* 内圆画笔
*/
private Paint mCirclePaint;
/**
* 小圆圈画笔
*/
private Paint mSmallCirclePaint;
/**
* 渐变器
*/
private SweepGradient mSweepGradient;
/**
* 当前需要画的进度
*/
private int finalCurrentValue;
/**
* 差值器缓存数
*/
private int tempValue;
/**
* 进度圆环内半径
*/
private int progressInsideRadius;
private boolean isRun = false;
private boolean isNeedToReset = false;
public CircleProgressView(Context context) {
this(context, null);
}
public CircleProgressView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public CircleProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initPaint();
}
private void initPaint() {
// 内圆
mCirclePaint = new Paint();
mCirclePaint.setAntiAlias(true);
mCirclePaint.setColor(Color.BLUE);
mCirclePaint.setStyle(Paint.Style.FILL);
// 圆弧背景
mBgArcPaint = new Paint();
mBgArcPaint.setAntiAlias(true);
mBgArcPaint.setColor(Color.WHITE);
mBgArcPaint.setStyle(Paint.Style.STROKE);
mBgArcPaint.setStrokeWidth(mBgArcWidth);
// 圆弧
mArcPaint = new Paint();
mArcPaint.setAntiAlias(true);
mArcPaint.setStyle(Paint.Style.STROKE);
mArcPaint.setStrokeWidth(mArcWidth);
mArcPaint.setStrokeCap(Paint.Cap.ROUND);
// 发光小圆
mSmallCirclePaint = new Paint();
mSmallCirclePaint.setAntiAlias(true);
mSmallCirclePaint.setColor(getResources().getColor(R.color.color_7cffff));
mSmallCirclePaint.setStyle(Paint.Style.FILL);
mSmallCirclePaint.setMaskFilter(new BlurMaskFilter(mSmallRadius / 2, BlurMaskFilter.Blur.SOLID));
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//求圆弧和背景圆弧的最大宽度
float maxArcWidth = Math.max(mArcWidth, mBgArcWidth);
//求最小值作为实际值,这是直径
int minSize = Math.min(w - getPaddingLeft() - getPaddingRight() - 2 * (int) maxArcWidth,
h - getPaddingTop() - getPaddingBottom() - 2 * (int) maxArcWidth);
//内圆半径: 进度圆环内半径 - 中间空余距离为17px = 内圆半径
mRadius = minSize / 2 - 28;
//获取圆的相关参数
mCenterPoint.x = w / 2;
mCenterPoint.y = h / 2;
//绘制进度圆弧的边界
mRectF.left = mCenterPoint.x - mRadius - maxArcWidth / 2;
mRectF.top = mCenterPoint.y - mRadius - maxArcWidth / 2;
mRectF.right = mCenterPoint.x + mRadius + maxArcWidth / 2;
mRectF.bottom = mCenterPoint.y + mRadius + maxArcWidth / 2;
//绘制背景圆弧的边界
mBgRectF.left = mCenterPoint.x - mRadius - (mArcWidth - mBgArcWidth) - mBgArcWidth / 2;
mBgRectF.top = mCenterPoint.y - mRadius - (mArcWidth - mBgArcWidth) - mBgArcWidth / 2;
mBgRectF.right = mCenterPoint.x + mRadius + (mArcWidth - mBgArcWidth) + mBgArcWidth / 2;
mBgRectF.bottom = mCenterPoint.y + mRadius + (mArcWidth - mBgArcWidth) + mBgArcWidth / 2;
updateArcPaint();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawArc(canvas);
}
private void drawArc(Canvas canvas) {
// 逆时针旋转94度
canvas.rotate(mStartAngle, mCenterPoint.x, mCenterPoint.y);
// 画内圆
canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, mRadius, mCirclePaint);
// 圆环背景
canvas.drawArc(mBgRectF, 0, 360, false, mBgArcPaint);
if (mCurrentValue == 0) {
return;
}
if (isNeedToReset) {
isNeedToReset = false;
tempValue = 0;
startAnimator();
return;
}
// 设置渐变
mArcPaint.setShader(mSweepGradient);
float currentAngle = finalCurrentValue * 1.0f / mMaxValue * 360;
// +4 是因为绘制的时候出现了圆弧起点有尾巴的问题
canvas.drawArc(mRectF, 4, currentAngle, false, mArcPaint);
// 设置发光的圆
setLayerType(LAYER_TYPE_SOFTWARE, null);
// 计算小圆距离中心点的距离
float tempRadius = mRadius + mArcWidth / 2 + 1;
// 根据求圆上一点的方式,求出圆上的点相对于圆心的距离
if (currentAngle >= 360) currentAngle = 358;
float y1 = tempRadius * (float) Math.sin((currentAngle + 4) * Math.PI / 180);
float x1 = tempRadius * (float) Math.cos((currentAngle + 4) * Math.PI / 180);
// 算出小圆圆心坐标,根据此坐标,画出小圆
canvas.drawCircle(mCenterPoint.x + x1, mCenterPoint.y + y1, mSmallRadius, mSmallCirclePaint);
}
private void updateArcPaint() {
// 设置渐变
int[] mGradientColors = {getResources().getColor(R.color.color_start), getResources().getColor(R.color.color_end)};
// 0点钟和9点钟位置
float[] positions = {0f, 0.5f};
mSweepGradient = new SweepGradient(mCenterPoint.x, mCenterPoint.y, mGradientColors, positions);
}
/**
* 开始计算差值器
*/
private void startAnimator() {
ValueAnimator animator = ValueAnimator.ofInt(0, mCurrentValue);
animator.setDuration(1000);
final int finalTempCurrentValue = mCurrentValue;
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
finalCurrentValue = (int) (animation.getAnimatedValue());
isRun = finalCurrentValue != finalTempCurrentValue;
if (finalCurrentValue > tempValue) {
tempValue = finalCurrentValue;
invalidate();
}
}
});
animator.start();
}
private void needToReset() {
isNeedToReset = true;
invalidate();
}
/**
* 设置圆弧当前值
*/
public void setCurrentValue(int mCurrentValue) {
this.mCurrentValue = mCurrentValue;
if (this.mCurrentValue > mMaxValue)
this.mCurrentValue = mMaxValue;
if (isRun) return;
needToReset();
}
/**
* 设置最大值
*/
public void setMaxValue(int mMaxValue) {
this.mMaxValue = mMaxValue;
}
}