点击重置后:
该项目总共实现了三种圆形进度条效果
buildTypes {
debug {
buildConfigField "boolean", "DEBUG", "true"
}
release {
buildConfigField "boolean", "DEBUG", "false"
}
defaultConfig {
buildConfigField "boolean", "DEBUG", "true"
}
attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- 是否开启抗锯齿 -->
<attr name="antiAlias" format="boolean" />
<!-- 圆弧起始角度,3点钟方向为0,顺时针递增,小于0或大于360进行取余 -->
<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" />
<!-- 精度,默认为0 -->
<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" />
<!-- 锁定水波不随圆环进度改变,默认锁定在50%处 -->
<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>
color.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
<color name="dark">#803cbcb7</color>
<color name="light">#800de6e8</color>
<color name="green">#00FF00</color>
<color name="blue">#EE9A00</color>
<color name="red">#EE0000</color>
<integer-array name="gradient_arc_color">
<item>@color/green</item>
<item>@color/blue</item>
<item>@color/red</item>
</integer-array>
</resources>
dimens.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="small">5dp</dimen>
<dimen name="medium">10dp</dimen>
<dimen name="normal">15dp</dimen>
<dimen name="large">20dp</dimen>
<dimen name="xlarge">25dp</dimen>
<dimen name="xxlarge">30dp</dimen>
<!-- text size -->
<dimen name="text_size_35">35sp</dimen>
<dimen name="text_size_34">34sp</dimen>
<dimen name="text_size_33">33sp</dimen>
<dimen name="text_size_32">32sp</dimen>
<dimen name="text_size_31">31sp</dimen>
<dimen name="text_size_30">30sp</dimen>
<dimen name="text_size_29">29sp</dimen>
<dimen name="text_size_28">28sp</dimen>
<dimen name="text_size_26">26sp</dimen>
<dimen name="text_size_25">25sp</dimen>
<dimen name="text_size_24">24sp</dimen>
<dimen name="text_size_23">23sp</dimen>
<dimen name="text_size_22">22sp</dimen>
<dimen name="text_size_21">21sp</dimen>
<dimen name="text_size_20">20sp</dimen>
<dimen name="text_size_19">19sp</dimen>
<dimen name="text_size_18">18sp</dimen>
<dimen name="text_size_17">17sp</dimen>
<dimen name="text_size_16">16sp</dimen>
<dimen name="text_size_15">15sp</dimen>
<dimen name="text_size_14">14sp</dimen>
<dimen name="text_size_13">13sp</dimen>
<dimen name="text_size_12">12sp</dimen>
<dimen name="text_size_11">11sp</dimen>
<dimen name="text_size_10">10sp</dimen>
<dimen name="text_size_9">9sp</dimen>
<dimen name="text_size_8">8sp</dimen>
<dimen name="text_size_7">7sp</dimen>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
</resources>
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<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.example.circularwaterripple.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="重置" />
<com.example.circularwaterripple.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: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.example.circularwaterripple.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"
tools:ignore="MissingClass" />
<com.example.circularwaterripple.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.example.circularwaterripple.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.example.circularwaterripple.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"
tools:ignore="ExtraText" />
</LinearLayout>
</ScrollView>
activity.test.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.example.circularwaterripple.CircleProgress
android:layout_width="400dp"
android:layout_height="400dp"
app:waveHeight="100dp" />
</LinearLayout>
CircleProgress 圆形进度条,类似 QQ 健康中运动步数的 UI 控件
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;
// 圆形进度条,类似 QQ 健康中运动步数的 UI 控件
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) {
/*
从mContext中获取与CircleProgressBar相关的属性。
obtainStyledAttributes是一个用于从给定的attrs和指定的styleable资源ID数组中获取属性的方法
*/
TypedArray typedArray = mContext.obtainStyledAttributes(attrs, R.styleable.CircleProgressBar);
/*
从TypedArray中获取CircleProgressBar_antiAlias属性,并将其值赋给antiAlias。
如果该属性在XML中未定义,那么将使用默认值Constant.ANTI_ALIAS。
getBoolean是一个方法,用于从TypedArray中获取布尔类型的属性
*/
antiAlias = typedArray.getBoolean(R.styleable.CircleProgressBar_antiAlias, Constant.ANTI_ALIAS);
/*
从TypedArray中获取CircleProgressBar_hint属性,并将其值赋给mHint。
getString是一个方法,用于从TypedArray中获取字符串类型的属性
*/
mHint = typedArray.getString(R.styleable.CircleProgressBar_hint);
/*
从TypedArray中获取CircleProgressBar_hintColor属性,并将其值赋给mHintColor。
如果该属性在XML中未定义,那么将使用默认值Color.BLACK。
getColor是一个方法,用于从TypedArray中获取颜色类型的属性
*/
mHintColor = typedArray.getColor(R.styleable.CircleProgressBar_hintColor, Color.BLACK);
/*
从TypedArray中获取CircleProgressBar_hintSize属性,并将其值赋给mHintSize。
如果该属性在XML中未定义,那么将使用默认值Constant.DEFAULT_HINT_SIZE。
getDimension是一个方法,用于从TypedArray中获取尺寸类型的属性
*/
mHintSize = typedArray.getDimension(R.styleable.CircleProgressBar_hintSize, Constant.DEFAULT_HINT_SIZE);
/*
从 TypedArray 中获取一个浮点数。这个浮点数的键是 R.styleable.CircleProgressBar_value,
如果在这个 TypedArray 中找不到这个键,那么就会返回默认值 Constant.DEFAULT_VALUE。
getFloat 方法将把这个键对应的值转换为浮点数,并且把这个浮点数赋值给 mValue
*/
mValue = typedArray.getFloat(R.styleable.CircleProgressBar_value, Constant.DEFAULT_VALUE);
mMaxValue = typedArray.getFloat(R.styleable.CircleProgressBar_maxValue, Constant.DEFAULT_MAX_VALUE);
//内容数值精度格式
//从typedArray中获取CircleProgressBar_precision的整数值,如果CircleProgressBar_precision在typedArray中不存在,那么就会使用默认值0
mPrecision = typedArray.getInt(R.styleable.CircleProgressBar_precision, 0);
//MiscUtil.getPrecisionFormat()方法根据mPrecision的值返回一个格式化对象,用于后续的格式化操作
mPrecisionFormat = MiscUtil.getPrecisionFormat(mPrecision);
//getColor是一个方法,用于从TypedArray中获取颜色类型的属性
mValueColor = typedArray.getColor(R.styleable.CircleProgressBar_valueColor, Color.BLACK);
//getDimension是一个方法,用于从TypedArray中获取尺寸类型的属性
mValueSize = typedArray.getDimension(R.styleable.CircleProgressBar_valueSize, Constant.DEFAULT_VALUE_SIZE);
//getString是一个方法,用于从TypedArray中获取字符串类型的属性
mUnit = typedArray.getString(R.styleable.CircleProgressBar_unit);
//getColor是一个方法,用于从TypedArray中获取颜色类型的属性
mUnitColor = typedArray.getColor(R.styleable.CircleProgressBar_unitColor, Color.BLACK);
getDimension是一个方法,用于从TypedArray中获取尺寸类型的属性
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);
//获取一个颜色数组的ID
int gradientArcColors = typedArray.getResourceId(R.styleable.CircleProgressBar_arcColors, 0);
if (gradientArcColors != 0) {
try {
int[] gradientColors = getResources().getIntArray(gradientArcColors);
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() {
mHintPaint = new TextPaint();
// 设置抗锯齿,会消耗较大资源,绘制图形速度会变慢。
mHintPaint.setAntiAlias(antiAlias);
// 设置绘制文字大小
mHintPaint.setTextSize(mHintSize);
// 设置画笔颜色
mHintPaint.setColor(mHintColor);
// 从中间向两边绘制,不需要再次计算文字
mHintPaint.setTextAlign(Paint.Align.CENTER);
mValuePaint = new TextPaint();
mValuePaint.setAntiAlias(antiAlias);
mValuePaint.setTextSize(mValueSize);
mValuePaint.setColor(mValueColor);
// 设置Typeface对象,即字体风格,包括粗体,斜体以及衬线体,非衬线体等
//Typeface 是一个表示字体类型的类
mValuePaint.setTypeface(Typeface.DEFAULT_BOLD);
//setTextAlign() 方法并传入 Paint.Align.CENTER,可以将 mValuePaint 的文本对齐方式设置为居中对齐
mValuePaint.setTextAlign(Paint.Align.CENTER);
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);
}
/*
调用了 MiscUtil.measure 方法来计算宽度和高度,然后使用 setMeasuredDimension 方法设置 View 的大小。
setMeasuredDimension 的参数是测量后的宽度和高度。
*/
@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);
//减去圆弧的宽度,否则会造成部分圆弧绘制在外围
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);
mHintOffset = mCenterPoint.y - mRadius * mTextOffsetPercentInRadius + getBaselineOffsetFromY(mHintPaint);
mUnitOffset = mCenterPoint.y + mRadius * mTextOffsetPercentInRadius + getBaselineOffsetFromY(mUnitPaint);
updateArcPaint();
Log.d(TAG, "onSizeChanged: 控件大小 = " + "(" + w + ", " + h + ")"
+ "圆心坐标 = " + mCenterPoint.toString()
+ ";圆半径 = " + mRadius
+ ";圆的外接矩形 = " + mRectF.toString());
}
private float getBaselineOffsetFromY(Paint paint) {
return MiscUtil.measureTextHeight(paint) / 2;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawText(canvas);
drawArc(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) {
// 绘制背景圆弧
// 从进度圆弧结束的地方开始重新绘制,优化性能
//save保存当前的绘图状态
canvas.save();
float currentAngle = mSweepAngle * mPercent;
//在画布(Canvas)上应用旋转操作。旋转的中心点是(mCenterPoint.x, mCenterPoint.y),旋转的角度是mStartAngle
canvas.rotate(mStartAngle, mCenterPoint.x, mCenterPoint.y);
//在画布上绘制一个弧形。
//弧形的边界是由mRectF定义的,起始角度是currentAngle,扫过的角度是mSweepAngle - currentAngle + 2。
//这个弧形不会闭合,因为参数中的false表示不闭合
canvas.drawArc(mRectF, currentAngle, mSweepAngle - currentAngle + 2, false, mBgArcPaint);
// 第一个参数 oval 为 RectF 类型,即圆弧显示区域
// startAngle 和 sweepAngle 均为 float 类型,分别表示圆弧起始角度和圆弧度数
// 3点钟方向为0度,顺时针递增
// 如果 startAngle < 0 或者 > 360,则相当于 startAngle % 360
// useCenter:如果为True时,在绘制圆弧时将圆心包括在内,通常用来绘制扇形
canvas.drawArc(mRectF, 2, currentAngle, false, mArcPaint);
//恢复到之前保存的图形状态时
canvas.restore();
}
//更新圆弧画笔
private void updateArcPaint() {
// 设置渐变
//SweepGradient是Android中的一种Shader(着色器),它创建的渐变效果是以一个指定的中心点进行的扫描渐变
mSweepGradient = new SweepGradient(mCenterPoint.x, mCenterPoint.y, mGradientColors, null);
//设置Shader,给圆弧添加颜色渐变效果
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;
}
// 设置当前值
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) {
//ValueAnimator对象用于动画的创建,ValueAnimator.ofFloat(start, end)表示这个动画将会在start和end之间进行变化
mAnimator = ValueAnimator.ofFloat(start, end);
//设置动画的持续时间
mAnimator.setDuration(animTime);
//addUpdateListener是添加一个监听器,监听动画的更新
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//animation.getAnimatedValue(): 这是调用动画对象的getAnimatedValue()方法。这个方法通常返回动画的当前值
mPercent = (float) animation.getAnimatedValue();
mValue = mPercent * mMaxValue;
//检查是否处于调试模式
if (BuildConfig.DEBUG) {
Log.d(TAG, "onAnimationUpdate: percent = " + mPercent
+ ";currentAngle = " + (mSweepAngle * mPercent)
+ ";value = " + mValue);
}
//告诉Android系统这个视图需要被重新绘制
invalidate();
}
});
//启动了 mAnimator
mAnimator.start();
}
// 获取最大值
public float getMaxValue() {
return mMaxValue;
}
//设置最大值
public void setMaxValue(float maxValue) {
mMaxValue = maxValue;
}
// 获取精度
public int getPrecision() {
return mPrecision;
}
public void setPrecision(int precision) {
mPrecision = precision;
mPrecisionFormat = MiscUtil.getPrecisionFormat(precision);
}
public int[] getGradientColors() {
return mGradientColors;
}
// 设置渐变
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();
//释放资源
}
}
Constant
public class Constant {
public static final boolean ANTI_ALIAS = true;
public static final int DEFAULT_SIZE = 150;
public static final int DEFAULT_START_ANGLE = 270;
public static final int DEFAULT_SWEEP_ANGLE = 360;
public static final int DEFAULT_ANIM_TIME = 1000;
public static final int DEFAULT_MAX_VALUE = 100;
public static final int DEFAULT_VALUE = 50;
public static final int DEFAULT_HINT_SIZE = 15;
public static final int DEFAULT_UNIT_SIZE = 30;
public static final int DEFAULT_VALUE_SIZE = 15;
public static final int DEFAULT_ARC_WIDTH = 15;
public static final int DEFAULT_WAVE_HEIGHT = 40;
}
DialProgress 带有刻度的圆形进度条
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;
// 带有刻度的圆形进度条
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;
//将常量 Constant.DEFAULT_SIZE 的值从设备独立像素(dp)转换为屏幕的实际像素(px),然后将结果赋值给 mDefaultSize
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) {
//obtainStyledAttributes是一个用于从给定的attrs和指定的styleable资源ID数组中获取属性的方法
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.DialProgress);
//getBoolean是一个方法,用于从TypedArray中获取布尔类型的属性
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);
//getString是一个方法,用于从TypedArray中获取字符串类型的属性
mHint = typedArray.getString(R.styleable.DialProgress_hint);
//getColor是一个方法,用于从TypedArray中获取颜色类型的属性
mHintColor = typedArray.getColor(R.styleable.DialProgress_hintColor, Color.BLACK);
mHintSize = typedArray.getDimension(R.styleable.DialProgress_hintSize, Constant.DEFAULT_HINT_SIZE);
//getDimension是一个方法,用于从TypedArray中获取尺寸类型的属性
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);
//调用TypedArray的 getResourceId 方法。这个方法接受两个参数:一个是资源名,另一个是默认值
int gradientArcColors = typedArray.getResourceId(R.styleable.DialProgress_arcColors, 0);
if (gradientArcColors != 0) {
try {
//getResources获取资源
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);
//设置画笔的样式,为FILL,FILL_OR_STROKE,或STROKE
mBgArcPaint.setStyle(Paint.Style.STROKE);
//设置画笔粗细
mBgArcPaint.setStrokeWidth(mArcWidth);
// 当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的图形样式,如圆形样式
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);
}
/*
调用了 MiscUtil.measure 方法来计算宽度和高度,然后使用 setMeasuredDimension 方法设置 View 的大小。
setMeasuredDimension 的参数是测量后的宽度和高度。
*/
@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;
//save保存当前的绘图状态
canvas.save();
//在画布(Canvas)上应用旋转操作。旋转的中心点是(mCenterPoint.x, mCenterPoint.y),旋转的角度是270
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();
}
private void drawDial(Canvas canvas) {
int total = (int) (mSweepAngle / mDialIntervalDegree);
//save保存当前的绘图状态
canvas.save();
//在画布(Canvas)上应用旋转操作。旋转的中心点是(mCenterPoint.x, mCenterPoint.y),旋转的角度是mStartAngle
canvas.rotate(mStartAngle, mCenterPoint.x, mCenterPoint.y);
for (int i = 0; i <= total; i++) {
//drawLine方法则用于在画布上绘制一条线
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;
}
// 设置当前值
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) {
//ValueAnimator对象用于动画的创建,ValueAnimator.ofFloat(start, end)表示这个动画将会在start和end之间进行变化
mAnimator = ValueAnimator.ofFloat(start, end);
//设置动画的持续时间
mAnimator.setDuration(animTime);
//addUpdateListener是添加一个监听器,监听动画的更新
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//animation.getAnimatedValue(): 这是调用动画对象的getAnimatedValue()方法。这个方法通常返回动画的当前值
mPercent = (float) animation.getAnimatedValue();
mValue = mPercent * mMaxValue;
//检查是否处于调试模式
if (BuildConfig.DEBUG) {
Log.d(TAG, "onAnimationUpdate: percent = " + mPercent
+ ";currentAngle = " + (mSweepAngle * mPercent)
+ ";value = " + mValue);
}
//告诉Android系统这个视图需要被重新绘制
invalidate();
}
});
//启动了 mAnimator
mAnimator.start();
}
public int[] getGradientColors() {
return mGradientColors;
}
public void setGradientColors(int[] gradientColors) {
mGradientColors = gradientColors;
updateArcPaint();
}
public void reset() {
startAnimator(mPercent, 0.0f, 1000L);
}
}
MainActivity
import android.annotation.SuppressLint;
import android.graphics.Color;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import androidx.appcompat.app.AppCompatActivity;
import java.util.Random;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private final static int[] COLORS = new int[]{Color.RED, Color.YELLOW, Color.GREEN, Color.BLUE};
private Button mBtnResetAll;
private CircleProgress mCircleProgress1, mCircleProgress2, mCircleProgress3;
private DialProgress mDialProgress;
private WaveProgress mWaveProgress;
private Random mRandom;
@SuppressLint("MissingInflatedId")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mBtnResetAll = (Button) findViewById(R.id.btn_reset_all);
mCircleProgress1 = (CircleProgress) findViewById(R.id.circle_progress_bar1);
mCircleProgress2 = (CircleProgress) findViewById(R.id.circle_progress_bar2);
mCircleProgress3 = (CircleProgress) findViewById(R.id.circle_progress_bar3);
mDialProgress = (DialProgress) findViewById(R.id.dial_progress_bar);
mWaveProgress = (WaveProgress) findViewById(R.id.wave_progress_bar);
mBtnResetAll.setOnClickListener(this);
mCircleProgress1.setOnClickListener(this);
mCircleProgress2.setOnClickListener(this);
mCircleProgress3.setOnClickListener(this);
mDialProgress.setOnClickListener(this);
mWaveProgress.setOnClickListener(this);
mRandom = new Random();
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.btn_reset_all) {
mCircleProgress1.reset();
mCircleProgress2.reset();
mCircleProgress3.reset();
mDialProgress.reset();
mWaveProgress.reset();
}
else if (v.getId() == R.id.circle_progress_bar1) {
mCircleProgress1.setValue(mRandom.nextInt((int) mCircleProgress1.getMaxValue()));
}
else if (v.getId() == R.id.circle_progress_bar2) {
mCircleProgress2.setValue(mRandom.nextFloat() * mCircleProgress2.getMaxValue());
}
else if (v.getId() == R.id.circle_progress_bar3) {
//在代码中动态改变渐变色,可能会导致颜色跳跃
mCircleProgress3.setGradientColors(COLORS);
mCircleProgress3.setValue(mRandom.nextFloat() * mCircleProgress3.getMaxValue());
}
else if (v.getId() == R.id.dial_progress_bar) {
mDialProgress.setValue(mRandom.nextFloat() * mDialProgress.getMaxValue());
}
else if (v.getId() == R.id.wave_progress_bar){
mWaveProgress.setValue(mRandom.nextFloat() * mWaveProgress.getMaxValue());
}
}
}
WaveProgress 圆形水波进度条
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;
// 水波进度条
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) {
//将常量 Constant.DEFAULT_SIZE 的值从设备独立像素(dp)转换为屏幕的实际像素(px),然后将结果赋值给 mDefaultSize
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) {
//obtainStyledAttributes是一个用于从给定的attrs和指定的styleable资源ID数组中获取属性的方法
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.WaveProgress);
//getBoolean是一个方法,用于从TypedArray中获取布尔类型的属性
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);
//getDimension是一个方法,用于从TypedArray中获取尺寸类型的属性
mValueSize = typedArray.getDimension(R.styleable.WaveProgress_valueSize, Constant.DEFAULT_VALUE_SIZE);
//getColor是一个方法,用于从TypedArray中获取颜色类型的属性
mValueColor = typedArray.getColor(R.styleable.WaveProgress_valueColor, Color.BLACK);
//getString是一个方法,用于从TypedArray中获取字符串类型的属性
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);
//getResources获取资源
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() {
mHintPaint = new TextPaint();
// 设置抗锯齿,会消耗较大资源,绘制图形速度会变慢。
mHintPaint.setAntiAlias(antiAlias);
// 设置绘制文字大小
mHintPaint.setTextSize(mHintSize);
// 设置画笔颜色
mHintPaint.setColor(mHintColor);
//setTextAlign() 方法并传入 Paint.Align.CENTER,可以将 mValuePaint 的文本对齐方式设置为居中对齐
mHintPaint.setTextAlign(Paint.Align.CENTER);
mCirclePaint = new Paint();
// 设置抗锯齿
mCirclePaint.setAntiAlias(antiAlias);
//设置画笔粗细
mCirclePaint.setStrokeWidth(mCircleWidth);
//设置画笔的样式,为FILL,FILL_OR_STROKE,或STROKE
mCirclePaint.setStyle(Paint.Style.STROKE);
// 当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的图形样式,如圆形样式
mCirclePaint.setStrokeCap(Paint.Cap.ROUND);
mWavePaint = new Paint();
mWavePaint.setAntiAlias(antiAlias);
mWavePaint.setStyle(Paint.Style.FILL);
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();
}
/*
调用了 MiscUtil.measure 方法来计算宽度和高度,然后使用 setMeasuredDimension 方法设置 View 的大小。
setMeasuredDimension 的参数是测量后的宽度和高度。
*/
@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;
mDarkPoints = getPoint(false, waveWidth);
mLightPoints = getPoint(isR2L, waveWidth);
}
// 从左往右或者从右往左获取贝塞尔点
private Point[] getPoint(boolean isR2L, float waveWidth) {
Point[] points = new Point[mAllPointCount];
//第1个点特殊处理,即数组的中点
points[mHalfPointCount] = new Point((int) (mCenterPoint.x + (isR2L ? mRadius : -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((isR2L ? 2 : 1) * points[mHalfPointCount].x - points[reverse].x,
points[mHalfPointCount].y * 2 - points[reverse].y);
}
//对从右向左的贝塞尔点数组反序,方便后续处理
return isR2L ? MiscUtil.reverse(points) : points;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawCircle(canvas);
drawLightWave(canvas);
drawDarkWave(canvas);
drawProgress(canvas);
}
// 绘制圆环
private void drawCircle(Canvas canvas) {
//save保存当前的绘图状态
canvas.save();
//在画布(Canvas)上应用旋转操作。旋转的中心点是(mCenterPoint.x, mCenterPoint.y),旋转的角度是270
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();
}
// 绘制深色波浪(贝塞尔曲线)
private void drawDarkWave(Canvas canvas) {
mWavePaint.setColor(mDarkWaveColor);
drawWave(canvas, mWavePaint, mDarkPoints, mDarkWaveOffset);
}
// 绘制浅色波浪(贝塞尔曲线)
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) {
//reset()用于重置或清理对象状态的方法
mWaveLimitPath.reset();
mWavePath.reset();
float height = lockWave ? 0 : mRadius - 2 * mRadius * mPercent;
//moveTo和lineTo绘制出水波区域矩形
//moveTo() 方法将路径的结束点移动到指定的位置,并以此为新的开始进行新的绘制
mWavePath.moveTo(points[0].x + waveOffset, points[0].y + height);
for (int i = 1; i < mAllPointCount; i += 2) {
//quadTo() 方法在 mWavePath 上绘制一个二次贝塞尔曲线
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);
//不管如何移动,波浪与圆路径的交集底部永远固定,否则会造成上移的时候底部为空的情况
//lineTo()方法用于在当前的绘画位置开始一条新的线,然后移动到指定的坐标
mWavePath.lineTo(points[mAllPointCount - 1].x, mCenterPoint.y + mRadius);
mWavePath.lineTo(points[0].x, mCenterPoint.y + mRadius);
//关闭路径,即连接路径的起点和终点
mWavePath.close();
//addCircle()方法用于在当前的绘画位置添加一个圆形
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) {
//String类的format方法,该方法将一个格式化的字符串返回
//"%.0f%%"表示一个浮点数应该被格式化为没有小数位的百分数
mPercentValue = String.format("%.0f%%", mPercent * 100);
mPrePercent = mPercent;
}
canvas.drawText(mPercentValue, mCenterPoint.x, y, mPercentPaint);
if (mHint != null) {
//descent和ascent两个函数可能分别返回字体的下沉(即,基线以下的最低点)和上升(基线以上的最高点)
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;
}
// 设置当前值
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;
}
//ValueAnimator对象用于动画的创建,ValueAnimator.ofFloat(start, end)表示这个动画将会在start和end之间进行变化
mProgressAnimator = ValueAnimator.ofFloat(start, end);
//设置动画的持续时间
mProgressAnimator.setDuration(animTime);
//addUpdateListener是添加一个监听器,监听动画的更新
mProgressAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//animation.getAnimatedValue()从名为 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);
}
//告诉Android系统这个视图需要被重新绘制
invalidate();
}
});
//启动了 mAnimator
mProgressAnimator.start();
}
private void startWaveAnimator() {
startLightWaveAnimator();
startDarkWaveAnimator();
}
private void stopWaveAnimator() {
//调用isRunning()方法检查mDarkWaveAnimator是否正在运行。如果动画正在播放,则此方法将返回true,否则返回false
if (mDarkWaveAnimator != null && mDarkWaveAnimator.isRunning()) {
//调用cancel()方法取消正在运行的动画
mDarkWaveAnimator.cancel();
//调用removeAllUpdateListeners()方法移除所有注册的更新监听器
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);
//设置了动画的重复次数 无限重复(ValueAnimator.INFINITE
mLightWaveAnimator.setRepeatCount(ValueAnimator.INFINITE);
//设置了动画的插值器
mLightWaveAnimator.setInterpolator(new LinearInterpolator());
//添加了一个更新监听器
mLightWaveAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//从一个动画(animation)中获取一个值,然后将该值赋给一个变量(mLightWaveOffset)
mLightWaveOffset = (float) animation.getAnimatedValue();
//postInvalidate()用于将组件的无效区域(invalid regions)发送到事件队列中,以便在稍后进行重绘(repaint)。
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);
//设置了动画的重复次数 无限重复(ValueAnimator.INFINITE
mDarkWaveAnimator.setRepeatCount(ValueAnimator.INFINITE);
//设置了动画的插值器
mDarkWaveAnimator.setInterpolator(new LinearInterpolator());
//添加了一个更新监听器
mDarkWaveAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//从一个动画(animation)中获取一个值,然后将该值赋给一个变量(mLightWaveOffset)
mDarkWaveOffset = (float) animation.getAnimatedValue();
//postInvalidate()用于将组件的无效区域(invalid regions)发送到事件队列中,以便在稍后进行重绘(repaint)。
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();
//调用isRunning()方法检查mDarkWaveAnimator是否正在运行。如果动画正在播放,则此方法将返回true,否则返回false
if (mProgressAnimator != null && mProgressAnimator.isRunning()) {
//调用cancel()方法取消正在运行的动画
mProgressAnimator.cancel();
//调用removeAllUpdateListeners()方法移除所有注册的更新监听器
mProgressAnimator.removeAllUpdateListeners();
mProgressAnimator = null;
}
}
public void reset() {
startAnimator(mPercent, 0.0f, 1000L);
}
}
MiscUtil
import android.content.Context;
import android.graphics.Paint;
import android.view.View;
public class MiscUtil {
//测量 View
public static int measure(int measureSpec, int defaultSize) {
int result = defaultSize;
int specMode = View.MeasureSpec.getMode(measureSpec);
int specSize = View.MeasureSpec.getSize(measureSpec);
if (specMode == View.MeasureSpec.EXACTLY) {
result = specSize;
} else if (specMode == View.MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
return result;
}
// dip 转换成px
public static int dipToPx(Context context, float dip) {
float density = context.getResources().getDisplayMetrics().density;
return (int) (dip * density + 0.5f * (dip >= 0 ? 1 : -1));
}
// 获取数值精度格式化字符串
public static String getPrecisionFormat(int precision) {
return "%." + precision + "f";
}
// 反转数组
public static <T> T[] reverse(T[] arrays) {
if (arrays == null) {
return null;
}
int length = arrays.length;
for (int i = 0; i < length / 2; i++) {
T t = arrays[i];
arrays[i] = arrays[length - i - 1];
arrays[length - i - 1] = t;
}
return arrays;
}
// 测量文字高度
public static float measureTextHeight(Paint paint) {
Paint.FontMetrics fontMetrics = paint.getFontMetrics();
return (Math.abs(fontMetrics.ascent) - fontMetrics.descent);
}
}