实现思路:
1、如何实现圆中水面上涨效果:利用Paint的setXfermode属性为PorterDuff.Mode.SRC_IN画出进度所在的矩形与圆的交集实现
2、如何水波纹效果:利用贝塞尔曲线,动态改变波峰值,实现“随着进度的增加,水波纹逐渐变小的效果”
话不多说,看代码。
首先是自定义属性值,有哪些可自定义属性值呢?
圆的背景颜色:circle_color,进度的颜色:progress_color,进度显示文字的颜色:text_color,进度文字的大小:text_size,还有最后一个:波纹最大高度:ripple_topheight
<declare-styleable name="WaterProgressView"><attr name="circle_color" format="color"/><!--圆的颜色--><attr name="progress_color" format="color"/><!--进度的颜色--><attr name="text_color" format="color"/><!--文字的颜色--><attr name="text_size" format="dimension"/><!--文字大小--><attr name="ripple_topheight" format="dimension"/><!--水页涟漪最大高度--></declare-styleable>下面是自定义View:WaterProgressView的部份代码:
public class WaterProgressView extends ProgressBar { //默认圆的背景色 public static final int DEFAULT_CIRCLE_COLOR = 0xff00cccc; //默认进度的颜色 public static final int DEFAULT_PROGRESS_COLOR = 0xff00CC66; //默认文字的颜色 public static final int DEFAULT_TEXT_COLOR = 0xffffffff; //默认文字的大小 public static final int DEFAULT_TEXT_SIZE = 18; //默认的波峰最高点 public static final int DEFAULT_RIPPLE_TOPHEIGHT = 10; private Context mContext; private Canvas mPaintCanvas; private Bitmap mBitmap; //画圆的画笔 private Paint mCirclePaint; //画圆的画笔的颜色 private int mCircleColor; //画进度的画笔 private Paint mProgressPaint; //画进度的画笔的颜色 private int mProgressColor ; //画进度的path private Path mProgressPath; //贝塞尔曲线波峰最大值 private int mRippleTop = 10; //进度文字的画笔 private Paint mTextPaint; //进度文字的颜色 private int mTextColor; private int mTextSize = 18; //目标进度,也就是双击时处理任务的进度,会影响曲线的振幅 private int mTargetProgress = 50; //监听双击和单击事件 private GestureDetector mGestureDetector;}获取自定义属性值:
private void getAttrValue(AttributeSet attrs) {TypedArray ta = mContext.obtainStyledAttributes(attrs, R.styleable.WaterProgressView);mCircleColor = ta.getColor(R.styleable.WaterProgressView_circle_color,DEFAULT_CIRCLE_COLOR); mProgressColor = ta.getColor(R.styleable.WaterProgressView_progress_color,DEFAULT_PROGRESS_COLOR);mTextColor = ta.getColor(R.styleable.WaterProgressView_text_color,DEFAULT_TEXT_COLOR); mTextSize = (int) ta.getDimension(R.styleable.WaterProgressView_text_size, DesityUtils.sp2px(mContext,DEFAULT_TEXT_SIZE));mRippleTop = (int)ta.getDimension(R.styleable.WaterProgressView_ripple_topheight,DesityUtils.dp2px(mContext,DEFAULT_RIPPLE_TOPHEIGHT));ta.recycle();}定义构造函数,注意
mProgressPaint.setXfermode
//当new该类时调用此构造函数public WaterProgressView(Context context) {this(context,null);}//当xml文件中定义该自定义View时调用此构造函数public WaterProgressView(Context context, AttributeSet attrs) {this(context, attrs,0);}public WaterProgressView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);this.mContext = context;getAttrValue(attrs);//初始化画笔的相关属性initPaint();mProgressPath = new Path(); }private void initPaint() {//初始化画圆的paint mCirclePaint = new Paint();mCirclePaint.setColor(mCircleColor);mCirclePaint.setStyle(Paint.Style.FILL);mCirclePaint.setAntiAlias(true);mCirclePaint.setDither(true);//初始化画进度的paint mProgressPaint = new Paint();mProgressPaint.setColor(mProgressColor);mProgressPaint.setAntiAlias(true);mProgressPaint.setDither(true);mProgressPaint.setStyle(Paint.Style.FILL);//其实mProgressPaint画的也是矩形,当设置xfermode为PorterDuff.Mode.SRC_IN后则显示的为圆与进度矩形的交集,则为半圆 mProgressPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));//初始化画进度文字的画笔 mTextPaint = new Paint();mTextPaint.setColor(mTextColor);mTextPaint.setStyle(Paint.Style.FILL);mTextPaint.setAntiAlias(true);mTextPaint.setDither(true);mTextPaint.setTextSize(mTextSize);}
onMeasure()
方法代码:@Overrideprotected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {//使用时,需要明确定义该View的尺寸,即用测量模式为MeasureSpec.EXACTLY int width = MeasureSpec.getSize(widthMeasureSpec);int height = MeasureSpec.getSize(heightMeasureSpec);setMeasuredDimension(width,height);//初始化Bitmap,让所有的drawCircle,drawPath,drawText都draw在该bitmap所在的canvas上,然后再将该bitmap 画在onDraw方法的canvas上, //所以此bitmap的width,height需要减去left,top,right,bottom的padding mBitmap = Bitmap.createBitmap(width-getPaddingLeft()-getPaddingRight(),height- getPaddingTop()-getPaddingBottom(), Bitmap.Config.ARGB_8888);mPaintCanvas = new Canvas(mBitmap);}接下来是核心部份,onDraw中的代码。我们先将Circle,进度条,进度文字draw到自定义canvas的bitmap上,再将此bitmap draw到onDraw方法中的canvas上。drawCircle与drawText应该没什么难度,关键点就在于画进度条,怎么画呢?既然有水波纹效果,有曲线,就用drawPath了。
其中ratio的代码如下,即ratio为当前进度占总进度的百分比
float ratio = getProgress()*1.0f/getMax();因为坐标是从B点向下和向右正向延伸的,则A点的坐标为(width,(1-ratio)*height),其中width为bitmap的宽,height为bitmap的高。我们先将mProgressPath.moveTo到A点,然后从A点顺时针方向确定path的各个关键点,如图,则代码如下:
int rightTop = (int) ((1-ratio)*height);mProgressPath.moveTo(width,rightTop);mProgressPath.lineTo(width,height);mProgressPath.lineTo(0,height);mProgressPath.lineTo(0,rightTop);如此mProgressPath已经lineTo到了C点,需要在A点与C点之间形成水波纹效果,则需要在A点与C点间画贝塞尔曲线。
我们设定波峰最高点为10,则一段波长为40,需要画width*1.0f/40
段这样的曲线,则画曲线的代码如下:
int count = (int) Math.ceil(width*1.0f/(10 *4));for(int i=0; i<count; i++) {mProgressPath.rQuadTo(10,10,2* 10,0); mProgressPath.rQuadTo(10,-10,2* 10,0);}mProgressPath.close();mPaintCanvas.drawPath(mProgressPath,mProgressPaint);
float top = (mTargetProgress-getProgress())*1.0f/mTargetProgress* mRippleTop;所以drawPath的代码更新如下:
float top = (mTargetProgress-getProgress())*1.0f/mTargetProgress* mRippleTop;for(int i=0; i<count; i++) {mProgressPath.rQuadTo(mRippleTop,top,2* mRippleTop,0); mProgressPath.rQuadTo(mRippleTop,-top,2* mRippleTop,0); }如此就能真正实现水面上涨的进度条了。
handler.postDelayed(runnable,time)
,每隔一段时间progress+1
,在runnable中invalidate()
不断更新进度,直到当前progress到达mTargetProgress。/** * 实现双击动画 */private void startDoubleTapAnimation() {setProgress(0);doubleTapHandler.postDelayed(doubleTapRunnable,60);}private Handler doubleTapHandler = new Handler(){@Overridepublic void handleMessage(Message msg) { super.handleMessage(msg);}};//双击处理线程,隔60ms发送一次数据private Runnable doubleTapRunnable = new Runnable() {@Overridepublic void run() { if(getProgress() < mTargetProgress) { invalidate(); setProgress(getProgress()+1); doubleTapHandler.postDelayed(doubleTapRunnable,60); } else { doubleTapHandler.removeCallbacks(doubleTapRunnable); }}};双击效果实现了,那如何实现单击效果呢?单击时要求水面不断涌动一段时间,水面波纹逐渐变小,然后水面变平。我们可以定义一个mSingleTapAnimationCount变量为水面涌动的次数,然后像双击时的处理一样,定义一个Handler隔一段时间发送一次更新界面的message,
mSingleTapAnimationCount--
,然后我们交替地让初始时的波峰一次为正一次为负,则能实现水面涌动的效果。private void startSingleTapAnimation() {isSingleTapAnimation = true;singleTapHandler.postDelayed(singleTapRunnable,200);}private Handler singleTapHandler = new Handler(){@Overridepublic void handleMessage(Message msg) { super.handleMessage(msg);}};//单击处理线程,隔200ms发送一次数据private Runnable singleTapRunnable = new Runnable() {@Overridepublic void run() { if(mSingleTapAnimationCount > 0) { invalidate(); mSingleTapAnimationCount--; singleTapHandler.postDelayed(singleTapRunnable,200); } else { singleTapHandler.removeCallbacks(singleTapRunnable); //是否正在进行单击动画isSingleTapAnimation = false;//重置单击动画运行次数为50次mSingleTapAnimationCount = 50; }}};onDraw中的代码作相应的更改,因单击与双击时drawPath中曲线部分的绘制逻辑不一样,则我们定义一个变量isSingleTapAnimation 区别是正在进行单击动画还是在进行双击动画。
//画进度mProgressPath.reset();//从右上边开始draw pathint rightTop = (int) ((1-ratio)*height);mProgressPath.moveTo(width,rightTop);mProgressPath.lineTo(width,height);mProgressPath.lineTo(0,height);mProgressPath.lineTo(0,rightTop);//画贝塞尔曲线,形成波浪线int count = (int) Math.ceil(width*1.0f/(mRippleTop *4));//不是单击animation状态if(!isSingleTapAnimation&&getProgress()>0) {float top = (mTargetProgress-getProgress())*1.0f/mTargetProgress* mRippleTop;for(int i=0; i<count; i++) { mProgressPath.rQuadTo(mRippleTop,-top,2* mRippleTop,0);mProgressPath.rQuadTo(mRippleTop,top,2* mRippleTop,0);}} else {//单击animation状态,为了将效果放大,将mRippleTop放大2倍 //同时偶数时曲线走向如图所示,奇数时则曲线刚好相反float top = (mSingleTapAnimationCount*1.0f/50)*10;//奇偶数时曲线切换if(mSingleTapAnimationCount%2==0) {for(int i=0; i<count; i++) { mProgressPath.rQuadTo(mRippleTop *2,top*2,2* mRippleTop,0);mProgressPath.rQuadTo(mRippleTop *2,-top*2,2* mRippleTop,0);}} else { for(int i=0; i<count; i++) { mProgressPath.rQuadTo(mRippleTop *2,-top*2,2* mRippleTop,0);mProgressPath.rQuadTo(mRippleTop *2,top*2,2* mRippleTop,0);}}}mProgressPath.close();mPaintCanvas.drawPath(mProgressPath,mProgressPaint);基本上重要的代码与核心逻辑与代码就在上面了。
//自定义bitmap的宽和高int width = getWidth()-getPaddingLeft()-getPaddingRight();int height = getHeight()-getPaddingTop()-getPaddingBottom();//画圆mPaintCanvas.drawCircle(width/2,height/2,height/2,mCirclePaint);2、当drawText时,不是从text的height的中间开始draw的,而是从baseline开始draw的
那如何获取baseline的height坐标呢
Paint.FontMetrics metrics = mTextPaint.getFontMetrics();//因为ascent在baseline之上,所以ascent为负数。descent+ascent为负数,所以是减而不是加float baseLine = height*1.0f/2 - (metrics.descent+metrics.ascent)/2;drawText的全部代码如下:
//画进度文字String text = ((int)(ratio*100))+"%";//获得文字的宽度float textWidth = mTextPaint.measureText(text);Paint.FontMetrics metrics = mTextPaint.getFontMetrics();//descent+ascent为负数,所以是减而不是加float baseLine = height*1.0f/2 - (metrics.descent+metrics.ascent)/2;mPaintCanvas.drawText(text,width/2-textWidth/2,baseLine,mTextPaint);3、因为要顾及到padding,记得将onDraw中的canvas translate到
(getPaddingLeft(),getPaddingTop())
处。canvas.translate(getPaddingLeft(),getPaddingTop());canvas.drawBitmap(mBitmap,0,0,null);最后记得将自定义的bitmap draw到onDraw中的canvas上。到这儿自定义水面上涨效果的进度条于写完了。