这几天写了在写一个记账软件,想要用一个圆形的进度条来展示收支百分比,特此记录。
效果类似于这样:
属性名 | 属性类型 | 备注 |
circle_background | color | 圆形背景颜色 |
circle_radius | dimension | 圆形半径 |
dark_wave_color | color | 深色波浪颜色 |
light_wave_color | color | 浅色波浪颜色 |
circle_border_color | color | 圆形边框颜色 |
text_color | color | 文字颜色 |
wave_speed | integer | 波浪的速度 |
wave_height | dimension | 波浪的高度 |
自定义 View 之 自定义属性 这篇文章写了如何自定属性。
在 values\attrs.xml 中创建属性:
<!-- 自定义圆形波浪进度条属性-->
<declare-styleable name="CircleWaveProgress">
<!--圆形背景颜色-->
<attr name="circle_background" format="color" />
<!--圆形半径-->
<attr name="circle_radius" format="dimension" />
<!--深色波浪颜色-->
<attr name="dark_wave_color" format="color" />
<!--浅色波浪颜色-->
<attr name="light_wave_color" format="color" />
<!--圆形边框颜色-->
<attr name="circle_border_color" format="color" />
<!--文字颜色-->
<attr name="text_color" format="color" />
<!--波浪的速度-->
<attr name="wave_speed" format="integer" />
<!--波浪的高度-->
<attr name="wave_height" format="dimension" />
<!--是否位于布局中心,系统自带属性-->
<attr name="android:layout_centerInParent" />
</declare-styleable>
然后创建一个 View 的子类 CirclrWaveProgress,在类中获取我们设置的属性值:
public class CircleWaveProgress extends View {
private AttributeSet mAttrs;
private TypedArray typedArray;
/**
* 属性 - 圆的半径
*/
private float circleRadius;
/**
* 属性 - 圆的背景色
*/
private int circleBackground;
/**
* 属性 - 深色波浪颜色
*/
private int darkWaveColor;
/**
* 属性 - 浅色波浪颜色
*/
private int lightWaveColor;
/**
* 属性 - 圆外围边框颜色
*/
private int circleBorderColor;
/**
* 属性 - 文字颜色
*/
private int textColor;
/**
* 属性 - 波浪速度
*/
private int waveSpeed;
/**
* 属性 - 波浪高度
*/
private float waveHeight;
/**
* 属性 - 控件是否位于布局中心
*/
private boolean centerInParent;
public CircleWaveProgress(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initAttrs(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 在测量时确定半径,如果没有设置半径,则半径设置为长宽较短的那个值的1/2
float defaultCircleRadius = widthSize > heightSize ? heightSize / 2 : widthSize / 2;
circleRadius = typedArray.getDimension(R.styleable.CircleWaveProgress_circle_radius, defaultCircleRadius);
}
private void initAttrs(Context context, AttributeSet attrs) {
typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleWaveProgress);
circleBackground = typedArray.getInt(R.styleable.CircleWaveProgress_circle_background, Color.RED);
darkWaveColor = typedArray.getInt(R.styleable.CircleWaveProgress_dark_wave_color, Color.BLUE);
lightWaveColor = typedArray.getInt(R.styleable.CircleWaveProgress_light_wave_color, Color.GRAY);
circleBorderColor = typedArray.getInt(R.styleable.CircleWaveProgress_circle_border_color, Color.GREEN);
textColor = typedArray.getInt(R.styleable.CircleWaveProgress_text_color, Color.BLACK);
waveSpeed = typedArray.getInt(R.styleable.CircleWaveProgress_wave_speed, 2000);
waveHeight = typedArray.getInt(R.styleable.CircleWaveProgress_wave_height, 2000);
centerInParent = typedArray.getBoolean(R.styleable.CircleWaveProgress_android_layout_centerInParent, false);
//记得回收 TypedArray,否则容易造成内存泄漏
typedArray.recycle();
}
}
所有绘制操作都是在 onDraw 方法中进行的,我们重写 onDraw 方法。
所谓贝塞尔曲线,通俗的讲就是通过三个点来精确的画出曲线,其中两个点为起点和终点,另一个点为控制点,这个图可以很直观的展示:
可以看到,通过移动三个点可以在不同位置绘制不同的曲线,我们可以用它来绘制波浪。
Android Path 类提供了绘制贝塞尔曲线的方法:
quadTo(float x1, float y1, float x2, float y2)
其参数从左到右依次是 控制点 X 坐标,控制点 Y 坐标,终点 X 坐标,终点 Y 坐标,而起始点坐标则为默认原点(也就是 0.0),可以通过 Path.moveTo 方法来设置起始点。
现在我们来确定一下几个点的位置:
我们现在屏幕上标出五个点:
再在两个点中间标出控制点:
他们的位置大致是这样的
通过这几个点去画曲线:
PointF point1 = new PointF(-circleRadius * 2, circleRadius);
PointF point2 = new PointF(-circleRadius, circleRadius);
PointF point3 = new PointF(0, circleRadius);
PointF point4 = new PointF(circleRadius, circleRadius);
PointF point5 = new PointF(circleRadius * 2, circleRadius);
PointF cPoint1 = new PointF(-circleRadius * 3 / 2, circleRadius + waveHeight);
PointF cPoint2 = new PointF(-circleRadius / 2, circleRadius - waveHeight);
PointF cPoint3 = new PointF(circleRadius / 2, circleRadius + waveHeight);
PointF cPoint4 = new PointF(circleRadius * 3 / 2, circleRadius - waveHeight);
path.moveTo(point1.x, point1.y);
path.quadTo(cPoint1.x, cPoint1.y, point2.x, point2.y);
path.quadTo(cPoint2.x, cPoint2.y, point3.x, point3.y);
path.quadTo(cPoint3.x, cPoint3.y, point4.x, point4.y);
path.quadTo(cPoint4.x, cPoint4.y, point5.x, point5.y);
canvas.drawPath(path, darkWavePaint);
这个时候绘制的效果是这样的:
红框内的内容没有显示出来,等到让波浪动起来的时候会用到。
接下来的工作是让波浪动起来,我们让每个点逐渐向右移动,这样屏幕外的内容逐渐进入到屏幕内,形成波浪效果。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
path.reset();
PointF point1 = new PointF(-circleRadius * 2, circleRadius);
PointF point2 = new PointF(-circleRadius, circleRadius);
PointF point3 = new PointF(0, circleRadius);
PointF point4 = new PointF(circleRadius, circleRadius);
PointF point5 = new PointF(circleRadius * 2, circleRadius);
PointF cPoint1 = new PointF(-circleRadius * 3 / 2, circleRadius + waveHeight);
PointF cPoint2 = new PointF(-circleRadius / 2, circleRadius - waveHeight);
PointF cPoint3 = new PointF(circleRadius / 2, circleRadius + waveHeight);
PointF cPoint4 = new PointF(circleRadius * 3 / 2, circleRadius - waveHeight);
drawPointF(canvas, point1, circleBorderPaint);
drawPointF(canvas, point2, circleBorderPaint);
drawPointF(canvas, point3, circleBorderPaint);
drawPointF(canvas, point4, circleBorderPaint);
drawPointF(canvas, point5, circleBorderPaint);
drawPointF(canvas, cPoint1, circleBorderPaint);
drawPointF(canvas, cPoint2, circleBorderPaint);
drawPointF(canvas, cPoint3, circleBorderPaint);
drawPointF(canvas, cPoint4, circleBorderPaint);
path.moveTo(point1.x + offSet, point1.y);
path.quadTo(cPoint1.x + offSet, cPoint1.y, point2.x + offSet, point2.y);
path.quadTo(cPoint2.x + offSet, cPoint2.y, point3.x + offSet, point3.y);
path.quadTo(cPoint3.x + offSet, cPoint3.y, point4.x + offSet, point4.y);
path.quadTo(cPoint4.x + offSet, cPoint4.y, point5.x + offSet, point5.y);
canvas.drawPath(path, darkWavePaint);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
startWaveAnimator();
}
private void startWaveAnimator() {
ValueAnimator animator = ValueAnimator.ofFloat(0, circleRadius * 2);
animator.setDuration(waveSpeed * 1000);
animator.setInterpolator(new LinearInterpolator());
animator.setRepeatCount(-1);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
offSet = (float) animation.getAnimatedValue();
postInvalidate();
}
});
animator.start();
}
代码也很简单,offSet 值逐渐递增,并且通知系统重绘,系统重绘时,path.reset 重置路径,然后让每个点的 X 坐标加上 offSet,这样就让波浪动起来了。
效果是这样:
这样看起来还不像波浪,我们把路径闭合,就像了:
path.lineTo(circleRadius * 2, circleRadius * 2);
path.lineTo(0, circleRadius * 2);
path.close();
效果是这样:
看起来丑丑的对不对,接下来用到绘制中的剪裁功能了,我们只需要显示一个圆形就可以了,完整代码如下:
public class CircleWaveProgress extends View {
private AttributeSet mAttrs;
private TypedArray typedArray;
/**
* 属性 - 圆的半径
*/
private float circleRadius;
/**
* 属性 - 圆的背景色
*/
private int circleBackground;
/**
* 属性 - 深色波浪颜色
*/
private int darkWaveColor;
/**
* 属性 - 浅色波浪颜色
*/
private int lightWaveColor;
/**
* 属性 - 圆外围边框颜色
*/
private int circleBorderColor;
/**
* 属性 - 圆外围边框宽度
*/
private float circleBorderWidth;
/**
* 属性 - 文字颜色
*/
private int textColor;
/**
* 属性 - 波浪速度
*/
private int waveSpeed;
/**
* 属性 - 波浪高度
*/
private float waveHeight;
/**
* 属性 - 控件是否位于布局中心
*/
private boolean centerInParent;
/**
* 绘制圆的画笔
*/
private Paint circlePaint;
/**
* 绘制圆边框的画笔
*/
private Paint circleBorderPaint;
/**
* 波浪偏移量
*/
private float offSet;
/**
* 深色波浪画笔
*/
private Paint darkWavePaint;
/**
* 圆形背景画笔
*/
private Paint circleBackgroundPaint;
private Path path;
public CircleWaveProgress(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initAttrs(context, attrs);
initDrawTool();
}
// 初始化控件绘制的画笔、路径等
private void initDrawTool() {
circlePaint = new Paint();
circlePaint.setColor(circleBackground);
circlePaint.setStyle(Paint.Style.FILL_AND_STROKE);
circleBorderPaint = new Paint();
circleBorderPaint.setColor(circleBorderColor);
circleBorderPaint.setStyle(Paint.Style.STROKE);
circleBorderPaint.setStrokeWidth(circleBorderWidth);
darkWavePaint = new Paint();
darkWavePaint.setColor(darkWaveColor);
path = new Path();
circleBackgroundPaint = new Paint();
circleBackgroundPaint.setColor(circleBackground);
circleBackgroundPaint.setStyle(Paint.Style.FILL);
}
//初始化控件属性
private void initAttrs(Context context, AttributeSet attrs) {
typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleWaveProgress);
circleBackground = typedArray.getInt(R.styleable.CircleWaveProgress_circle_background, Color.RED);
darkWaveColor = typedArray.getInt(R.styleable.CircleWaveProgress_dark_wave_color, Color.BLUE);
lightWaveColor = typedArray.getInt(R.styleable.CircleWaveProgress_light_wave_color, Color.GRAY);
circleBorderColor = typedArray.getInt(R.styleable.CircleWaveProgress_circle_border_color, Color.GREEN);
circleBorderWidth = typedArray.getDimension(R.styleable.CircleWaveProgress_circle_border_width, 20);
textColor = typedArray.getInt(R.styleable.CircleWaveProgress_text_color, Color.BLACK);
waveSpeed = typedArray.getInt(R.styleable.CircleWaveProgress_wave_speed, 2);
waveHeight = typedArray.getDimension(R.styleable.CircleWaveProgress_wave_height, 0);
centerInParent = typedArray.getBoolean(R.styleable.CircleWaveProgress_android_layout_centerInParent, false);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 在测量时确定半径,如果没有设置半径,则半径设置为长宽较短的那个值的1/2
float defaultCircleRadius = widthSize > heightSize ? heightSize / 2 : widthSize / 2;
circleRadius = typedArray.getDimension(R.styleable.CircleWaveProgress_circle_radius, defaultCircleRadius);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
path.reset();
canvas.drawCircle(circleRadius, circleRadius, circleRadius, circleBackgroundPaint);
path.addCircle(circleRadius, circleRadius, circleRadius, Path.Direction.CCW);
canvas.clipPath(path);
PointF point1 = new PointF(-circleRadius * 2, circleRadius);
PointF point2 = new PointF(-circleRadius, circleRadius);
PointF point3 = new PointF(0, circleRadius);
PointF point4 = new PointF(circleRadius, circleRadius);
PointF point5 = new PointF(circleRadius * 2, circleRadius);
PointF cPoint1 = new PointF(-circleRadius * 3 / 2, circleRadius + waveHeight);
PointF cPoint2 = new PointF(-circleRadius / 2, circleRadius - waveHeight);
PointF cPoint3 = new PointF(circleRadius / 2, circleRadius + waveHeight);
PointF cPoint4 = new PointF(circleRadius * 3 / 2, circleRadius - waveHeight);
path.moveTo(point1.x + offSet, point1.y);
path.quadTo(cPoint1.x + offSet, cPoint1.y, point2.x + offSet, point2.y);
path.quadTo(cPoint2.x + offSet, cPoint2.y, point3.x + offSet, point3.y);
path.quadTo(cPoint3.x + offSet, cPoint3.y, point4.x + offSet, point4.y);
path.quadTo(cPoint4.x + offSet, cPoint4.y, point5.x + offSet, point5.y);
path.lineTo(circleRadius * 2, circleRadius * 2);
path.lineTo(0, circleRadius * 2);
path.close();
canvas.drawPath(path, darkWavePaint);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
startDarkWaveAnimator();
}
private void startDarkWaveAnimator() {
ValueAnimator animator = ValueAnimator.ofFloat(0, circleRadius * 2);
animator.setDuration(waveSpeed * 1000);
animator.setInterpolator(new LinearInterpolator());
animator.setRepeatCount(-1);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
offSet = (float) animation.getAnimatedValue();
postInvalidate();
}
});
animator.start();
}
}
效果如下,效果是不是出来了?
画了一个波浪,我们再画一个,这次让波浪从右往左移动,给人一种错落有致的感觉,会比较好看。
在设置一条从右往左的波浪,方法和上面一样,不赘述。效果如下:
问题来了,我们现在波浪是在圆的正中央,我们想要设置波浪所占百分比,也很简单,控制每个点的 Y 坐标就可以了嘛。
我们设置一个 waveProgress 属性,然后将 Y 轴坐标设置为 半径2(1-waveProgress/100) 就 OK 啦~
代码如下:
public class CircleWaveProgress extends View {
private AttributeSet mAttrs;
private TypedArray typedArray;
/**
* 属性 - 圆的半径
*/
private float circleRadius;
/**
* 属性 - 圆的背景色
*/
private int circleBackground;
/**
* 属性 - 深色波浪颜色
*/
private int darkWaveColor;
/**
* 属性 - 浅色波浪颜色
*/
private int lightWaveColor;
/**
* 属性 - 圆外围边框颜色
*/
private int circleBorderColor;
/**
* 属性 - 圆外围边框宽度
*/
private float circleBorderWidth;
/**
* 属性 - 文字颜色
*/
private int textColor;
/**
* 属性 - 波浪速度
*/
private int waveSpeed;
/**
* 属性 - 波浪高度
*/
private float waveHeight;
/**
* 属性 - 控件是否位于布局中心
*/
private boolean centerInParent;
/**
* 属性 - 波浪进度
*/
private float waveProgress;
/**
* 绘制圆的画笔
*/
private Paint circlePaint;
/**
* 绘制圆边框的画笔
*/
private Paint circleBorderPaint;
/**
* 波浪偏移量
*/
private float offSet;
/**
* 深色波浪画笔
*/
private Paint darkWavePaint;
/**
* 浅色波浪画笔
*/
private Paint lightWavePaint;
/**
* 圆形背景画笔
*/
private Paint circleBackgroundPaint;
/**
* 深色浅色波浪路径
*/
private Path darkWavePath;
private Path lightWavePath;
/**
* 切割圆形路径
*/
private Path circleClipPath;
private float waveProgressPercent;
private ValueAnimator animator;
public CircleWaveProgress(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initAttrs(context, attrs);
initDrawTool();
}
// 初始化控件绘制的画笔、路径等
private void initDrawTool() {
//圆形边框画笔及属性
circleBorderPaint = new Paint();
circleBorderPaint.setColor(circleBorderColor);
circleBorderPaint.setStyle(Paint.Style.FILL);
circleBorderPaint.setAntiAlias(true);
//圆形背景画笔及属性
circleBackgroundPaint = new Paint();
circleBackgroundPaint.setColor(circleBackground);
circleBackgroundPaint.setStyle(Paint.Style.FILL);
circleBackgroundPaint.setAntiAlias(true);
//深色波浪画笔及属性
darkWavePaint = new Paint();
darkWavePaint.setColor(darkWaveColor);
darkWavePaint.setAntiAlias(true);
//深色波浪路径
darkWavePath = new Path();
//浅色波浪画笔及属性
lightWavePaint = new Paint();
lightWavePaint.setColor(lightWaveColor);
lightWavePaint.setAntiAlias(true);
//浅色波浪路径
lightWavePath = new Path();
//切割圆形路径
circleClipPath = new Path();
}
//初始化控件属性
private void initAttrs(Context context, AttributeSet attrs) {
typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleWaveProgress);
circleBackground = typedArray.getInt(R.styleable.CircleWaveProgress_circle_background, Color.RED);
darkWaveColor = typedArray.getInt(R.styleable.CircleWaveProgress_dark_wave_color, Color.BLUE);
lightWaveColor = typedArray.getInt(R.styleable.CircleWaveProgress_light_wave_color, Color.GRAY);
circleBorderColor = typedArray.getInt(R.styleable.CircleWaveProgress_circle_border_color, Color.GREEN);
circleBorderWidth = typedArray.getDimension(R.styleable.CircleWaveProgress_circle_border_width, 20);
textColor = typedArray.getInt(R.styleable.CircleWaveProgress_text_color, Color.BLACK);
waveSpeed = typedArray.getInt(R.styleable.CircleWaveProgress_wave_speed, 2);
waveHeight = typedArray.getDimension(R.styleable.CircleWaveProgress_wave_height, 0);
centerInParent = typedArray.getBoolean(R.styleable.CircleWaveProgress_android_layout_centerInParent, false);
waveProgress = typedArray.getFloat(R.styleable.CircleWaveProgress_wave_progress, 0);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 在测量时确定半径,如果没有设置半径,则半径设置为长宽较短的那个值的1/2
float defaultCircleRadius = widthSize > heightSize ? heightSize / 2 : widthSize / 2;
circleRadius = typedArray.getDimension(R.styleable.CircleWaveProgress_circle_radius, defaultCircleRadius);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
darkWavePath.reset();
lightWavePath.reset();
//画背景圆
canvas.drawCircle(circleRadius + circleBorderWidth, circleRadius + circleBorderWidth, circleRadius, circleBackgroundPaint);
//切割为圆形
circleClipPath.addCircle(circleRadius + circleBorderWidth, circleRadius + circleBorderWidth, circleRadius, Path.Direction.CCW);
canvas.clipPath(circleClipPath);
//浅色波浪
lightWavePath.moveTo(0 - offSet, circleRadius * 2 * getWaveProgressPercent());
lightWavePath.quadTo(circleRadius / 2 - offSet, circleRadius * 2 * getWaveProgressPercent() - waveHeight, circleRadius - offSet, circleRadius * 2 * getWaveProgressPercent());
lightWavePath.quadTo(circleRadius * 3 / 2 - offSet, circleRadius * 2 * getWaveProgressPercent() + waveHeight, circleRadius * 2 - offSet, circleRadius * 2 * getWaveProgressPercent());
lightWavePath.quadTo(circleRadius * 5 / 2 - offSet, circleRadius * 2 * getWaveProgressPercent() - waveHeight, circleRadius * 3 - offSet, circleRadius * 2 * getWaveProgressPercent());
lightWavePath.quadTo(circleRadius * 7 / 2 - offSet, circleRadius * 2 * getWaveProgressPercent() + waveHeight, circleRadius * 4 - offSet, circleRadius * 2 * getWaveProgressPercent());
lightWavePath.lineTo(circleRadius * 2 + circleBorderWidth * 2, circleRadius * 2 + circleBorderWidth * 2);
lightWavePath.lineTo(0, circleRadius * 2 + circleBorderWidth * 2);
lightWavePath.close();
canvas.drawPath(lightWavePath, lightWavePaint);
//深色波浪
darkWavePath.moveTo(-circleRadius * 2, circleRadius * 2 * getWaveProgressPercent());
darkWavePath.quadTo(-circleRadius * 3 / 2 + offSet, circleRadius * 2 * getWaveProgressPercent() + waveHeight, -circleRadius + offSet, circleRadius * 2 * getWaveProgressPercent());
darkWavePath.quadTo(-circleRadius / 2 + offSet, circleRadius * 2 * getWaveProgressPercent() - waveHeight, 0 + offSet, circleRadius * 2 * getWaveProgressPercent());
darkWavePath.quadTo(circleRadius / 2 + offSet, circleRadius * 2 * getWaveProgressPercent() + waveHeight, circleRadius + offSet, circleRadius * 2 * getWaveProgressPercent());
darkWavePath.quadTo(circleRadius * 3 / 2 + offSet, circleRadius * 2 * getWaveProgressPercent() - waveHeight, circleRadius * 2 + offSet, circleRadius * 2 * getWaveProgressPercent());
darkWavePath.lineTo(circleRadius * 2 + circleBorderWidth * 2, circleRadius * 2 + circleBorderWidth * 2);
darkWavePath.lineTo(0, circleRadius * 2 + circleBorderWidth * 2);
darkWavePath.close();
canvas.drawPath(darkWavePath, darkWavePaint);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
startWaveAnimator();
}
private void startWaveAnimator() {
animator = ValueAnimator.ofFloat(0, circleRadius * 2);
animator.setDuration(waveSpeed * 1000);
animator.setInterpolator(new LinearInterpolator());
animator.setRepeatCount(-1);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
offSet = (float) animation.getAnimatedValue();
postInvalidate();
}
});
animator.start();
}
public void setWaveProgress(float waveProgress) {
this.waveProgress = waveProgress;
postInvalidate();
}
public float getWaveProgress() {
return waveProgress;
}
public void setWaveProgressPercent(float waveProgressPercent) {
this.waveProgressPercent = waveProgressPercent;
}
public float getWaveProgressPercent() {
return 1 - (waveProgress / 100);
}
}
效果如下:
这个没啥可说的,就是画一个圆罢了,自定义 View 简单图形的绘制
我们想要外边框也跟着进度变化,Canvas 也提供了画弧线的响应方法 canvas.drawArc
,直接绘制背景圆形之前绘制一个包含圆心的圆弧,绘制角度随着变化更改,同时通知系统重绘即可。
//画外围圆圈
RectF rect = new RectF(0, 0, (circleRadius + circleBorderWidth) * 2, (circleRadius + circleBorderWidth) * 2);
canvas.drawArc(rect, 270, 360 * (1 - getWaveProgressPercent()), true, circleBorderPaint);
//画背景圆
canvas.drawCircle(circleRadius + circleBorderWidth, circleRadius + circleBorderWidth, circleRadius, circleBackgroundPaint);
//切割为圆形
circleClipPath.addCircle(circleRadius + circleBorderWidth, circleRadius + circleBorderWidth, circleRadius, Path.Direction.CCW);
canvas.clipPath(circleClipPath);
效果如下:
Android 同样提供了绘制文字的 API:canvas.drawText
,但是这个绘制文字有一些需要注意的地方,我们先来画一个看看效果,然后再调整:
canvas.drawCircle(circleRadius + circleBorderWidth, circleRadius + circleBorderWidth, circleRadius + circleBorderWidth, circleBackgroundPaint);
TextPaint paint = new TextPaint();
paint.setColor(textColor);
paint.setTextSize(textSize);
String progressStr = getWaveProgress() + "%";
canvas.drawText(progressStr, circleRadius + circleBorderWidth, circleRadius + circleBorderWidth, paint);
这个时候的效果是这样的:
我们发现,文字并不是位于圆中央的,具体原因,这篇文章里说的很清楚:HenCoder Android 开发进阶:自定义 View 1-3 drawText() 文字的绘制
处理方法也很简单,我们获取到文字的宽和高,然后调整绘制的坐标不就 OK 了吗?!
获取文字宽高的方法看这里 android测量文字的宽高
获取到文字的宽高之后,在绘制时将原本的 X 坐标设置 -width/2,Y 坐标设置 +height/2,这样不就是在正中央绘制文字了么。
最终代码是这样的:
public class CircleWaveProgress extends View {
private AttributeSet mAttrs;
private TypedArray typedArray;
/**
* 属性 - 圆的半径
*/
private float circleRadius;
/**
* 属性 - 圆的背景色
*/
private int circleBackground;
/**
* 属性 - 深色波浪颜色
*/
private int darkWaveColor;
/**
* 属性 - 浅色波浪颜色
*/
private int lightWaveColor;
/**
* 属性 - 圆外围边框颜色
*/
private int circleBorderColor;
/**
* 属性 - 圆外围边框宽度
*/
private float circleBorderWidth;
/**
* 属性 - 文字颜色
*/
private int textColor;
/**
* 属性 - 波浪速度
*/
private int waveSpeed;
/**
* 属性 - 波浪高度
*/
private float waveHeight;
/**
* 属性 - 控件是否位于布局中心
*/
private boolean centerInParent;
/**
* 属性 - 波浪进度
*/
private float waveProgress;
/**
* 属性 - 文字尺寸
*/
private float textSize;
/**
* 绘制圆的画笔
*/
private Paint circlePaint;
/**
* 绘制圆边框的画笔
*/
private Paint circleBorderPaint;
/**
* 波浪偏移量
*/
private float offSet;
/**
* 深色波浪画笔
*/
private Paint darkWavePaint;
/**
* 浅色波浪画笔
*/
private Paint lightWavePaint;
/**
* 圆形背景画笔
*/
private Paint circleBackgroundPaint;
/**
* 深色浅色波浪路径
*/
private Path darkWavePath;
private Path lightWavePath;
/**
* 切割圆形路径
*/
private Path circleClipPath;
/**
* 进度百分比
*/
private float waveProgressPercent;
/**
* 波浪动画
*/
private ValueAnimator animator;
/**
* 测量文字宽高的 Rect
*/
private Rect textMeasureRect;
/**
* 外围圆弧 Rect
*/
private RectF borderArcRect;
public CircleWaveProgress(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initAttrs(context, attrs);
initDrawTool();
}
/**
* 初始化控件绘制的画笔、路径等
*/
private void initDrawTool() {
//圆形边框画笔及属性
circleBorderPaint = new Paint();
circleBorderPaint.setColor(circleBorderColor);
circleBorderPaint.setStyle(Paint.Style.FILL);
circleBorderPaint.setAntiAlias(true);
//圆形背景画笔及属性
circleBackgroundPaint = new Paint();
circleBackgroundPaint.setColor(circleBackground);
circleBackgroundPaint.setStyle(Paint.Style.FILL);
circleBackgroundPaint.setAntiAlias(true);
//深色波浪画笔及属性
darkWavePaint = new Paint();
darkWavePaint.setColor(darkWaveColor);
darkWavePaint.setAntiAlias(true);
//深色波浪路径
darkWavePath = new Path();
//浅色波浪画笔及属性
lightWavePaint = new Paint();
lightWavePaint.setColor(lightWaveColor);
lightWavePaint.setAntiAlias(true);
//浅色波浪路径
lightWavePath = new Path();
//切割圆形路径
circleClipPath = new Path();
//测量文字宽高 Rect
textMeasureRect = new Rect();
}
/**
* 初始化控件属性
* @param context
* @param attrs
*/
private void initAttrs(Context context, AttributeSet attrs) {
typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleWaveProgress);
circleBackground = typedArray.getInt(R.styleable.CircleWaveProgress_circle_background, Color.RED);
darkWaveColor = typedArray.getInt(R.styleable.CircleWaveProgress_dark_wave_color, Color.BLUE);
lightWaveColor = typedArray.getInt(R.styleable.CircleWaveProgress_light_wave_color, Color.GRAY);
circleBorderColor = typedArray.getInt(R.styleable.CircleWaveProgress_circle_border_color, Color.GREEN);
circleBorderWidth = typedArray.getDimension(R.styleable.CircleWaveProgress_circle_border_width, 20);
textColor = typedArray.getInt(R.styleable.CircleWaveProgress_text_color, Color.BLACK);
waveSpeed = typedArray.getInt(R.styleable.CircleWaveProgress_wave_speed, 2);
waveHeight = typedArray.getDimension(R.styleable.CircleWaveProgress_wave_height, 0);
centerInParent = typedArray.getBoolean(R.styleable.CircleWaveProgress_android_layout_centerInParent, false);
waveProgress = typedArray.getFloat(R.styleable.CircleWaveProgress_wave_progress, 0);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 在测量时确定半径,如果没有设置半径,则半径设置为长宽较短的那个值的1/2
float defaultCircleRadius = widthSize > heightSize ? heightSize / 2 : widthSize / 2;
circleRadius = typedArray.getDimension(R.styleable.CircleWaveProgress_circle_radius, defaultCircleRadius);
textSize = typedArray.getDimension(R.styleable.CircleWaveProgress_text_size, circleRadius / 2);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
darkWavePath.reset();
lightWavePath.reset();
//画外围圆圈
borderArcRect = new RectF(0, 0, (circleRadius + circleBorderWidth) * 2, (circleRadius + circleBorderWidth) * 2);
canvas.drawArc(borderArcRect, 270, 360 * (1 - getWaveProgressPercent()), true, circleBorderPaint);
//画背景圆
canvas.drawCircle(circleRadius + circleBorderWidth, circleRadius + circleBorderWidth, circleRadius, circleBackgroundPaint);
//切割为圆形
circleClipPath.addCircle(circleRadius + circleBorderWidth, circleRadius + circleBorderWidth, circleRadius, Path.Direction.CCW);
canvas.clipPath(circleClipPath);
//浅色波浪
lightWavePath.moveTo(0 - offSet, circleRadius * 2 * getWaveProgressPercent());
lightWavePath.quadTo(circleRadius / 2 - offSet, circleRadius * 2 * getWaveProgressPercent() - waveHeight, circleRadius - offSet, circleRadius * 2 * getWaveProgressPercent());
lightWavePath.quadTo(circleRadius * 3 / 2 - offSet, circleRadius * 2 * getWaveProgressPercent() + waveHeight, circleRadius * 2 - offSet, circleRadius * 2 * getWaveProgressPercent());
lightWavePath.quadTo(circleRadius * 5 / 2 - offSet, circleRadius * 2 * getWaveProgressPercent() - waveHeight, circleRadius * 3 - offSet, circleRadius * 2 * getWaveProgressPercent());
lightWavePath.quadTo(circleRadius * 7 / 2 - offSet, circleRadius * 2 * getWaveProgressPercent() + waveHeight, circleRadius * 4 - offSet, circleRadius * 2 * getWaveProgressPercent());
lightWavePath.lineTo(circleRadius * 2 + circleBorderWidth * 2, circleRadius * 2 + circleBorderWidth * 2);
lightWavePath.lineTo(0, circleRadius * 2 + circleBorderWidth * 2);
lightWavePath.close();
canvas.drawPath(lightWavePath, lightWavePaint);
//深色波浪
darkWavePath.moveTo(-circleRadius * 2, circleRadius * 2 * getWaveProgressPercent());
darkWavePath.quadTo(-circleRadius * 3 / 2 + offSet, circleRadius * 2 * getWaveProgressPercent() + waveHeight, -circleRadius + offSet, circleRadius * 2 * getWaveProgressPercent());
darkWavePath.quadTo(-circleRadius / 2 + offSet, circleRadius * 2 * getWaveProgressPercent() - waveHeight, 0 + offSet, circleRadius * 2 * getWaveProgressPercent());
darkWavePath.quadTo(circleRadius / 2 + offSet, circleRadius * 2 * getWaveProgressPercent() + waveHeight, circleRadius + offSet, circleRadius * 2 * getWaveProgressPercent());
darkWavePath.quadTo(circleRadius * 3 / 2 + offSet, circleRadius * 2 * getWaveProgressPercent() - waveHeight, circleRadius * 2 + offSet, circleRadius * 2 * getWaveProgressPercent());
darkWavePath.lineTo(circleRadius * 2 + circleBorderWidth * 2, circleRadius * 2 + circleBorderWidth * 2);
darkWavePath.lineTo(0, circleRadius * 2 + circleBorderWidth * 2);
darkWavePath.close();
canvas.drawPath(darkWavePath, darkWavePaint);
//画文字
TextPaint textPaint = new TextPaint();
textPaint.setColor(textColor);
textPaint.setTextSize(textSize);
textPaint.getTextBounds(getWaveProgress() + "%", 0, (getWaveProgress() + "%").length(), textMeasureRect);
canvas.drawText(getWaveProgress() + "%", circleRadius + circleBorderWidth - textMeasureRect.width() / 2, circleRadius + circleBorderWidth + textMeasureRect.height() / 2, textPaint);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
startWaveAnimator();
}
private void startWaveAnimator() {
animator = ValueAnimator.ofFloat(0, circleRadius * 2);
animator.setDuration(waveSpeed * 1000);
animator.setInterpolator(new LinearInterpolator());
animator.setRepeatCount(-1);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
offSet = (float) animation.getAnimatedValue();
postInvalidate();
}
});
animator.start();
}
public void setWaveProgress(float waveProgress) {
this.waveProgress = waveProgress;
postInvalidate();
}
public float getWaveProgress() {
return waveProgress;
}
public void setWaveProgressPercent(float waveProgressPercent) {
this.waveProgressPercent = waveProgressPercent;
}
public float getWaveProgressPercent() {
return 1 - (waveProgress / 100);
}
}
效果是这样的:
代码都做了很详细的注释,不多做解释。
现在波浪的进度是通过调用 View 的 setWavePogress 方法来设置的,现在想要改成直接设置一个进度(譬如 85%) 然后波浪会直接在固定时间内直接从 0% 涨到我们设置的进度。
我们直接添加一个方法 setFinalProgressPercent:
public void setFinalProgressPercent(int finalProgress){
ValueAnimator animator = ValueAnimator.ofInt(0,finalProgress);
animator.setDuration(2000);
animator.setRepeatCount(0);
animator.setInterpolator(new LinearInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
setWaveProgress((int)animation.getAnimatedValue());
}
});
animator.start();
}
我直接设置了动画时间为 2 秒,也可以通过属性设置,添加一个属性就是了。