在完成一个自定义控件是首先应当分析设计稿,并做好撸码的准备。这一步尤为重要!必须有一个清晰的思路才能在写代码时少走弯路! 通过以上的动图,大概可以分析出以下信息: 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、从功能上看,这是一个展示进度的进度条。当进度越大,波浪就会越高。
实际开发中,通过分析并不能完全了解这个控件的所有属性。但是它的功能,开发思路要从分析中明确。
关于自定义属性: 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(); }这个控件对宽高的计算要求不高,调用setMeasuredDimension确定控件的宽高就行。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //圆形的进度条,正好是正方形的内切圆(这边暂时没有考虑padding的影响) setMeasuredDimension(mProgressHeight, mProgressHeight); }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; } }提供一些公开的方法来实现外界对功能的调用。
/** * 设置进度 * * @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; }写完代码之后多次测试,修复bug。
代码地址: https://github.com/AxeChen/WaveProgress