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

Santo72V 8年前
   <p>实现效果如下</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/4dc60ea41df5f10b465a72aeb3f507d2.gif"></p>    <p style="text-align:center">water_progress.gif</p>    <p>实现思路:</p>    <p>1、如何实现圆中水面上涨效果:利用Paint的setXfermode属性为PorterDuff.Mode.SRC_IN画出进度所在的矩形与圆的交集实现</p>    <p>2、如何水波纹效果:利用贝塞尔曲线,动态改变波峰值,实现“随着进度的增加,水波纹逐渐变小的效果”</p>    <p>废话不多说,看代码。</p>    <p>首先是自定义属性值,有哪些可自定义属性值呢?圆的背景颜色:circle_color,进度的颜色:progress_color,</p>    <p>进度显示文字的颜色:text_color,进度文字的大小:text_size,还有最后一个:波纹最大高度:ripple_topheight</p>    <pre>  <code class="language-java"><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></code></pre>    <p>下面是自定义View:WaterProgressView的部份代码:</p>    <p>成员变量</p>    <pre>  <code class="language-java">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;  }</code></pre>    <p>获取自定义属性值:</p>    <pre>  <code class="language-java">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();    }</code></pre>    <p>定义构造函数,注意 mProgressPaint.setXfermode</p>    <pre>  <code class="language-java">//当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);    }</code></pre>    <p>onMeasure()方法代码:</p>    <pre>  <code class="language-java">@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);  }</code></pre>    <p>接下来是核心部份,onDraw中的代码。我们先将Circle,进度条,进度文字draw到自定义canvas的bitmap上,再将此bitmap draw到onDraw方法中的canvas上。drawCircle与drawText应该没什么难度,关键点就在于画进度条,怎么画呢?既然有水波纹效果,有曲线,就用drawPath了。drawPath的流程如下:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/782893032986cf064f9cc874b69efcd5.png"></p>    <p style="text-align:center">draw_path_01.png</p>    <p>其中ratio的代码如下,即ratio为当前进度占总进度的百分比</p>    <pre>  <code class="language-java">float ratio = getProgress()*1.0f/getMax();</code></pre>    <p>因为坐标是从B点向下和向右正向延伸的,则A点的坐标为(width,(1-ratio)*height),其中width为bitmap的宽,height为bitmap的高。我们先将mProgressPath.moveTo到A点,然后从A点顺时针方向确定path的各个关键点,如图,则代码如下:</p>    <pre>  <code class="language-java">int rightTop = (int) ((1-ratio)*height);  mProgressPath.moveTo(width,rightTop);  mProgressPath.lineTo(width,height);  mProgressPath.lineTo(0,height);  mProgressPath.lineTo(0,rightTop);</code></pre>    <p>如此mProgressPath已经lineTo到了C点,需要在A点与C点之间形成水波纹效果,则需要在A点与C点间画贝塞尔曲线。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/b01f79f96e632f5ba511f26e8fb9f83d.png"></p>    <p style="text-align:center">贝塞尔曲线.png</p>    <p>我们设定波峰最高点为10,则一段波长为40,需要画width*1.0f/40段这样的曲线,则画曲线的代码如下:</p>    <pre>  <code class="language-java">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);</code></pre>    <p>这样就能画出水面上涨且有波纹效果的进度条了。但我们还要实现随着水面上涨,越接近目标进度,水面波纹应该越来越小,则应该把10抽出为变量定义为mRippleTop等初始时波峰最大值,然后定义top为随着进度不断接近目标进度时曲线的实时波峰值 ,其中mTargetProgress为目标progress,因为有一个目标进度才能实现当前进度不断接近目标进度的过程中,水面渐趋于平面的效果:</p>    <pre>  <code class="language-java">float top = (mTargetProgress-getProgress())*1.0f/mTargetProgress* mRippleTop;</code></pre>    <p>所以drawPath的代码更新如下:</p>    <pre>  <code class="language-java">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);      }</code></pre>    <p>如此就能真正实现水面上涨的进度条了。</p>    <p>但如何实现图中双击时水面从0%上涨到目标进度,单击时水面在目标进度不断涌动的效果呢?</p>    <p>先说说双击效果的实现:这个简单,定义一个Handler,,当双击时,handler.postDelayed(runnable,time),每隔一段时间progress+1,在runnable中invalidate()不断更新进度,直到当前progress到达mTargetProgress。代码如下</p>    <pre>  <code class="language-java">/**   * 实现双击动画   */  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);             }        }  };</code></pre>    <p>双击效果实现了,那如何实现单击效果呢?单击时要求水面不断涌动一段时间,水面波纹逐渐变小,然后水面变平。我们可以定义一个mSingleTapAnimationCount变量为水面涌动的次数,然后像双击时的处理一样,定义一个Handler隔一段时间发送一次更新界面的message,mSingleTapAnimationCount--,然后我们交替地让初始时的波峰一次为正一次为负,则能实现水面涌动的效果。核心代码如下:</p>    <pre>  <code class="language-java">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;              }        }  };</code></pre>    <p>onDraw中的代码作相应的更改,因单击与双击时drawPath中曲线部分的绘制逻辑不一样,则我们定义一个变量isSingleTapAnimation 区别是正在进行单击动画还是在进行双击动画。更改后的代码如下:</p>    <pre>  <code class="language-java">//画进度  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);</code></pre>    <p>基本上重要的代码与核心逻辑与代码就在上面了。</p>    <p>注意点:</p>    <p>1、当drawCircle时要考虑到padding,则circle的宽和高为getWidth与getHeight减去padding值,代码如下:</p>    <pre>  <code class="language-java">//自定义bitmap的宽和高  int width = getWidth()-getPaddingLeft()-getPaddingRight();  int height = getHeight()-getPaddingTop()-getPaddingBottom();    //画圆  mPaintCanvas.drawCircle(width/2,height/2,height/2,mCirclePaint);</code></pre>    <p>2、当drawText时,不是从text的height的中间开始draw的,而是从baseline开始draw的</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/9027f6894e17a4436bdcd3151e68c4e9.png"></p>    <p style="text-align:center">baseline.png</p>    <p>那如何获取baseline的height坐标呢</p>    <pre>  <code class="language-java">Paint.FontMetrics metrics = mTextPaint.getFontMetrics();  //因为ascent在baseline之上,所以ascent为负数。descent+ascent为负数,所以是减而不是加  float baseLine = height*1.0f/2 - (metrics.descent+metrics.ascent)/2;</code></pre>    <p>drawText的全部代码如下:</p>    <pre>  <code class="language-java">//画进度文字  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);</code></pre>    <p>3、因为要顾及到padding,记得将onDraw中的canvas translate到(getPaddingLeft(),getPaddingTop())处。</p>    <pre>  <code class="language-java">canvas.translate(getPaddingLeft(),getPaddingTop());  canvas.drawBitmap(mBitmap,0,0,null);</code></pre>    <p>最后记得将自定义的bitmap draw到onDraw中的canvas上。到这儿自定义水面上涨效果的进度条于写完了。</p>    <p> </p>    <p> </p>    <p>来自:http://www.jianshu.com/p/693a3d130a85</p>    <p> </p>