如果你想读懂或者更好的理解本篇文章关于自定义圆环或圆弧的内容.请你务必提前阅读下Android自定义View之画圆环(手把手教你如何一步步画圆环).在这篇文章中,详细描述了最基本的自定义圆环的绘制流程以及操作步骤.请务必阅读,不然的话,理解本片文章比较吃力.(嘿嘿,不怕阁下笑话,当初我就是没学会走,就想着跑,后来发现跑偏了…于是从最基本最简单的开始学起).切记,切记,切记,一定要看啊,不然的话,有些基本知识,我是默认你已经知道了哦.
另外,为了保证完整性,下面我会将三种自定义带进度条圆环的整个代码贴出来.可能比较长,请保持耐心.如果你没有耐心,直接拿去用也是可以的.
本篇文章主要来源于Android 自定义 View 之圆形进度条总结 (大神写的很详细)
之所以要重新写一篇.主要给基于原因:
工程包括主程序和其依赖的工程类库.其中三种自定义带进度圆环就是被封装在了工程类库中.
自定义带进度圆环思路主要可以分为以下几步:
1.自定义View属性
2.View 的测量
3.计算绘制 View 所需参数
4.圆弧的绘制及渐变的实现
5.文字的绘制
6.动画效果的实现
切记,接下来提到的三种自定义圆环都基本遵从上述步骤.
接下来,本人将会从三个方面对自定义圆形进度条做讲解:
鉴于接下来自定义圆环的自定义view属性全都统一整合在res/value/attrxml文件中,那么索性统一介绍下.
自定义属性资源文件:attr.xml
<resources>
<attr name="antiAlias" format="boolean" />
<attr name="startAngle" format="float" />
<attr name="sweepAngle" format="float" />
<attr name="animTime" format="integer" />
<attr name="maxValue" format="float" />
<attr name="value" format="float" />
<attr name="unit" format="string|reference" />
<attr name="unitSize" format="dimension" />
<attr name="unitColor" format="color|reference" />
<attr name="hint" format="string|reference" />
<attr name="hintSize" format="dimension" />
<attr name="hintColor" format="color|reference" />
<attr name="precision" format="integer" />
<attr name="valueSize" format="dimension" />
<attr name="valueColor" format="color|reference" />
<attr name="arcColor1" format="color|reference" />
<attr name="arcColor2" format="color|reference" />
<attr name="arcColor3" format="color|reference" />
<attr name="bgArcColor" format="color|reference" />
<attr name="arcWidth" format="dimension" />
<attr name="arcColors" format="color|reference" />
<attr name="textOffsetPercentInRadius" format="float" />
<declare-styleable name="CircleProgressBar">
<attr name="antiAlias" />
<attr name="startAngle" />
<attr name="sweepAngle" />
<attr name="animTime" />
<attr name="maxValue" />
<attr name="value" />
<attr name="precision" />
<attr name="valueSize" />
<attr name="valueColor" />
<attr name="textOffsetPercentInRadius" />
<attr name="hint" />
<attr name="hintSize" />
<attr name="hintColor" />
<attr name="unit" />
<attr name="unitSize" />
<attr name="unitColor" />
<attr name="arcWidth" />
<attr name="arcColors" />
<attr name="bgArcColor" />
<attr name="bgArcWidth" format="dimension" />
declare-styleable>
<declare-styleable name="DialProgress">
<attr name="antiAlias" />
<attr name="startAngle" />
<attr name="sweepAngle" />
<attr name="animTime" />
<attr name="maxValue" />
<attr name="value" />
<attr name="precision" />
<attr name="valueSize" />
<attr name="valueColor" />
<attr name="textOffsetPercentInRadius" />
<attr name="unit" />
<attr name="unitSize" />
<attr name="unitColor" />
<attr name="hint" />
<attr name="hintSize" />
<attr name="hintColor" />
<attr name="arcWidth" />
<attr name="dialWidth" format="dimension|reference" />
<attr name="dialIntervalDegree" format="integer" />
<attr name="arcColors" />
<attr name="bgArcColor" />
<attr name="dialColor" format="color|reference" />
declare-styleable>
<declare-styleable name="WaveProgress">
<attr name="antiAlias" />
<attr name="darkWaveAnimTime" format="integer" />
<attr name="lightWaveAnimTime" format="integer" />
<attr name="maxValue" />
<attr name="value" />
<attr name="valueColor" />
<attr name="valueSize" />
<attr name="hint" />
<attr name="hintSize" />
<attr name="hintColor" />
<attr name="circleWidth" format="dimension" />
<attr name="circleColor" format="color|reference" />
<attr name="bgCircleColor" format="color|reference" />
<attr name="lockWave" format="boolean" />
<attr name="waveNum" format="integer" />
<attr name="waveHeight" format="dimension" />
<attr name="darkWaveColor" format="color|reference" />
<attr name="showLightWave" format="boolean" />
<attr name="lightWaveColor" format="color|reference" />
<attr name="lightWaveDirect" format="enum">
<enum name="L2R" value="0" />
<enum name="R2L" value="1" />
attr>
declare-styleable>
resources>
各种属性都做了详细的注解,相信你能看懂.
既然属性定义好了,那么就要设置属性.没错属性的设置实在布局文件中引用自定义控件的时候调用.
布局文件activity_main,xml
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.littlejie.app.MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<Button
android:id="@+id/btn_reset_all"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/resetStr" />
<com.littlejie.circleprogress.CircleProgress
android:id="@+id/circle_progress_bar1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
app:antiAlias="true"
app:arcColors="@array/gradient_arc_color"
app:arcWidth="@dimen/small"
app:bgArcColor="@color/colorAccent"
app:bgArcWidth="@dimen/small"
app:hint="截止当前已走"
app:hintSize="15sp"
app:maxValue="10000"
app:startAngle="135"
app:sweepAngle="270"
app:unit="步"
app:unitSize="15sp"
app:value="10000"
app:valueSize="25sp" />
<com.littlejie.circleprogress.CircleProgress
android:id="@+id/circle_progress_bar2"
android:layout_width="100dp"
android:layout_height="200dp"
android:layout_gravity="center_horizontal"
app:antiAlias="true"
app:arcWidth="@dimen/small"
app:bgArcColor="@color/colorAccent"
app:bgArcWidth="@dimen/small"
app:hint="百分比"
app:hintSize="@dimen/text_size_15"
app:maxValue="100"
app:startAngle="135"
app:sweepAngle="270"
app:textOffsetPercentInRadius="0.5"
app:unit="%"
app:unitSize="@dimen/text_size_15"
app:value="75"
app:valueSize="@dimen/text_size_20" />
<com.littlejie.circleprogress.CircleProgress
android:id="@+id/circle_progress_bar3"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center_horizontal"
app:antiAlias="true"
app:arcWidth="@dimen/small"
app:bgArcColor="@android:color/darker_gray"
app:bgArcWidth="@dimen/small"
app:hint="当前进度"
app:hintSize="@dimen/text_size_25"
app:maxValue="100"
app:startAngle="270"
app:sweepAngle="360"
app:unit="%"
app:unitSize="@dimen/text_size_25"
app:value="100"
app:valueSize="@dimen/text_size_35" />
<com.littlejie.circleprogress.DialProgress
android:id="@+id/dial_progress_bar"
android:layout_width="300dp"
android:layout_height="300dp"
android:layout_gravity="center_horizontal"
android:padding="@dimen/medium"
app:animTime="1000"
app:arcColors="@array/gradient_arc_color"
app:arcWidth="@dimen/large"
app:dialIntervalDegree="3"
app:dialWidth="2dp"
app:hint="当前时速"
app:hintSize="@dimen/text_size_25"
app:maxValue="300"
app:startAngle="135"
app:sweepAngle="270"
app:unit="km/h"
app:unitSize="@dimen/text_size_25"
app:value="300"
app:valueSize="@dimen/text_size_35" />
<com.littlejie.circleprogress.WaveProgress
android:id="@+id/wave_progress_bar"
android:layout_width="300dp"
android:layout_height="300dp"
android:layout_gravity="center_horizontal"
app:darkWaveAnimTime="1000"
app:darkWaveColor="@color/dark"
app:lightWaveAnimTime="2000"
app:lightWaveColor="@color/light"
app:lightWaveDirect="R2L"
app:lockWave="false"
app:valueSize="@dimen/text_size_35"
app:waveHeight="30dp"
app:waveNum="1" />
LinearLayout>
ScrollView>
OK,开始动手
Ok,在介绍正文之前,顺便提下圆弧和圆环的区别.看图:
通俗点就是闭合的是圆环,非闭合的是圆弧.
介绍这个就是为了告诉大家,接下来的自定义圆弧和圆环进度条虽然显示效果不一样,但是画法是一样的.仅仅只要改变绘制圆环时的起始角度(mStartAngle)以及扫过的角度(mSweepAngle)即可造成圆弧和圆环的差异.仅此而已.
ok,贴出基本圆形进度条的实现类 CircleProgress.java.
package com.littlejie.circleprogress;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
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.graphics.Typeface;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import com.littlejie.circleprogress.utils.Constant;
import com.littlejie.circleprogress.utils.MiscUtil;
public class CircleProgress extends View {
private static final String TAG = CircleProgress.class.getSimpleName();
private Context mContext;
//默认大小
private int mDefaultSize;
//是否开启抗锯齿
private boolean antiAlias;
//绘制提示
private TextPaint mHintPaint;
private CharSequence mHint;
private int mHintColor;
private float mHintSize;
private float mHintOffset;
//绘制单位
private TextPaint mUnitPaint;
private CharSequence mUnit;
private int mUnitColor;
private float mUnitSize;
private float mUnitOffset;
//绘制数值
private TextPaint mValuePaint;
private float mValue;
private float mMaxValue;
private float mValueOffset;
private int mPrecision;
private String mPrecisionFormat;
private int mValueColor;
private float mValueSize;
//绘制圆弧
private Paint mArcPaint;
private float mArcWidth;
private float mStartAngle, mSweepAngle;
private RectF mRectF;
//渐变的颜色是360度,如果只显示270,那么则会缺失部分颜色
private SweepGradient mSweepGradient;
private int[] mGradientColors = {Color.GREEN, Color.YELLOW, Color.RED};
//当前进度,[0.0f,1.0f]
private float mPercent;
//动画时间
private long mAnimTime;
//属性动画
private ValueAnimator mAnimator;
//绘制背景圆弧
private Paint mBgArcPaint;
private int mBgArcColor;
private float mBgArcWidth;
//圆心坐标,半径
private Point mCenterPoint;
private float mRadius;
private float mTextOffsetPercentInRadius;
public CircleProgress(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
mContext = context;
mDefaultSize = MiscUtil.dipToPx(mContext, Constant.DEFAULT_SIZE);
mAnimator = new ValueAnimator();
mRectF = new RectF();//画矩形
mCenterPoint = new Point();
initAttrs(attrs);
initPaint();
setValue(mValue);
}
private void initAttrs(AttributeSet attrs) {
TypedArray typedArray = mContext.obtainStyledAttributes(attrs, R.styleable.CircleProgressBar);
antiAlias = typedArray.getBoolean(R.styleable.CircleProgressBar_antiAlias, Constant.ANTI_ALIAS);
mHint = typedArray.getString(R.styleable.CircleProgressBar_hint);
mHintColor = typedArray.getColor(R.styleable.CircleProgressBar_hintColor, Color.BLACK);
mHintSize = typedArray.getDimension(R.styleable.CircleProgressBar_hintSize, Constant.DEFAULT_HINT_SIZE);
mValue = typedArray.getFloat(R.styleable.CircleProgressBar_value, Constant.DEFAULT_VALUE);//50
mMaxValue = typedArray.getFloat(R.styleable.CircleProgressBar_maxValue, Constant.DEFAULT_MAX_VALUE);//100
//内容数值精度格式
mPrecision = typedArray.getInt(R.styleable.CircleProgressBar_precision, 0);
mPrecisionFormat = MiscUtil.getPrecisionFormat(mPrecision);
mValueColor = typedArray.getColor(R.styleable.CircleProgressBar_valueColor, Color.BLACK);
mValueSize = typedArray.getDimension(R.styleable.CircleProgressBar_valueSize, Constant.DEFAULT_VALUE_SIZE);
mUnit = typedArray.getString(R.styleable.CircleProgressBar_unit);
mUnitColor = typedArray.getColor(R.styleable.CircleProgressBar_unitColor, Color.BLACK);
mUnitSize = typedArray.getDimension(R.styleable.CircleProgressBar_unitSize, Constant.DEFAULT_UNIT_SIZE);
mArcWidth = typedArray.getDimension(R.styleable.CircleProgressBar_arcWidth, Constant.DEFAULT_ARC_WIDTH);
mStartAngle = typedArray.getFloat(R.styleable.CircleProgressBar_startAngle, Constant.DEFAULT_START_ANGLE);
mSweepAngle = typedArray.getFloat(R.styleable.CircleProgressBar_sweepAngle, Constant.DEFAULT_SWEEP_ANGLE);
mBgArcColor = typedArray.getColor(R.styleable.CircleProgressBar_bgArcColor, Color.WHITE);
mBgArcWidth = typedArray.getDimension(R.styleable.CircleProgressBar_bgArcWidth, Constant.DEFAULT_ARC_WIDTH);//圆弧宽度(一般和背景圆弧宽度相等)
mTextOffsetPercentInRadius = typedArray.getFloat(R.styleable.CircleProgressBar_textOffsetPercentInRadius, 0.33f);
// mPercent = typedArray.getFloat(R.styleable.CircleProgressBar_percent, 0);
mAnimTime = typedArray.getInt(R.styleable.CircleProgressBar_animTime, Constant.DEFAULT_ANIM_TIME);
int gradientArcColors = typedArray.getResourceId(R.styleable.CircleProgressBar_arcColors, 0);//圆弧颜色
Log.i(TAG, "initAttrs: gradientArcColors::"+gradientArcColors);
if (gradientArcColors != 0) {
try {
int[] gradientColors = getResources().getIntArray(gradientArcColors);
Log.i(TAG, "initAttrs: gradientColors.length::"+gradientColors.length);
if (gradientColors.length == 0) {//如果渐变色为数组为0,则尝试以单色读取色值
int color = getResources().getColor(gradientArcColors);
mGradientColors = new int[2];
mGradientColors[0] = color;
mGradientColors[1] = color;
} else if (gradientColors.length == 1) {//如果渐变数组只有一种颜色,默认设为两种相同颜色
mGradientColors = new int[2];
mGradientColors[0] = gradientColors[0];
mGradientColors[1] = gradientColors[0];
} else {
mGradientColors = gradientColors;
}
} catch (Resources.NotFoundException e) {
throw new Resources.NotFoundException("the give resource not found.");
}
}
typedArray.recycle();
}
private void initPaint() {
// hint画笔
mHintPaint = new TextPaint();
// 设置抗锯齿,会消耗较大资源,绘制图形速度会变慢。
mHintPaint.setAntiAlias(antiAlias);
// 设置绘制文字大小
mHintPaint.setTextSize(mHintSize);
// 设置画笔颜色
mHintPaint.setColor(mHintColor);
// 从中间向两边绘制,不需要再次计算文字
mHintPaint.setTextAlign(Paint.Align.CENTER);
// value画笔
mValuePaint = new TextPaint();
mValuePaint.setAntiAlias(antiAlias);
mValuePaint.setTextSize(mValueSize);
mValuePaint.setColor(mValueColor);
// 设置Typeface对象,即字体风格,包括粗体,斜体以及衬线体,非衬线体等
mValuePaint.setTypeface(Typeface.DEFAULT_BOLD);
mValuePaint.setTextAlign(Paint.Align.CENTER);
// unit画笔
mUnitPaint = new TextPaint();
mUnitPaint.setAntiAlias(antiAlias);
mUnitPaint.setTextSize(mUnitSize);
mUnitPaint.setColor(mUnitColor);
mUnitPaint.setTextAlign(Paint.Align.CENTER);
// 圆环画笔
mArcPaint = new Paint();
mArcPaint.setAntiAlias(antiAlias);
// 设置画笔的样式,为FILL,FILL_OR_STROKE,或STROKE
mArcPaint.setStyle(Paint.Style.STROKE);//画圆环必有,不然是扇弧
// 设置画笔粗细
mArcPaint.setStrokeWidth(mArcWidth);
// 当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的图形样式,如圆形样式
// Cap.ROUND,或方形样式 Cap.SQUARE
mArcPaint.setStrokeCap(Paint.Cap.ROUND);//作用于圆环结尾
// 绘制背景圆环
mBgArcPaint = new Paint();
mBgArcPaint.setAntiAlias(antiAlias);
mBgArcPaint.setColor(mBgArcColor);
mBgArcPaint.setStyle(Paint.Style.STROKE);
mBgArcPaint.setStrokeWidth(mBgArcWidth);
mBgArcPaint.setStrokeCap(Paint.Cap.ROUND);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(MiscUtil.measure(widthMeasureSpec, mDefaultSize),
MiscUtil.measure(heightMeasureSpec, mDefaultSize));
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
Log.d(TAG, "onSizeChanged: w = " + w + "; h = " + h + "; oldw = " + oldw + "; oldh = " + oldh);
//求圆弧和背景圆弧的最大宽度
float maxArcWidth = Math.max(mArcWidth, mBgArcWidth);
//求最小值作为实际值
int minSize = Math.min(w - getPaddingLeft() - getPaddingRight() - 2 * (int) maxArcWidth,
h - getPaddingTop() - getPaddingBottom() - 2 * (int) maxArcWidth);
Log.i(TAG, "onSizeChanged: mArcWidth::"+mArcWidth+"\n"+" mBgArcWidth::"+mBgArcWidth+"\n"+" maxArcWidth::"+maxArcWidth+"\n"
+" minSize::"+minSize);
//获取圆心,圆半径
mRadius = minSize / 2;
//获取圆的相关参数
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;
//计算文字绘制时的 baseline
//由于文字的baseline、descent、ascent等属性只与textSize和typeface有关,所以此时可以直接计算
//若value、hint、unit由同一个画笔绘制或者需要动态设置文字的大小,则需要在每次更新后再次计算
mValueOffset = mCenterPoint.y + getBaselineOffsetFromY(mValuePaint);//value处在中间
mHintOffset = mCenterPoint.y - mRadius * mTextOffsetPercentInRadius + getBaselineOffsetFromY(mHintPaint);//hint向上偏移1/3
mUnitOffset = mCenterPoint.y + mRadius * mTextOffsetPercentInRadius + getBaselineOffsetFromY(mUnitPaint);//unit向下偏移1/3
updateArcPaint();
Log.d(TAG, "onSizeChanged: 控件大小 = " + "(" + w + ", " + h + ")"
+ "圆心坐标 = " + mCenterPoint.toString()
+ ";圆半径 = " + mRadius
+ ";圆的外接矩形 = " + mRectF.toString());
}
/**
* 根据paint获取text字体高度的y方向的中点
* @param paint
* @return
*/
private float getBaselineOffsetFromY(Paint paint) {
return MiscUtil.measureTextHeight(paint) / 2;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawText(canvas);
drawArc(canvas);
}
/**
* 绘制内容文字
*
* @param canvas
*/
private void drawText(Canvas canvas) {
// 计算文字宽度,由于Paint已设置为居中绘制,故此处不需要重新计算
// float textWidth = mValuePaint.measureText(mValue.toString());
// float x = mCenterPoint.x - textWidth / 2;
canvas.drawText(String.format(mPrecisionFormat, mValue), mCenterPoint.x, mValueOffset, mValuePaint);
if (mHint != null) {
canvas.drawText(mHint.toString(), mCenterPoint.x, mHintOffset, mHintPaint);
}
if (mUnit != null) {
canvas.drawText(mUnit.toString(), mCenterPoint.x, mUnitOffset, mUnitPaint);
}
}
private void drawArc(Canvas canvas) {
// 绘制背景圆弧
// 从进度圆弧结束的地方开始重新绘制,优化性能
canvas.save();
//用于表示数值对应的圆弧的当前角度
float currentAngle = mSweepAngle * mPercent;
//顺时针旋转135度(将起点至于7.5点钟方向)
canvas.rotate(mStartAngle, mCenterPoint.x, mCenterPoint.y);//从135度开始绘制
Log.i(TAG, "drawArc: currentAngle::"+currentAngle
+" mSweepAngle::"+mSweepAngle
+" (mSweepAngle - currentAngle + 2)::"+(mSweepAngle - currentAngle + 2));
canvas.drawArc(mRectF, currentAngle, mSweepAngle - currentAngle, false, mBgArcPaint);
// 第一个参数 oval 为 RectF 类型,即圆弧显示区域
// startAngle 和 sweepAngle 均为 float 类型,分别表示圆弧起始角度和圆弧度数
// 3点钟方向为0度,顺时针递增
// 如果 startAngle < 0 或者 > 360,则相当于 startAngle % 360
// useCenter:如果为True时,在绘制圆弧时将圆心包括在内,通常用来绘制扇形
//因为画圆弧的画笔是圆头类型的,在起始地方0度偏左还会有一个半圆,但是我们又采用了渐变色渲染,所以圆头部分就变成了结束的颜色值
canvas.drawArc(mRectF, 2, currentAngle, false, mArcPaint);
canvas.restore();
}
/**
* 更新圆弧画笔
*/
private void updateArcPaint() {
// 设置渐变
mSweepGradient = new SweepGradient(mCenterPoint.x, mCenterPoint.y, mGradientColors, null);
mArcPaint.setShader(mSweepGradient);
}
public boolean isAntiAlias() {
return antiAlias;
}
public CharSequence getHint() {
return mHint;
}
public void setHint(CharSequence hint) {
mHint = hint;
}
public CharSequence getUnit() {
return mUnit;
}
public void setUnit(CharSequence unit) {
mUnit = unit;
}
public float getValue() {
return mValue;
}
//**********************************用于点击设置随机值,用于进度显示*****************************
/**
* 设置当前值
*
* @param value
*/
public void setValue(float value) {
if (value > mMaxValue) {
value = mMaxValue;
}
float start = mPercent;
float end = value / mMaxValue;
startAnimator(start, end, mAnimTime);
}
private void startAnimator(float start, float end, long animTime) {
Log.i(TAG, "startAnimator: start::"+start+" end::"+end);
mAnimator = ValueAnimator.ofFloat(start, end);//获取%区间 当前进度,[0.0f,1.0f]
mAnimator.setDuration(animTime);
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mPercent = (float) animation.getAnimatedValue();
mValue = mPercent * mMaxValue;
Log.d(TAG, "onAnimationUpdate: percent = " + mPercent
+ ";currentAngle = " + (mSweepAngle * mPercent)
+ ";value = " + mValue);
invalidate();
}
});
mAnimator.start();
}
/**
* 获取最大值
*
* @return
*/
public float getMaxValue() {
return mMaxValue;
}
/**
* 设置最大值
*
* @param maxValue
*/
public void setMaxValue(float maxValue) {
mMaxValue = maxValue;
}
/**
* 获取精度
*
* @return
*/
public int getPrecision() {
return mPrecision;
}
public void setPrecision(int precision) {
mPrecision = precision;
mPrecisionFormat = MiscUtil.getPrecisionFormat(precision);
}
public int[] getGradientColors() {
return mGradientColors;
}
/**
* 设置渐变
*
* @param gradientColors
*/
public void setGradientColors(int[] gradientColors) {
mGradientColors = gradientColors;
updateArcPaint();
}
public long getAnimTime() {
return mAnimTime;
}
public void setAnimTime(long animTime) {
mAnimTime = animTime;
}
/**
* 重置
*/
public void reset() {
startAnimator(mPercent, 0.0f, 1000L);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
//释放资源
}
}
原博文中代码结构比较清晰了,我在其中又添加了注解.基本每行代码你都能读懂,应该不难理解.这里不在赘述.
Num 1. 可以略微关注下(了解下即可,不影响展现效果),代码中在绘制彩色圆弧(注意不是圆环)时
canvas.drawArc(mRectF, currentAngle, mSweepAngle - currentAngle, false, mBgArcPaint);
// 第一个参数 oval 为 RectF 类型,即圆弧显示区域
// startAngle 和 sweepAngle 均为 float 类型,分别表示圆弧起始角度和圆弧度数
// 3点钟方向为0度,顺时针递增
// 如果 startAngle < 0 或者 > 360,则相当于 startAngle % 360
// useCenter:如果为True时,在绘制圆弧时将圆心包括在内,通常用来绘制扇形
//因为画圆弧的画笔是圆头类型的,在起始地方0度偏左还会有一个半圆,但是我们又采用了渐变色渲染,所以圆头部分就变成了结束的颜色值
canvas.drawArc(mRectF, 2, currentAngle, false, mArcPaint);
你可能发现了, canvas.drawArc(mRectF, 2, currentAngle, false, mArcPaint);中的第二个参数是2,为什么会这样呢?正常情况下不应该是0么?
没错,之所以这样,是因为在构造圆环画笔的时候,我们为了好看,使用了
mArcPaint.setStrokeCap(Paint.Cap.ROUND);//作用于圆环结尾
这样的话,因为画圆弧的画笔是圆头类型的,在起始地方0度偏左还会有一个半圆,但是我们又采用了渐变色渲染,所以圆头部分就变成了结束的颜色值.可能有点难懂,直接上图,看看如果第二个参数设置成"0"的话,会出现什么效果.如图:
看到没?左下角多了个"小红点"(小半圆).至于为什么设置成"2"能遮住小红点,我不太理解.如果有知道的小伙伴请告知!!!
但是这样做的话,会造成圆弧最下端左高又低(不仔细看看不出来哦),当然了一般要求不高的话,这样做是完全可以的.
那么有没有可以遮住小红点,又不会造成圆弧底部左高右低的方法呢?
办法到是有一个,那就是以小红点的中心为圆心,圆弧宽/2为半径画一个与起点颜色一致的圆即可.详情可参考:一步步做Android自定义圆环百分比控件,里面的解决方案可参考.但是不建议如此,因为没什么必要.(实在不行,你可以调节mSweepAngle角度也是可以的)
Num 2. 代码中在绘制圆环(或圆弧)渐变色的时候,原博文中是这样的.
if (gradientArcColors != 0) {
try {
int[] gradientColors = getResources().getIntArray(gradientArcColors);
Log.i(TAG, "initAttrs: gradientColors.length::"+gradientColors.length);
if (gradientColors.length == 0) {//如果渐变色为数组为0,则尝试以单色读取色值
int color = getResources().getColor(gradientArcColors);
mGradientColors = new int[2];
mGradientColors[0] = color;
mGradientColors[1] = color;
} else if (gradientColors.length == 1) {//如果渐变数组只有一种颜色,默认设为两种相同颜色
mGradientColors = new int[2];
mGradientColors[0] = gradientColors[0];
mGradientColors[1] = gradientColors[0];
} else {
mGradientColors = gradientColors;
}
} catch (Resources.NotFoundException e) {
throw new Resources.NotFoundException("the give resource not found.");
}
}
可是我实在想不明白:
1. 什么情况下能满足(gradientColors.length == 0)? 应该不存在的
2. 在if (gradientColors.length == 1)给渐变色赋值时,既然mGradientColors = new int[2];了
,那么 mGradientColors[1] = gradientColors[0];是什么鬼? mGradientColors[1] = gradientColors[1];才正常.相信作者是笔误吧.
因此.上述代码改成下面是完全可以的.
if (gradientArcColors != 0) {
try {
int[] gradientColors = getResources().getIntArray(gradientArcColors);
Log.i(TAG, "initAttrs: gradientColors.length::"+gradientColors.length);
if (gradientColors.length == 1) {//如果渐变色为数组为0,则尝试以单色读取色值
mGradientColors = new int[2];
mGradientColors[0] = gradientColors[0];
} else if (gradientColors.length == 2) {//如果渐变数组只有一种颜色,默认设为两种相同颜色
mGradientColors = new int[2];
mGradientColors[0] = gradientColors[0];
mGradientColors[1] = gradientColors[1];
} else {
mGradientColors = new int[3];
mGradientColors[0] = gradientColors[0];
mGradientColors[1] = gradientColors[1];
mGradientColors[2] = gradientColors[2];
// mGradientColors = gradientColors;
}
} catch (Resources.NotFoundException e) {
throw new Resources.NotFoundException("the give resource not found.");
}
}
这里还有两个注意点:
1. 不要在 ValueAnimator.AnimatorUpdateListener 中输出 Log,特别是动画调用频繁的情况下,因为输出 Log 频繁会生成大量 String 对象造成内存抖动,当然也可以使用 StringBuilder 来优化。
2. 关于 invalidate() 和 postInvalidate() 两者最本质的前者只能在 UI 线程中使用,而后者可以在非 UI 线程中使用,其实 postInvalidate() 内部也是使用 Handler 实现的。
DialProgress 与 CircleProgress 的实现极其相似.仅仅是**在基本圆环进度条的基础上添加了一个不断旋转的"小白色矩形(刻度中间的白色间隔)"**而已.因为两者之间其实就差了一个刻度,但考虑到扩展以及类职责的单一,所以将两者分开。这里主要讲一下刻度的绘制。刻度绘制主要使用 Canvas 类的 save()、rotate()和restore() 方法,当然你也可以使用 translate() 方法对坐标系进行平移,方便计算。
再次强调下,图中的彩色刻度不是我们画上去的,我们只是绘制了一个"白色刻度间隔",然后通过旋转canvas来实现"白色刻度间隔"的沿圆弧绘制.明白了吗,绘制的是刻度间隔,不是刻度(想想也会明白,刻度其实是一个个小梯形,绘制梯形不太现实).
上代码:DialProgress.java
package com.littlejie.circleprogress;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
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.graphics.Typeface;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import com.littlejie.circleprogress.utils.Constant;
import com.littlejie.circleprogress.utils.MiscUtil;
/**
* 带有刻度的圆形进度条
* Created by littlejie on 2017/2/26.
*/
public class DialProgress extends View {
private static final String TAG = DialProgress.class.getSimpleName();
private Context mContext;
//圆心坐标
private Point mCenterPoint;
private float mRadius;
private float mTextOffsetPercentInRadius;
private boolean antiAlias;
//绘制提示
private TextPaint mHintPaint;
private CharSequence mHint;
private int mHintColor;
private float mHintSize;
private float mHintOffset;
//绘制数值
private Paint mValuePaint;
private int mValueColor;
private float mMaxValue;
private float mValue;
private float mValueSize;
private float mValueOffset;
private String mPrecisionFormat;
//绘制单位
private Paint mUnitPaint;
private float mUnitSize;
private int mUnitColor;
private float mUnitOffset;
private CharSequence mUnit;
//前景圆弧
private Paint mArcPaint;
private float mArcWidth;
//刻度之间的间隔
private int mDialIntervalDegree;
private float mStartAngle, mSweepAngle;
private RectF mRectF;
//渐变
private int[] mGradientColors = {Color.GREEN, Color.YELLOW, Color.RED};
//当前进度,[0.0f,1.0f]
private float mPercent;
//动画时间
private long mAnimTime;
//属性动画
private ValueAnimator mAnimator;
//背景圆弧
private Paint mBgArcPaint;
private int mBgArcColor;
//刻度线颜色
private Paint mDialPaint;
private float mDialWidth;
private int mDialColor;
private int mDefaultSize;
public DialProgress(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
mContext = context;
mDefaultSize = MiscUtil.dipToPx(context, Constant.DEFAULT_SIZE);
mRectF = new RectF();
mCenterPoint = new Point();
initConfig(context, attrs);
initPaint();
setValue(mValue);
}
private void initConfig(Context context, AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.DialProgress);
antiAlias = typedArray.getBoolean(R.styleable.DialProgress_antiAlias, true);
mMaxValue = typedArray.getFloat(R.styleable.DialProgress_maxValue, Constant.DEFAULT_MAX_VALUE);
mValue = typedArray.getFloat(R.styleable.DialProgress_value, Constant.DEFAULT_VALUE);
mValueSize = typedArray.getDimension(R.styleable.DialProgress_valueSize, Constant.DEFAULT_VALUE_SIZE);
mValueColor = typedArray.getColor(R.styleable.DialProgress_valueColor, Color.BLACK);
mDialIntervalDegree = typedArray.getInt(R.styleable.DialProgress_dialIntervalDegree, 10);
int precision = typedArray.getInt(R.styleable.DialProgress_precision, 0);
mPrecisionFormat = MiscUtil.getPrecisionFormat(precision);
mUnit = typedArray.getString(R.styleable.DialProgress_unit);
mUnitColor = typedArray.getColor(R.styleable.DialProgress_unitColor, Color.BLACK);
mUnitSize = typedArray.getDimension(R.styleable.DialProgress_unitSize, Constant.DEFAULT_UNIT_SIZE);
mHint = typedArray.getString(R.styleable.DialProgress_hint);
mHintColor = typedArray.getColor(R.styleable.DialProgress_hintColor, Color.BLACK);
mHintSize = typedArray.getDimension(R.styleable.DialProgress_hintSize, Constant.DEFAULT_HINT_SIZE);
mArcWidth = typedArray.getDimension(R.styleable.DialProgress_arcWidth, Constant.DEFAULT_ARC_WIDTH);
mStartAngle = typedArray.getFloat(R.styleable.DialProgress_startAngle, Constant.DEFAULT_START_ANGLE);
mSweepAngle = typedArray.getFloat(R.styleable.DialProgress_sweepAngle, Constant.DEFAULT_SWEEP_ANGLE);
mAnimTime = typedArray.getInt(R.styleable.DialProgress_animTime, Constant.DEFAULT_ANIM_TIME);
mBgArcColor = typedArray.getColor(R.styleable.DialProgress_bgArcColor, Color.GRAY);
mDialWidth = typedArray.getDimension(R.styleable.DialProgress_dialWidth, 2);
mDialColor = typedArray.getColor(R.styleable.DialProgress_dialColor, Color.WHITE);
mTextOffsetPercentInRadius = typedArray.getFloat(R.styleable.DialProgress_textOffsetPercentInRadius, 0.33f);
int gradientArcColors = typedArray.getResourceId(R.styleable.DialProgress_arcColors, 0);
if (gradientArcColors != 0) {
try {
int[] gradientColors = getResources().getIntArray(gradientArcColors);
if (gradientColors.length == 0) {
int color = getResources().getColor(gradientArcColors);
mGradientColors = new int[2];
mGradientColors[0] = color;
mGradientColors[1] = color;
} else if (gradientColors.length == 1) {
mGradientColors = new int[2];
mGradientColors[0] = gradientColors[0];
mGradientColors[1] = gradientColors[0];
} else {
mGradientColors = gradientColors;
}
} catch (Resources.NotFoundException e) {
throw new Resources.NotFoundException("the give resource not found.");
}
}
typedArray.recycle();
}
private void initPaint() {
mHintPaint = new TextPaint();
// 设置抗锯齿,会消耗较大资源,绘制图形速度会变慢。
mHintPaint.setAntiAlias(antiAlias);
// 设置绘制文字大小
mHintPaint.setTextSize(mHintSize);
// 设置画笔颜色
mHintPaint.setColor(mHintColor);
// 从中间向两边绘制,不需要再次计算文字
mHintPaint.setTextAlign(Paint.Align.CENTER);
mValuePaint = new Paint();
mValuePaint.setAntiAlias(antiAlias);
mValuePaint.setTextSize(mValueSize);
mValuePaint.setColor(mValueColor);
mValuePaint.setTypeface(Typeface.DEFAULT_BOLD);
mValuePaint.setTextAlign(Paint.Align.CENTER);
mUnitPaint = new Paint();
mUnitPaint.setAntiAlias(antiAlias);
mUnitPaint.setTextSize(mUnitSize);
mUnitPaint.setColor(mUnitColor);
mUnitPaint.setTextAlign(Paint.Align.CENTER);
mArcPaint = new Paint();
mArcPaint.setAntiAlias(antiAlias);
mArcPaint.setStyle(Paint.Style.STROKE);
mArcPaint.setStrokeWidth(mArcWidth);
mArcPaint.setStrokeCap(Paint.Cap.BUTT);
mBgArcPaint = new Paint();
mBgArcPaint.setAntiAlias(antiAlias);
mBgArcPaint.setStyle(Paint.Style.STROKE);
mBgArcPaint.setStrokeWidth(mArcWidth);
mBgArcPaint.setStrokeCap(Paint.Cap.BUTT);
mBgArcPaint.setColor(mBgArcColor);
mDialPaint = new Paint();
mDialPaint.setAntiAlias(antiAlias);
mDialPaint.setColor(mDialColor);
mDialPaint.setStrokeWidth(mDialWidth);
}
/**
* 更新圆弧画笔
*/
private void updateArcPaint() {
// 设置渐变
// 渐变的颜色是360度,如果只显示270,那么则会缺失部分颜色
SweepGradient sweepGradient = new SweepGradient(mCenterPoint.x, mCenterPoint.y, mGradientColors, null);
mArcPaint.setShader(sweepGradient);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(MiscUtil.measure(widthMeasureSpec, mDefaultSize),
MiscUtil.measure(heightMeasureSpec, mDefaultSize));
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
Log.d(TAG, "onSizeChanged: w = " + w + "; h = " + h + "; oldw = " + oldw + "; oldh = " + oldh);
int minSize = Math.min(getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - 2 * (int) mArcWidth,
getMeasuredHeight() - getPaddingTop() - getPaddingBottom() - 2 * (int) mArcWidth);
mRadius = minSize / 2;
mCenterPoint.x = getMeasuredWidth() / 2;
mCenterPoint.y = getMeasuredHeight() / 2;
//绘制圆弧的边界
mRectF.left = mCenterPoint.x - mRadius - mArcWidth / 2;
mRectF.top = mCenterPoint.y - mRadius - mArcWidth / 2;
mRectF.right = mCenterPoint.x + mRadius + mArcWidth / 2;
mRectF.bottom = mCenterPoint.y + mRadius + mArcWidth / 2;
mValueOffset = mCenterPoint.y + getBaselineOffsetFromY(mValuePaint);
mHintOffset = mCenterPoint.y - mRadius * mTextOffsetPercentInRadius + getBaselineOffsetFromY(mHintPaint);
mUnitOffset = mCenterPoint.y + mRadius * mTextOffsetPercentInRadius + getBaselineOffsetFromY(mUnitPaint);
updateArcPaint();
Log.d(TAG, "onMeasure: 控件大小 = " + "(" + getMeasuredWidth() + ", " + getMeasuredHeight() + ")"
+ ";圆心坐标 = " + mCenterPoint.toString()
+ ";圆半径 = " + mRadius
+ ";圆的外接矩形 = " + mRectF.toString());
}
private float getBaselineOffsetFromY(Paint paint) {
return MiscUtil.measureTextHeight(paint) / 2;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawArc(canvas);
drawDial(canvas);
drawText(canvas);
}
private void drawArc(Canvas canvas) {
// 绘制背景圆弧
// 从进度圆弧结束的地方开始重新绘制,优化性能
float currentAngle = mSweepAngle * mPercent;
canvas.save();
canvas.rotate(mStartAngle, mCenterPoint.x, mCenterPoint.y);
canvas.drawArc(mRectF, currentAngle, mSweepAngle - currentAngle, false, mBgArcPaint);
// 第一个参数 oval 为 RectF 类型,即圆弧显示区域
// startAngle 和 sweepAngle 均为 float 类型,分别表示圆弧起始角度和圆弧度数
// 3点钟方向为0度,顺时针递增
// 如果 startAngle < 0 或者 > 360,则相当于 startAngle % 360
// useCenter:如果为True时,在绘制圆弧时将圆心包括在内,通常用来绘制扇形
canvas.drawArc(mRectF, 0, currentAngle, false, mArcPaint);
canvas.restore();
}
/**
* 绘制刻度
* @param canvas
*/
private void drawDial(Canvas canvas) {
//获取分成多少个间隔
int total = (int) (mSweepAngle / mDialIntervalDegree);
canvas.save();
canvas.rotate(mStartAngle, mCenterPoint.x, mCenterPoint.y);
for (int i = 0; i <= total; i++) {
//这一点可能比较难理解点:drawLine(...)从表面看画的是圆最右边的一条白线(白色小矩形),但是由于在drawArc()中已经将canvas顺时针旋转了135度,一次刻度间隔的白线也就从圆弧起点开始了
canvas.drawLine(mCenterPoint.x + mRadius, mCenterPoint.y, mCenterPoint.x + mRadius + mArcWidth, mCenterPoint.y, mDialPaint);
canvas.rotate(mDialIntervalDegree, mCenterPoint.x, mCenterPoint.y);
}
canvas.restore();
}
private void drawText(Canvas canvas) {
canvas.drawText(String.format(mPrecisionFormat, mValue), mCenterPoint.x, mValueOffset, mValuePaint);
if (mUnit != null) {
canvas.drawText(mUnit.toString(), mCenterPoint.x, mUnitOffset, mUnitPaint);
}
if (mHint != null) {
canvas.drawText(mHint.toString(), mCenterPoint.x, mHintOffset, mHintPaint);
}
}
public float getMaxValue() {
return mMaxValue;
}
public void setMaxValue(float maxValue) {
mMaxValue = maxValue;
}
/**
* 设置当前值
*
* @param value
*/
public void setValue(float value) {
if (value > mMaxValue) {
value = mMaxValue;
}
float start = mPercent;
float end = value / mMaxValue;
startAnimator(start, end, mAnimTime);
}
private void startAnimator(float start, float end, long animTime) {
mAnimator = ValueAnimator.ofFloat(start, end);
mAnimator.setDuration(animTime);
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mPercent = (float) animation.getAnimatedValue();
mValue = mPercent * mMaxValue;
if (BuildConfig.DEBUG) {
Log.d(TAG, "onAnimationUpdate: percent = " + mPercent
+ ";currentAngle = " + (mSweepAngle * mPercent)
+ ";value = " + mValue);
}
invalidate();
}
});
mAnimator.start();
}
public int[] getGradientColors() {
return mGradientColors;
}
public void setGradientColors(int[] gradientColors) {
mGradientColors = gradientColors;
updateArcPaint();
}
public void reset() {
startAnimator(mPercent, 0.0f, 1000L);
}
}
要说的话,都已经在注解中了.
老样子,下面将其中你可能有疑问的地方拿出说明下:
(1). 代码中有这样一句话
//这一点可能比较难理解点:drawLine(...)从表面看画的是圆最右边的一条白线(白色小矩形),但是由于在drawArc()中已经将canvas顺时针旋转了135度,一次刻度间隔的白线也就从圆弧起点开始了
canvas.drawLine(mCenterPoint.x + mRadius, mCenterPoint.y, mCenterPoint.x + mRadius + mArcWidth, mCenterPoint.y, mDialPaint);
如果只看代码中的canvas.drawLine(…)绘制的"白色刻度间隔"应该是在圆弧最右侧.这个不难理解,因为在绘制圆弧时drawArc和SweepGradient这两个类的起始点0度不是在我们习惯的圆环最上面那个点,而是从圆环最右边那个点开始,如图(再贴一遍吧)
如果只看这行代码,确实是这样,但是别忘了,还有
canvas.rotate(mDialIntervalDegree, mCenterPoint.x, mCenterPoint.y);
canvas.rotate(…)的作用就在于将"白色刻度间隔"顺时针旋转135度(这个角度是可调的,在xml中设置自定义控件的属性即可),也就是左下角圆弧起始处,这样一来,"白色刻度间隔"就会从起始处沿圆弧绘制了.
可以看下刻度间隔的绘制流程1----->2------->3------->4:
1. 2. 3. 4.
就是这样,明白了吧.
原博文作者是真牛逼,我花了好长时间,才基本弄明白.我也是照着"灵魂画手"的图,然后各种log和断点调试.才基本弄明白,但是对于其中对绘制水波曲线上的各种坐标计算的的算法到底是怎么总结出来的,抱歉,水平有限-------我只能验证其中的坐标算法正确.但是如何来的,我无能为力.
水波纹效果的进度条实现需要用到贝塞尔曲线,主要难点在于 绘制区域的计算 和 波浪效果 的实现,其余的逻辑跟上述两种进度条相似。
这里使用了 Path 类,该类在 Android 2D 绘图中是非常重要的,Path 不仅能够绘制简单图形,也可以绘制这些比较复杂的图形。也可以对多个路径进行布尔操作,类似设置 Paint 的 setXfermode() ,具体使用可以参考这篇博客:安卓自定义View进阶-Path基本操作
先上代码:WaveProgress.java
package com.littlejie.circleprogress;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.RectF;
import android.os.Build;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.animation.LinearInterpolator;
import com.littlejie.circleprogress.utils.Constant;
import com.littlejie.circleprogress.utils.MiscUtil;
/**
* 水波进度条
* Created by littlejie on 2017/2/26.
*/
public class WaveProgress extends View {
private static final String TAG = WaveProgress.class.getSimpleName();
//浅色波浪方向
private static final int L2R = 0;
private static final int R2L = 1;
private int mDefaultSize;
//圆心
private Point mCenterPoint;
//半径
private float mRadius;
//圆的外接矩形
private RectF mRectF;
//深色波浪移动距离
private float mDarkWaveOffset;
//浅色波浪移动距离
private float mLightWaveOffset;
//浅色波浪方向
private boolean isR2L;
//是否锁定波浪不随进度移动
private boolean lockWave;
//是否开启抗锯齿
private boolean antiAlias;
//最大值
private float mMaxValue;
//当前值
private float mValue;
//当前进度
private float mPercent;
//绘制提示
private TextPaint mHintPaint;
private CharSequence mHint;
private int mHintColor;
private float mHintSize;
private Paint mPercentPaint;
private float mValueSize;
private int mValueColor;
//圆环宽度
private float mCircleWidth;
//圆环
private Paint mCirclePaint;
//圆环颜色
private int mCircleColor;
//背景圆环颜色
private int mBgCircleColor;
//水波路径
private Path mWaveLimitPath;
private Path mWavePath;
//水波高度
private float mWaveHeight;
//水波数量
private int mWaveNum;
//深色水波
private Paint mWavePaint;
//深色水波颜色
private int mDarkWaveColor;
//浅色水波颜色
private int mLightWaveColor;
//深色水波贝塞尔曲线上的起始点、控制点
private Point[] mDarkPoints;
//浅色水波贝塞尔曲线上的起始点、控制点
private Point[] mLightPoints;
//贝塞尔曲线点的总个数
private int mAllPointCount;
private int mHalfPointCount;
private ValueAnimator mProgressAnimator;
private long mDarkWaveAnimTime;
private ValueAnimator mDarkWaveAnimator;
private long mLightWaveAnimTime;
private ValueAnimator mLightWaveAnimator;
public WaveProgress(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
mDefaultSize = MiscUtil.dipToPx(context, Constant.DEFAULT_SIZE);
mRectF = new RectF();
mCenterPoint = new Point();
initAttrs(context, attrs);
initPaint();
initPath();
}
private void initAttrs(Context context, AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.WaveProgress);
antiAlias = typedArray.getBoolean(R.styleable.WaveProgress_antiAlias, true);
mDarkWaveAnimTime = typedArray.getInt(R.styleable.WaveProgress_darkWaveAnimTime, Constant.DEFAULT_ANIM_TIME);
mLightWaveAnimTime = typedArray.getInt(R.styleable.WaveProgress_lightWaveAnimTime, Constant.DEFAULT_ANIM_TIME);
mMaxValue = typedArray.getFloat(R.styleable.WaveProgress_maxValue, Constant.DEFAULT_MAX_VALUE);
mValue = typedArray.getFloat(R.styleable.WaveProgress_value, Constant.DEFAULT_VALUE);
mValueSize = typedArray.getDimension(R.styleable.WaveProgress_valueSize, Constant.DEFAULT_VALUE_SIZE);
mValueColor = typedArray.getColor(R.styleable.WaveProgress_valueColor, Color.BLACK);
mHint = typedArray.getString(R.styleable.WaveProgress_hint);
mHintColor = typedArray.getColor(R.styleable.WaveProgress_hintColor, Color.BLACK);
mHintSize = typedArray.getDimension(R.styleable.WaveProgress_hintSize, Constant.DEFAULT_HINT_SIZE);
mCircleWidth = typedArray.getDimension(R.styleable.WaveProgress_circleWidth, Constant.DEFAULT_ARC_WIDTH);
mCircleColor = typedArray.getColor(R.styleable.WaveProgress_circleColor, Color.GREEN);
mBgCircleColor = typedArray.getColor(R.styleable.WaveProgress_bgCircleColor, Color.WHITE);
mWaveHeight = typedArray.getDimension(R.styleable.WaveProgress_waveHeight, Constant.DEFAULT_WAVE_HEIGHT);
mWaveNum = typedArray.getInt(R.styleable.WaveProgress_waveNum, 1);
mDarkWaveColor = typedArray.getColor(R.styleable.WaveProgress_darkWaveColor,
getResources().getColor(android.R.color.holo_blue_dark));
mLightWaveColor = typedArray.getColor(R.styleable.WaveProgress_lightWaveColor,
getResources().getColor(android.R.color.holo_green_light));
isR2L = typedArray.getInt(R.styleable.WaveProgress_lightWaveDirect, R2L) == R2L;
lockWave = typedArray.getBoolean(R.styleable.WaveProgress_lockWave, false);
typedArray.recycle();
}
private void initPaint() {
//todo hint画笔
mHintPaint = new TextPaint();
// 设置抗锯齿,会消耗较大资源,绘制图形速度会变慢。
mHintPaint.setAntiAlias(antiAlias);
// 设置绘制文字大小
mHintPaint.setTextSize(mHintSize);
// 设置画笔颜色
mHintPaint.setColor(mHintColor);
// 从中间向两边绘制,不需要再次计算文字
mHintPaint.setTextAlign(Paint.Align.CENTER);
//todo 圆环画笔
mCirclePaint = new Paint();
mCirclePaint.setAntiAlias(antiAlias);
mCirclePaint.setStrokeWidth(mCircleWidth);
mCirclePaint.setStyle(Paint.Style.STROKE);
mCirclePaint.setStrokeCap(Paint.Cap.ROUND);
//todo 波浪画笔
mWavePaint = new Paint();
mWavePaint.setAntiAlias(antiAlias);
mWavePaint.setStyle(Paint.Style.FILL);
//todo 数值画笔
mPercentPaint = new Paint();
mPercentPaint.setTextAlign(Paint.Align.CENTER);
mPercentPaint.setAntiAlias(antiAlias);
mPercentPaint.setColor(mValueColor);
mPercentPaint.setTextSize(mValueSize);
}
private void initPath() {
mWaveLimitPath = new Path();
mWavePath = new Path();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(MiscUtil.measure(widthMeasureSpec, mDefaultSize),
MiscUtil.measure(heightMeasureSpec, mDefaultSize));
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
Log.d(TAG, "onSizeChanged: w = " + w + "; h = " + h + "; oldw = " + oldw + "; oldh = " + oldh);
int minSize = Math.min(getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - 2 * (int) mCircleWidth,
getMeasuredHeight() - getPaddingTop() - getPaddingBottom() - 2 * (int) mCircleWidth);
mRadius = minSize / 2;
mCenterPoint.x = getMeasuredWidth() / 2;
mCenterPoint.y = getMeasuredHeight() / 2;
//绘制圆弧的边界
mRectF.left = mCenterPoint.x - mRadius - mCircleWidth / 2;
mRectF.top = mCenterPoint.y - mRadius - mCircleWidth / 2;
mRectF.right = mCenterPoint.x + mRadius + mCircleWidth / 2;
mRectF.bottom = mCenterPoint.y + mRadius + mCircleWidth / 2;
Log.d(TAG, "onSizeChanged: 控件大小 = " + "(" + getMeasuredWidth() + ", " + getMeasuredHeight() + ")"
+ ";圆心坐标 = " + mCenterPoint.toString()
+ ";圆半径 = " + mRadius
+ ";圆的外接矩形 = " + mRectF.toString());
initWavePoints();
//开始动画
setValue(mValue);
startWaveAnimator();
}
private void initWavePoints() {
//当前波浪宽度
float waveWidth = (mRadius * 2) / mWaveNum;
mAllPointCount = 8 * mWaveNum + 1;
mHalfPointCount = mAllPointCount / 2;
Log.i(TAG, "initWavePoints: mHalfPointCount::"+mHalfPointCount);
mDarkPoints = getPoint(false, waveWidth);//从左向右
mLightPoints = getPoint(isR2L, waveWidth);//从右向左
}
/**
* 从左往右或者从右往左获取贝塞尔点
*
* @return
*/
private Point[] getPoint(boolean isR2L, float waveWidth) {
Point[] points = new Point[mAllPointCount];//9
//第1个点特殊处理,即数组的中点//todo 待论证
points[mHalfPointCount] = new Point((int) (mCenterPoint.x + (isR2L ? mRadius : -mRadius)), mCenterPoint.y);
Log.i(TAG, "getPoint: points[mHalfPointCount]::"+points[mHalfPointCount]);//points[4]::Point(15, 450)
//屏幕内的贝塞尔曲线点
for (int i = mHalfPointCount + 1; i < mAllPointCount; i += 4) {
Log.i(TAG, "getPoint: i="+i+" (i / 4 - mWaveNum)::"+(i / 4 - mWaveNum)+" waveWidth::"+waveWidth+" final::"+(waveWidth * (i / 4 - mWaveNum)));
//width为中点到原点沿x方向的offset
float width = points[mHalfPointCount].x + waveWidth * (i / 4 - mWaveNum);
//points[mHalfPointCount].x::15 waveWidth::870.0 width::15.0
Log.i(TAG, "getPoint: points[mHalfPointCount].x::"+points[mHalfPointCount].x+" waveWidth::"+waveWidth+" width::"+width);
points[i] = new Point((int) (waveWidth*1 / 4 + width), (int) (mCenterPoint.y - mWaveHeight));//points[5]::Point(232, 360)
points[i + 1] = new Point((int) (waveWidth *2/ 4 + width), mCenterPoint.y);//points[6]::Point(450, 450)
points[i + 2] = new Point((int) (waveWidth * 3 / 4 + width), (int) (mCenterPoint.y + mWaveHeight));//points[7]::Point(667, 540)
points[i + 3] = new Point((int) (waveWidth + width), mCenterPoint.y);//points[8]::Point(885, 450)
Log.i(TAG, "getPoint:"+"\n"
+"points["+i+"]::"+points[i]+"\n"
+"points["+(i+1)+"]::"+points[i+1]+"\n"
+"points["+(i+2)+"]::"+points[i+2]+"\n"
+"points["+(i+3)+"]::"+points[i+3]+"\n");
}
//屏幕外的贝塞尔曲线点
for (int i = 0; i < mHalfPointCount; i++) {
int reverse = mAllPointCount - i - 1;//8 7 6 5
points[i] = new Point((isR2L ? 2 : 1) * points[mHalfPointCount].x - points[reverse].x,
points[mHalfPointCount].y * 2 - points[reverse].y);
// getPoint: points[0]::Point(-870, 450)
// getPoint: points[1]::Point(-652, 360)
// getPoint: points[2]::Point(-435, 450)
// getPoint: points[3]::Point(-217, 540)
Log.i(TAG, "getPoint: "+"points["+i+"]::"+points[i]+"\n");
}
//对从右向左的贝塞尔点数组反序,方便后续处理
return isR2L ? MiscUtil.reverse(points) : points;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawCircle(canvas);
drawLightWave(canvas);
drawDarkWave(canvas);
drawProgress(canvas);
}
/**
* 绘制圆环
*
* @param canvas
*/
private void drawCircle(Canvas canvas) {
canvas.save();
canvas.rotate(270, mCenterPoint.x, mCenterPoint.y);
int currentAngle = (int) (360 * mPercent);
//画背景圆环
mCirclePaint.setColor(mBgCircleColor);
canvas.drawArc(mRectF, currentAngle, 360 - currentAngle, false, mCirclePaint);
//画圆环
mCirclePaint.setColor(mCircleColor);
canvas.drawArc(mRectF, 0, currentAngle, false, mCirclePaint);
canvas.restore();
}
/**
* 绘制深色波浪(贝塞尔曲线)
*
* @param canvas
*/
private void drawDarkWave(Canvas canvas) {
mWavePaint.setColor(mDarkWaveColor);
drawWave(canvas, mWavePaint, mDarkPoints, mDarkWaveOffset);
}
/**
* 绘制浅色波浪(贝塞尔曲线)
*
* @param canvas
*/
private void drawLightWave(Canvas canvas) {
mWavePaint.setColor(mLightWaveColor);
//从右向左的水波位移应该被减去
drawWave(canvas, mWavePaint, mLightPoints, isR2L ? -mLightWaveOffset : mLightWaveOffset);
}
@TargetApi(Build.VERSION_CODES.KITKAT)
private void drawWave(Canvas canvas, Paint paint, Point[] points, float waveOffset) {
mWaveLimitPath.reset();
mWavePath.reset();
float height = lockWave ? 0 : mRadius - 2 * mRadius * mPercent;
//moveTo和lineTo绘制出水波区域矩形
mWavePath.moveTo(points[0].x + waveOffset, points[0].y + height);
for (int i = 1; i < mAllPointCount; i += 2) {
mWavePath.quadTo(points[i].x + waveOffset, points[i].y + height,
points[i + 1].x + waveOffset, points[i + 1].y + height);
}
//mWavePath.lineTo(points[mAllPointCount - 1].x, points[mAllPointCount - 1].y + height);
//不管如何移动,波浪与圆路径的交集底部永远固定,否则会造成上移的时候底部为空的情况
mWavePath.lineTo(points[mAllPointCount - 1].x, mCenterPoint.y + mRadius);
mWavePath.lineTo(points[0].x, mCenterPoint.y + mRadius);
mWavePath.close();
mWaveLimitPath.addCircle(mCenterPoint.x, mCenterPoint.y, mRadius, Path.Direction.CW);
//取该圆与波浪路径的交集,形成波浪在圆内的效果
mWaveLimitPath.op(mWavePath, Path.Op.INTERSECT);
canvas.drawPath(mWaveLimitPath, paint);
}
//前一次绘制时的进度
private float mPrePercent;
//当前进度值
private String mPercentValue;
private void drawProgress(Canvas canvas) {
float y = mCenterPoint.y - (mPercentPaint.descent() + mPercentPaint.ascent()) / 2;
if (BuildConfig.DEBUG) {
Log.d(TAG, "mPercent = " + mPercent + "; mPrePercent = " + mPrePercent);
}
if (mPrePercent == 0.0f || Math.abs(mPercent - mPrePercent) >= 0.01f) {
mPercentValue = String.format("%.0f%%", mPercent * 100);
mPrePercent = mPercent;
}
canvas.drawText(mPercentValue, mCenterPoint.x, y, mPercentPaint);
if (mHint != null) {
float hy = mCenterPoint.y * 2 / 3 - (mHintPaint.descent() + mHintPaint.ascent()) / 2;
canvas.drawText(mHint.toString(), mCenterPoint.x, hy, mHintPaint);
}
}
public float getMaxValue() {
return mMaxValue;
}
public void setMaxValue(float maxValue) {
mMaxValue = maxValue;
}
public float getValue() {
return mValue;
}
/**
* 设置当前值
*
* @param value
*/
public void setValue(float value) {
if (value > mMaxValue) {
value = mMaxValue;
}
float start = mPercent;
float end = value / mMaxValue;
Log.d(TAG, "setValue, value = " + value + ";start = " + start + "; end = " + end);
startAnimator(start, end, mDarkWaveAnimTime);
}
private void startAnimator(final float start, float end, long animTime) {
Log.d(TAG, "startAnimator,value = " + mValue
+ ";start = " + start + ";end = " + end + ";time = " + animTime);
//当start=0且end=0时,不需要启动动画
if (start == 0 && end == 0) {
return;
}
mProgressAnimator = ValueAnimator.ofFloat(start, end);
mProgressAnimator.setDuration(animTime);
mProgressAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mPercent = (float) animation.getAnimatedValue();
if (mPercent == 0.0f || mPercent == 1.0f) {
stopWaveAnimator();
} else {
startWaveAnimator();
}
mValue = mPercent * mMaxValue;
if (BuildConfig.DEBUG) {
Log.d(TAG, "onAnimationUpdate: percent = " + mPercent
+ ";value = " + mValue);
}
invalidate();
}
});
mProgressAnimator.start();
}
private void startWaveAnimator() {
startLightWaveAnimator();
startDarkWaveAnimator();
}
private void stopWaveAnimator() {
if (mDarkWaveAnimator != null && mDarkWaveAnimator.isRunning()) {
mDarkWaveAnimator.cancel();
mDarkWaveAnimator.removeAllUpdateListeners();
mDarkWaveAnimator = null;
}
if (mLightWaveAnimator != null && mLightWaveAnimator.isRunning()) {
mLightWaveAnimator.cancel();
mLightWaveAnimator.removeAllUpdateListeners();
mLightWaveAnimator = null;
}
}
private void startLightWaveAnimator() {
if (mLightWaveAnimator != null && mLightWaveAnimator.isRunning()) {
return;
}
mLightWaveAnimator = ValueAnimator.ofFloat(0, 2 * mRadius);
mLightWaveAnimator.setDuration(mLightWaveAnimTime);
mLightWaveAnimator.setRepeatCount(ValueAnimator.INFINITE);
mLightWaveAnimator.setInterpolator(new LinearInterpolator());
mLightWaveAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mLightWaveOffset = (float) animation.getAnimatedValue();
postInvalidate();
}
});
mLightWaveAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
mLightWaveOffset = 0;
}
@Override
public void onAnimationEnd(Animator animation) {
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
mLightWaveAnimator.start();
}
private void startDarkWaveAnimator() {
if (mDarkWaveAnimator != null && mDarkWaveAnimator.isRunning()) {
return;
}
mDarkWaveAnimator = ValueAnimator.ofFloat(0, 2 * mRadius);
mDarkWaveAnimator.setDuration(mDarkWaveAnimTime);
mDarkWaveAnimator.setRepeatCount(ValueAnimator.INFINITE);
mDarkWaveAnimator.setInterpolator(new LinearInterpolator());
mDarkWaveAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mDarkWaveOffset = (float) animation.getAnimatedValue();
postInvalidate();
}
});
mDarkWaveAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
mDarkWaveOffset = 0;
}
@Override
public void onAnimationEnd(Animator animation) {
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
mDarkWaveAnimator.start();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
stopWaveAnimator();
if (mProgressAnimator != null && mProgressAnimator.isRunning()) {
mProgressAnimator.cancel();
mProgressAnimator.removeAllUpdateListeners();
mProgressAnimator = null;
}
}
}
上示意图:
图中黑色的圆为我们要绘制的进度条圆,黑色的曲线为初始状态的的波浪,该波浪使用贝塞尔曲线绘制,其中奇数的点为贝塞尔曲线的起始点,偶数的点为贝塞尔曲线的控制点。例如:1——>2——>3就为一条贝塞尔曲线,1 是起点,2 是控制点,3 是终点。从图中可以看到波浪在园内圆外各一个(1—>5 和 5->9),通过对波浪在 x 轴上做平移,即图中蓝色实线,来实现波浪的动态效果,所以一个波浪的完整动画效果需要有两个波浪来实现。同理,通过控制 y 轴的偏移量,即图中蓝色虚线,可以实现波浪随进度的上涨下降。
贝塞尔曲线上起始点和控制点的计算如下:
/**
* 计算贝塞尔曲线上的起始点和控制点
* @param waveWidth 一个完整波浪的宽度
*/
private Point[] getPoint(float waveWidth) {
Point[] points = new Point[mAllPointCount];
//第1个点特殊处理,即数组的中心
points[mHalfPointCount] = new Point((int) (mCenterPoint.x - mRadius), mCenterPoint.y);
//屏幕内的贝塞尔曲线点
for (int i = mHalfPointCount + 1; i < mAllPointCount; i += 4) {
float width = points[mHalfPointCount].x + waveWidth * (i / 4 - mWaveNum);
points[i] = new Point((int) (waveWidth / 4 + width), (int) (mCenterPoint.y - mWaveHeight));
points[i + 1] = new Point((int) (waveWidth / 2 + width), mCenterPoint.y);
points[i + 2] = new Point((int) (waveWidth * 3 / 4 + width), (int) (mCenterPoint.y + mWaveHeight));
points[i + 3] = new Point((int) (waveWidth + width), mCenterPoint.y);
}
//屏幕外的贝塞尔曲线点
for (int i = 0; i < mHalfPointCount; i++) {
int reverse = mAllPointCount - i - 1;
points[i] = new Point(points[mHalfPointCount].x - points[reverse].x,
points[mHalfPointCount].y * 2 - points[reverse].y);
}
return points;
}
以上,我们已经获取到绘制贝塞尔曲线所需的路径点。接下来,我们就需要来计算出绘制区域,即使用 Path 类。
紫色区域为贝塞尔曲线需要绘制的整体区域。
红色区域为上图紫色区域与圆的交集,也就是波浪要显示的区域
代码如下:
/该方法必须在 Android 19以上的版本才能使用(Path.op())
@TargetApi(Build.VERSION_CODES.KITKAT)
private void drawWave(Canvas canvas, Paint paint, Point[] points, float waveOffset) {
mWaveLimitPath.reset();
mWavePath.reset();
//lockWave 用于判断波浪是否随进度条上涨下降
float height = lockWave ? 0 : mRadius - 2 * mRadius * mPercent;
//moveTo和lineTo绘制出水波区域矩形
mWavePath.moveTo(points[0].x + waveOffset, points[0].y + height);
for (int i = 1; i < mAllPointCount; i += 2) {
mWavePath.quadTo(points[i].x + waveOffset, points[i].y + height,
points[i + 1].x + waveOffset, points[i + 1].y + height);
}
mWavePath.lineTo(points[mAllPointCount - 1].x, points[mAllPointCount - 1].y + height);
//不管如何移动,波浪与圆路径的交集底部永远固定,否则会造成上移的时候底部为空的情况
mWavePath.lineTo(points[mAllPointCount - 1].x, mCenterPoint.y + mRadius);
mWavePath.lineTo(points[0].x, mCenterPoint.y + mRadius);
mWavePath.close();
mWaveLimitPath.addCircle(mCenterPoint.x, mCenterPoint.y, mRadius, Path.Direction.CW);
//取该圆与波浪路径的交集,形成波浪在圆内的效果
mWaveLimitPath.op(mWavePath, Path.Op.INTERSECT);
canvas.drawPath(mWaveLimitPath, paint);
对获取坐标点的代码进行优化之后则是WaveProgress.java代码中贴出的
**
* 从左往右或者从右往左获取贝塞尔点
* @return
*/
private Point[] getPoint(boolean isR2L, float waveWidth) {
..........
.........
........
return isR2L ? MiscUtil.reverse(points) : points;
}
建议大家分析代码时先画从左向右的深色水波(暂时将浅色水波逻辑注释掉),然后再画从右向左的浅色水波.
这样比较好分析.下图中,我将绘制深色水波时,计算处的贝塞尔曲线的各个点的坐标都标出来了.(浅色水波的分析方法一样)要注意,自定义view的坐标原点实在控件所处的父布局的左上角.
好了,至此,自定义带进度的圆环介绍完毕.
参考博文:Android 自定义 View 之圆形进度条总结
原博文代码已全部上传至 Git ,欢迎大家 Star 和 Fork,传送门:CircleProgress。
https://github.com/MyLifeMyTravel/CircleProgress
如果你想看在下添加了更多注解之后的源码,也可以移步下载:
https://download.csdn.net/download/zhangqunshuai/10492766