贝塞尔曲线:写一个浪起来的进度条
效果展示
效果展示开发过程
1、分析设计稿
在完成一个自定义控件是首先应当分析设计稿,并做好撸码的准备。这一步尤为重要!必须有一个清晰的思路才能在写代码时少走弯路!
通过以上的动图,大概可以分析出以下信息:
1、自绘控件。所谓自绘控件就是通过onDraw画出来的控件。组合控件就是通过系统的控件(比如Textview,ImageView等)进行组合而成的控件。从图中可以看出用系统控件组合比较难实现效果。
2、波浪的效果实现,需要用到贝塞尔曲线。
贝塞尔曲线原理:
http://www.jianshu.com/p/1af5c3655fa3
http://www.jianshu.com/p/55c721887568
波浪的效果实现:
http://www.jianshu.com/p/f7ec5296053e
3、它有半径、颜色、文字大小等等属性。
4、从功能上看,这是一个展示进度的进度条。当进度越大,波浪就会越高。
实际开发中,通过分析并不能完全了解这个控件的所有属性。但是它的功能,开发思路要从分析中明确。
2、添加自定义属性
关于自定义属性:
http://www.jianshu.com/p/5ff5bb9a88f4
通过分析定义自定义属性。方便设置控件的属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="WaveProgressView">
//半径
<attr name="radius" format="dimension"/>
//进度
<attr name="progress" format="integer"/>
//进度数字的颜色
<attr name="textColor" format="color"/>
//波浪的颜色
<attr name="waveColor" format="color"/>
//边框的颜色
<attr name="borderColor" format="color"/>
//是否隐藏进度文字
<attr name="hideText" format="boolean"/>
//边框的宽度
<attr name="borderWidth" format="dimension"/>
//进度文字的大小
<attr name="textSize" format="dimension"/>
//水波浪的高度
<attr name="waveHeight" format="dimension"/>
</declare-styleable>
</resources>
获取属性,初始化属性:
定义属性的全局变量,并且初始化值,便于随时使用。
/**
* 默认波长
*/
private static final int DEFAULT_RADIUS = 100;
/**
* 默认波峰和波谷的高度
*/
private static final int DEFAULT_WAVE_HEIGHT = 5;
/**
* 默认的最大的进度
*/
private static final int DEFAULT_MAX_PROGRESS = 100;
/**
* 默认边框宽度
*/
private static final int DEFAULT_BORDER_WIDTH = 2;
/**
* 默认的进度字体大小
*/
private static final int DEFAULT_TEXT_SIZE = 16;
//进度
private int mProgress;
//半径
private int mRadius = DEFAULT_RADIUS;
//进度条的高度
private int mProgressHeight;
//文字的大小
private int mTextSize;
//波高
private int mWaveHeight;
//文字颜色
private int mTextColor;
//波浪的颜色
private int mWaveColor;
//圆形边框的颜色
private int mBorderColor;
//圆形边框的宽度
private int borderWidth;
//是否隐藏进度文字
private boolean isHideProgressText = false;
//进度条的贝塞尔曲线
private Path mBerzierPath;
//用于裁剪的Path
private Path mCirclePath;
// 画圆的画笔
private Paint mCirclePaint;
// 画文字的笔
private Paint mTextPaint;
// 画波浪的笔
private Paint mWavePaint;
// 文字的区域
private Rect mTextRect;
... ...
private void getAttrs(AttributeSet attrs) {
TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.WaveProgressView);
mRadius = ta.getDimensionPixelSize(R.styleable.WaveProgressView_radius, DEFAULT_RADIUS);
mProgressHeight = mRadius * 2;
mTextColor = ta.getColor(R.styleable.WaveProgressView_textColor, Color.BLACK);
mWaveColor = ta.getColor(R.styleable.WaveProgressView_waveColor, Color.RED);
mBorderColor = ta.getColor(R.styleable.WaveProgressView_borderColor, Color.RED);
borderWidth = ta.getDimensionPixelOffset(R.styleable.WaveProgressView_borderWidth, dp2px(DEFAULT_BORDER_WIDTH));
mTextSize = ta.getDimensionPixelSize(R.styleable.WaveProgressView_textSize, sp2px(DEFAULT_TEXT_SIZE));
mWaveHeight = ta.getDimensionPixelSize(R.styleable.WaveProgressView_waveHeight, dp2px(DEFAULT_WAVE_HEIGHT));
mProgress = ta.getInteger(R.styleable.WaveProgressView_progress, 0);
isHideProgressText = ta.getBoolean(R.styleable.WaveProgressView_hideText, false);
ta.recycle();
}
3、重写onMeasure方法,确定控件大小.
这个控件对宽高的计算要求不高,调用setMeasuredDimension确定控件的宽高就行。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//圆形的进度条,正好是正方形的内切圆(这边暂时没有考虑padding的影响)
setMeasuredDimension(mProgressHeight, mProgressHeight);
}
4、重写onDraw,依次绘出所有的元素
onDraw中绘制各种元素时,一定要分步骤依次绘制。绘制会因为各种因素导致绘制达不到效果,所以每次写好绘制一个元素之后最好编译看效果。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mBerzierPath.reset();
//画曲线
mBerzierPath.moveTo(-mProgressHeight + mMoveX, getWaveY());
for (int i = -mProgressHeight; i < mProgressHeight * 3; i += mProgressHeight) {
//圆内最长一个波长
mBerzierPath.rQuadTo(mProgressHeight / 4, mWaveHeight, mProgressHeight / 2, 0);
mBerzierPath.rQuadTo(mProgressHeight / 4, -mWaveHeight, mProgressHeight / 2, 0);
}
mBerzierPath.lineTo(mProgressHeight, mProgressHeight);
mBerzierPath.lineTo(0, getHeight());
mBerzierPath.close();
//裁剪一个圆形的区域
canvas.clipPath(mCirclePath);
canvas.drawPath(mBerzierPath, mWavePaint);
//画圆
canvas.drawCircle(mRadius, mRadius, mRadius, mCirclePaint);
//开启属性动画使波浪浪起来(这里只需要启动一次)
if (!isStartAnimation) {
isStartAnimation = true;
startAnimation();
}
//画文字(画文字可不是直接drawText这么简单,要找基线去画)
String progress = mProgress + "%";
if (isHideProgressText) {
progress = "";
}
mTextPaint.getTextBounds(progress, 0, progress.length(), mTextRect);
canvas.drawText(progress, mRadius - mTextRect.width() / 2,
mRadius + mTextRect.height() / 2, mTextPaint);
}
使波浪动起来的属性动画:
private void startAnimation() {
mAnimator = ValueAnimator.ofInt(0, mProgressHeight);
mAnimator.setDuration(2000);
mAnimator.setRepeatCount(ValueAnimator.INFINITE);
mAnimator.setInterpolator(new LinearInterpolator());
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mMoveX = (int) animation.getAnimatedValue();
postInvalidate();
}
});
mAnimator.start();
}
这个控件有两个重点:
1、波浪绘制和波浪'浪'起来的效果。
实际在绘制的时候在控件左右两边多画了一个波浪,然后把波浪向右移动,就造成了波浪在上下浪的错觉。
这里面有说明:
http://www.jianshu.com/p/f7ec5296053e
这里可以这样理解:
波浪的理解2、进度的实现
通过当前进度和最大进度获取进度的比例。通过比例去计算水波纹的高度。
private int getWaveY() {
float scale = mProgress * 1f / DEFAULT_MAX_PROGRESS * 1f;
if (scale >= 1) {
return 0;
} else {
int height = (int) (scale * mProgressHeight);
return mProgressHeight - height;
}
}
5、为实际的业务添加一些公开的方法
提供一些公开的方法来实现外界对功能的调用。
/**
* 设置进度
*
* @param progress
*/
public void setProgress(int progress) {
this.mProgress = progress;
postInvalidate();
}
/**
* 设置字体的颜色
*
* @param color
*/
public void setTextColor(int color) {
mTextPaint.setColor(color);
}
/**
* 设置波浪的颜色
*
* @param color
*/
public void setWaveColor(int color) {
mWavePaint.setColor(color);
}
/**
* 设置
*
* @param color
*/
public void setBorderColor(int color) {
mCirclePaint.setColor(color);
}
/**
* 设置隐藏进度文字
*
* @param flag
*/
public void hideProgressText(boolean flag) {
isHideProgressText = flag;
}
/**
* 获取当前进度
*
* @return
*/
public int getProgress() {
return mProgress;
}
6、代码完成之后的测试和bug修改。
写完代码之后多次测试,修复bug。
代码地址:
感谢各位程序员老爷看完我的博客!
https://github.com/AxeChen/WaveProgress (赠人star,手有余香)