自定义View实现水面上涨效果ProgressBar

实现效果如下

water_progress.gif

实现思路:
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()方法代码:

@Override
protected 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了。drawPath的流程如下:

draw_path_01.png

其中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点间画贝塞尔曲线。

贝塞尔曲线.png

我们设定波峰最高点为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);

这样就能画出水面上涨且有波纹效果的进度条了。但我们还要实现随着水面上涨,越接近目标进度,水面波纹应该越来越小,则应该把10抽出为变量定义为mRippleTop等初始时波峰最大值,然后定义top为随着进度不断接近目标进度时曲线的实时波峰值 ,其中mTargetProgress为目标progress,因为有一个目标进度才能实现当前进度不断接近目标进度的过程中,水面渐趋于平面的效果:

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);    
}

如此就能真正实现水面上涨的进度条了。

但如何实现图中双击时水面从0%上涨到目标进度,单击时水面在目标进度不断涌动的效果呢?
先说说双击效果的实现:这个简单,定义一个Handler,,当双击时,handler.postDelayed(runnable,time),每隔一段时间progress+1,在runnable中invalidate()不断更新进度,直到当前progress到达mTargetProgress。代码如下

/**
 * 实现双击动画
 */
private void startDoubleTapAnimation() {    
  setProgress(0);    
  doubleTapHandler.postDelayed(doubleTapRunnable,60);
}

private Handler doubleTapHandler = new Handler(){    
  @Override    
  public void handleMessage(Message msg) {        
    super.handleMessage(msg);   
  }
};

//双击处理线程,隔60ms发送一次数据
private Runnable doubleTapRunnable = new Runnable() {    
  @Override    
  public 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(){    
  @Override    
  public void handleMessage(Message msg) {        
    super.handleMessage(msg);   
  }
};

//单击处理线程,隔200ms发送一次数据
private Runnable singleTapRunnable = new Runnable() {    
  @Override    
  public 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 path
int 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);

基本上重要的代码与核心逻辑与代码就在上面了。
注意点:
1、当drawCircle时要考虑到padding,则circle的宽和高为getWidth与getHeight减去padding值,代码如下:

//自定义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.png

那如何获取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上。到这儿自定义水面上涨效果的进度条于写完了。
完整代码下载

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容