Android自定义View之圆形进度条总结

最近撸了一个圆形进度条的开源项目,算是第一次完完整整的使用自定义 View 。在此对项目开发思路做个小结,欢迎大家 Star 和 Fork。

该项目总共实现了三种圆形进度条效果

  1. CircleProgress:圆形进度条,可以实现仿 QQ 健康计步器的效果,支持配置进度条背景色、宽度、起始角度,支持进度条渐变
  2. DialProgress:类似 CircleProgress,但是支持刻度
  3. WaveProgress:实现了水波纹效果的圆形进度条,不支持渐变和起始角度配置,如需此功能可参考 CircleProgress 自行实现。

先上效果图,有图才好说。

CircleProgress 效果图  

Android自定义View之圆形进度条总结

DialProgress 和 WaveProgress 效果图 

Android自定义View之圆形进度条总结

恩,那么接下来,就来讲讲怎么实现以上自定义进度条的效果。

圆形进度条

圆形进度条是第一个实现的进度条效果,用了我大半天的时间,实现起来并不复杂。

其思路主要可以分为以下几步:

  1. View 的测量
  2. 计算绘制 View 所需参数
  3. 圆弧的绘制及渐变的实现
  4. 文字的绘制
  5. 动画效果的实现

首先,我们要测量出所绘制 View 的大小,即重写 onMeasure() 方法,代码如下:


  1. @Override 
  2.  
  3. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 
  4.  
  5.    super.onMeasure(widthMeasureSpec, heightMeasureSpec); 
  6.  
  7.    setMeasuredDimension(MiscUtil.measure(widthMeasureSpec, mDefaultSize), 
  8.  
  9.            MiscUtil.measure(heightMeasureSpec, mDefaultSize)); 
  10.  
  11.  

由于其他两个进度条类都需要实现 View 的测量,这里对代码进行了封装:


  1. /** 
  2.  
  3. * 测量 View 
  4.  
  5.  
  6. * @param measureSpec 
  7.  
  8. * @param defaultSize View 的默认大小 
  9.  
  10. * @return 测量出来的 View 大小 
  11.  
  12. */ 
  13.  
  14. public static int measure(int measureSpec, int defaultSize) { 
  15.  
  16.    int result = defaultSize; 
  17.  
  18.    int specMode = View.MeasureSpec.getMode(measureSpec); 
  19.  
  20.    int specSize = View.MeasureSpec.getSize(measureSpec); 
  21.  
  22.   
  23.  
  24.    if (specMode == View.MeasureSpec.EXACTLY) { 
  25.  
  26.        result = specSize; 
  27.  
  28.    } else if (specMode == View.MeasureSpec.AT_MOST) { 
  29.  
  30.        result = Math.min(result, specSize); 
  31.  
  32.    } 
  33.  
  34.    return result; 
  35.  
  36.  

关于 View 测量可以看下这篇博客 Android 自定义View 中的onMeasure的用法

接下来,在 onSizeChanged() 中计算绘制圆及文字所需的参数,考虑到屏幕旋转的情况,故未直接在 onMeasure() 方法中直接计算。这里以下面草图来讲解绘制计算过程中的注意事项,图丑,勿怪~  

Android自定义View之圆形进度条总结

图中,外面蓝色矩形为 View,里面黑色矩形为圆的外接矩形,蓝色矩形和黑色矩形中间空白的地方为 View 的内边距(padding)。两个蓝色的圆其实是一个圆,代表圆的粗细,这是因为 Android 在绘制圆或者圆弧的时候是圆的边宽的中心与外接矩形相交,所以在计算的时候要考虑到内边距(padding) 和 圆与外接矩形的相交。

默认不考虑圆弧的宽度,绘制出来的效果如下:  

Android自定义View之圆形进度条总结


  1. @Override 
  2.  
  3. protected void onSizeChanged(int w, int h, int oldw, int oldh) { 
  4.  
  5.    super.onSizeChanged(w, h, oldw, oldh); 
  6.  
  7.    Log.d(TAG, "onSizeChanged: w = " + w + "; h = " + h + "; oldw = " + oldw + "; oldh = " + oldh); 
  8.  
  9.    //求圆弧和背景圆弧的最大宽度 
  10.  
  11.    float maxArcWidth = Math.max(mArcWidth, mBgArcWidth); 
  12.  
  13.    //求最小值作为实际值 
  14.  
  15.    int minSize = Math.min(w - getPaddingLeft() - getPaddingRight() - 2 * (int) maxArcWidth, 
  16.  
  17.            h - getPaddingTop() - getPaddingBottom() - 2 * (int) maxArcWidth); 
  18.  
  19.    //减去圆弧的宽度,否则会造成部分圆弧绘制在外围 
  20.  
  21.    mRadius = minSize / 2; 
  22.  
  23.    //获取圆的相关参数 
  24.  
  25.    mCenterPoint.x = w / 2; 
  26.  
  27.    mCenterPoint.y = h / 2; 
  28.  
  29.    //绘制圆弧的边界 
  30.  
  31.    mRectF.left = mCenterPoint.x - mRadius - maxArcWidth / 2; 
  32.  
  33.    mRectF.top = mCenterPoint.y - mRadius - maxArcWidth / 2; 
  34.  
  35.    mRectF.right = mCenterPoint.x + mRadius + maxArcWidth / 2; 
  36.  
  37.    mRectF.bottom = mCenterPoint.y + mRadius + maxArcWidth / 2; 
  38.  
  39.    //计算文字绘制时的 baseline 
  40.  
  41.    //由于文字的baseline、descent、ascent等属性只与textSize和typeface有关,所以此时可以直接计算 
  42.  
  43.    //若value、hint、unit由同一个画笔绘制或者需要动态设置文字的大小,则需要在每次更新后再次计算 
  44.  
  45.    mValueOffset = mCenterPoint.y - (mValuePaint.descent() + mValuePaint.ascent()) / 2; 
  46.  
  47.    mHintOffset = mCenterPoint.y * 2 / 3 - (mHintPaint.descent() + mHintPaint.ascent()) / 2; 
  48.  
  49.    mUnitOffset = mCenterPoint.y * 4 / 3 - (mUnitPaint.descent() + mUnitPaint.ascent()) / 2; 
  50.  
  51.    updateArcPaint(); 
  52.  
  53.    Log.d(TAG, "onSizeChanged: 控件大小 = " + "(" + w + ", " + h + ")" 
  54.  
  55.            + "圆心坐标 = " + mCenterPoint.toString() 
  56.  
  57.            + ";圆半径 = " + mRadius 
  58.  
  59.            + ";圆的外接矩形 = " + mRectF.toString());  

关于 Android 中文字绘制可以参考以下两篇文章:

1. Android 自定义View学习(三)——Paint 绘制文字属性

2. measureText() vs .getTextBounds()

以上,已经基本完成了 View 绘制所需全部参数的计算。接下来就是绘制圆弧及文字了。

绘制圆弧需要用到 Canvas 的


  1. // oval 为 RectF 类型,即圆弧显示区域 
  2.  
  3. // startAngle 和 sweepAngle  均为 float 类型,分别表示圆弧起始角度和圆弧度数。3点钟方向为0度,顺时针递增 
  4.  
  5. // 如果 startAngle < 0 或者 > 360,则相当于 startAngle % 360 
  6.  
  7. // useCenter:如果为 true 时,在绘制圆弧时将圆心包括在内,通常用来绘制扇形 
  8.  
  9. // 绘制圆弧的画笔 
  10.  
  11. drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint);  

为了方便计算,绘制圆弧的时候使用了 Canvas 的 rotate() 方法,对坐标系进行了旋转


  1. private void drawArc(Canvas canvas) { 
  2.  
  3.    // 绘制背景圆弧 
  4.  
  5.    // 从进度圆弧结束的地方开始重新绘制,优化性能 
  6.  
  7.    canvas.save(); 
  8.  
  9.    float currentAngle = mSweepAngle * mPercent; 
  10.  
  11.    canvas.rotate(mStartAngle, mCenterPoint.x, mCenterPoint.y); 
  12.  
  13.    // +2 是因为绘制的时候出现了圆弧起点有尾巴的问题 
  14.  
  15.    canvas.drawArc(mRectF, currentAngle, mSweepAngle - currentAngle + 2, false, mBgArcPaint); 
  16.  
  17.    canvas.drawArc(mRectF, 2, currentAngle, false, mArcPaint); 
  18.  
  19.    canvas.restore(); 
  20.  
  21.  

恩,圆环已经绘制完成,那么接下来就是实现圆环的渐变,这里使用 SweepGradient 类。SweepGradient 可以实现从中心放射性渐变的效果,如下图:

Android自定义View之圆形进度条总结

SweepGradient 类有两个构造方法,


  1. /** 
  2.  
  3. * @param cx 渲染中心点x坐标 
  4.  
  5. * @param cy 渲染中心点y坐标 
  6.  
  7. * @param colors 围绕中心渲染的颜色数组,至少要有两种颜色值 
  8.  
  9. * @param positions 相对位置的颜色数组,可为null,  若为null,可为null,颜色沿渐变线均匀分布。一般不需要设置该参数 
  10.  
  11.  
  12. public SweepGradient(float cx, float cy, int[] colors, float[] positions) 
  13.  
  14.   
  15.  
  16. /** 
  17.  
  18. * @param cx 渲染中心点x坐标 
  19.  
  20. * @param cy 渲染中心点y坐标 
  21.  
  22. * @param color0 起始渲染颜色 
  23.  
  24. * @param color1 结束渲染颜色 
  25.  
  26.  
  27. public SweepGradient(float cx, float cy, int color0, int color1)  

这里我们选择第一个构造方法。由于设置渐变需要每次都创建一个新的 SweepGradient 对象,所以最好不要放到 onDraw 方法中去更新,最好在初始化的时候就设置好,避免频繁创建导致内存抖动。


  1. private void updateArcPaint() { 
  2.  
  3.    // 设置渐变 
  4.  
  5.    int[] mGradientColors = {Color.GREEN, Color.YELLOW, Color.RED}; 
  6.  
  7.    mSweepGradient = new SweepGradient(mCenterPoint.x, mCenterPoint.y, mGradientColors, null); 
  8.  
  9.    mArcPaint.setShader(mSweepGradient); 
  10.  
  11.  

这里还有一个值得注意的地方,草图如下

Android自定义View之圆形进度条总结

假设,渐变颜色如下:


  1. int[] mGradientColors = {Color.GREEN, Color.YELLOW, Color.RED, Color.BLUE}; 

因为 SweepGradient 渐变是 360 度的,所以如果你绘制的圆弧只有 270度,则蓝色部分(图中黑色阴影部分)的渐变就会不可见。

接下来,就是文字的绘制了。文字绘制在上述提到的文章中已经进行了详细的讲解,这里就不再赘述。代码如下:


  1. private void drawText(Canvas canvas) { 
  2.  
  3.    canvas.drawText(String.format(mPrecisionFormat, mValue), mCenterPoint.x, mValueOffset, mValuePaint); 
  4.  
  5.   
  6.  
  7.    if (mHint != null) { 
  8.  
  9.        canvas.drawText(mHint.toString(), mCenterPoint.x, mHintOffset, mHintPaint); 
  10.  
  11.    } 
  12.  
  13.   
  14.  
  15.    if (mUnit != null) { 
  16.  
  17.        canvas.drawText(mUnit.toString(), mCenterPoint.x, mUnitOffset, mUnitPaint); 
  18.  
  19.    } 
  20.  
  21.  

最后,我们来实现进度条的动画效果。这里我们使用 Android 的属性动画来实现进度更新。


  1. private void startAnimator(float start, float end, long animTime) { 
  2.  
  3.    mAnimator = ValueAnimator.ofFloat(start, end); 
  4.  
  5.    mAnimator.setDuration(animTime); 
  6.  
  7.    mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 
  8.  
  9.        @Override 
  10.  
  11.        public void onAnimationUpdate(ValueAnimator animation) { 
  12.  
  13.            mPercent = (float) animation.getAnimatedValue(); 
  14.  
  15.            mValue = mPercent * mMaxValue; 
  16.  
  17.            if (BuildConfig.DEBUG) { 
  18.  
  19.                Log.d(TAG, "onAnimationUpdate: percent = " + mPercent 
  20.  
  21.                        + ";currentAngle = " + (mSweepAngle * mPercent) 
  22.  
  23.                        + ";value = " + mValue); 
  24.  
  25.            } 
  26.  
  27.            invalidate(); 
  28.  
  29.        } 
  30.  
  31.    }); 
  32.  
  33.    mAnimator.start(); 
  34.  
  35.  

这里有两个注意点:

1. 不要在 ValueAnimator.AnimatorUpdateListener 中输出 Log,特别是动画调用频繁的情况下,因为输出 Log 频繁会生成大量 String 对象造成内存抖动,当然也可以使用 StringBuilder 来优化。

2. 关于 invalidate() 和 postInvalidate() 两者最本质的前者只能在 UI 线程中使用,而后者可以在非 UI 线程中使用,其实 postInvalidate() 内部也是使用 Handler 实现的。

关于 Android 属性动画可以参考:

1. Android 属性动画(Property Animation) 完全解析 (上)

2. Android 属性动画(Property Animation) 完全解析 (下)

补充:同一个属性如何支持颜色和颜色数组

考虑到圆弧设置单色和渐变的区别,即单色只需要提供一种色值,而渐变至少需要提供两种色值。可以有以下几种解决方案:

  1. 定义两个属性,渐变的优先级高于单色的。
  2. 定义一个 format 为 string 属性,以 #FFFFFF|#000000 形式提供色值
  3. 定义一个 format 为 color|reference 的属性,其中 reference 属性指代渐变色的数组。

这里选用第三种方案,实现如下:


  1. &lt;!-- 圆形进度条 --&gt; 
  2.  
  3. &lt;declare-styleable name="CircleProgressBar"&gt; 
  4.  
  5.     &lt;!-- 圆弧颜色, --&gt; 
  6.  
  7.     &lt;attr name="arcColors" format="color|reference" /&gt; 
  8.  
  9. &lt;/declare-styleable&gt; 
  10.  
  11.   
  12.  
  13. &lt;!-- colors.xml --&gt; 
  14.  
  15. &lt;color name="green"&gt;#00FF00&lt;/color&gt; 
  16.  
  17. &lt;color name="blue"&gt;#EE9A00&lt;/color&gt; 
  18.  
  19. &lt;color name="red"&gt;#EE0000&lt;/color&gt; 
  20.  
  21. &lt;!-- 渐变颜色数组 --&gt; 
  22.  
  23. &lt;integer-array name="gradient_arc_color"&gt; 
  24.  
  25.    &lt;item&gt;@color/green&lt;/item&gt; 
  26.  
  27.    &lt;item&gt;@color/blue&lt;/item&gt; 
  28.  
  29.    &lt;item&gt;@color/red&lt;/item&gt; 
  30.  
  31. &lt;/integer-array&gt; 
  32.  
  33.   
  34.  
  35. &lt;!-- 布局文件中使用 --&gt; 
  36.  
  37. &lt;!-- 使用渐变 --&gt; 
  38.  
  39. &lt;com.littlejie.circleprogress.DialProgress 
  40.  
  41.     android:id="@+id/dial_progress_bar" 
  42.  
  43.     android:layout_width="300dp" 
  44.  
  45.     android:layout_height="300dp" 
  46.  
  47.     app:arcColors="@array/gradient_arc_color" /&gt; 
  48.  
  49. &lt;!-- 使用单色 --&gt;     
  50.  
  51. &lt;com.littlejie.circleprogress.DialProgress 
  52.  
  53.     android:id="@+id/dial_progress_bar" 
  54.  
  55.     android:layout_width="300dp" 
  56.  
  57.     android:layout_height="300dp" 
  58.  
  59.     app:arcColors="@color/green" /&gt;  

代码中读取 xml 中配置:


  1. int gradientArcColors = typedArray.getResourceId(R.styleable.CircleProgressBar_arcColors, 0); 
  2.  
  3.    if (gradientArcColors != 0) { 
  4.  
  5.        try { 
  6.  
  7.            int[] gradientColors = getResources().getIntArray(gradientArcColors); 
  8.  
  9.            if (gradientColors.length == 0) {//如果渐变色为数组为0,则尝试以单色读取色值 
  10.  
  11.                int color = getResources().getColor(gradientArcColors); 
  12.  
  13.                mGradientColors = new int[2]; 
  14.  
  15.                mGradientColors[0] = color; 
  16.  
  17.                mGradientColors[1] = color; 
  18.  
  19.            } else if (gradientColors.length == 1) {//如果渐变数组只有一种颜色,默认设为两种相同颜色 
  20.  
  21.                mGradientColors = new int[2]; 
  22.  
  23.                mGradientColors[0] = gradientColors[0]; 
  24.  
  25.                mGradientColors[1] = gradientColors[0]; 
  26.  
  27.            } else { 
  28.  
  29.                mGradientColors = gradientColors; 
  30.  
  31.            } 
  32.  
  33.        } catch (Resources.NotFoundException e) { 
  34.  
  35.            throw new Resources.NotFoundException("the give resource not found."); 
  36.  
  37.        } 
  38.  
  39.    }  

带刻度进度条

前面,详细讲了 CircleProgress 的绘制思路,接下来讲 DialProgress。

实话说,DialProgress 与 CircleProgress 的实现极其相似,因为两者之间其实就差了一个刻度,但考虑到扩展以及类职责的单一,所以将两者分开。

这里主要讲一下刻度的绘制。刻度绘制主要使用 Canvas 类的 save()、rotate()和restore() 方法,当然你也可以使用 translate() 方法对坐标系进行平移,方便计算。


  1. /** 
  2.  
  3. * 用来保存Canvas的状态。save之后,可以调用Canvas的平移、放缩、旋转、错切、裁剪等操作。 
  4.  
  5. */ 
  6.  
  7. public void save() 
  8.  
  9.   
  10.  
  11. /** 
  12.  
  13. * 旋转一定的角度绘制图像 
  14.  
  15. * @param degrees 旋转角度 
  16.  
  17. * @param x 旋转中心点x轴坐标 
  18.  
  19. * @param y 旋转中心点y轴坐标 
  20.  
  21. */ 
  22.  
  23. public void rotate(float degrees, float x, float y) 
  24.  
  25.   
  26.  
  27. /** 
  28.  
  29. * 在当前的坐标上平移(x,y)个像素单位 
  30.  
  31. * 若dx <0 ,沿x轴向上平移; dx >0  沿x轴向下平移 
  32.  
  33. * 若dy <0 ,沿y轴向上平移; dy >0  沿y轴向下平移 
  34.  
  35. */ 
  36.  
  37. public void translate(float dx, float dy) 
  38.  
  39.   
  40.  
  41. /** 
  42.  
  43. * 用来恢复Canvas之前保存的状态。防止save后对Canvas执行的操作对后续的绘制有影响。 
  44.  
  45. */ 
  46.  
  47. public void restore() 
  48.  
  49.  
  50. private void drawDial(Canvas canvas) { 
  51.  
  52.    int total = (int) (mSweepAngle / mDialIntervalDegree); 
  53.  
  54.    canvas.save(); 
  55.  
  56.    canvas.rotate(mStartAngle, mCenterPoint.x, mCenterPoint.y); 
  57.  
  58.    for (int i = 0; i <= total; i++) { 
  59.  
  60.        canvas.drawLine(mCenterPoint.x + mRadius, mCenterPoint.y, mCenterPoint.x + mRadius + mArcWidth, mCenterPoint.y, mDialPaint); 
  61.  
  62.        canvas.rotate(mDialIntervalDegree, mCenterPoint.x, mCenterPoint.y); 
  63.  
  64.    } 
  65.  
  66.    canvas.restore(); 
  67.  
  68.  

关于 Canvas 的画布操作可以参考这篇文章:安卓自定义View进阶-Canvas之画布操作

水波纹效果的进度条

水波纹效果的进度条实现需要用到贝塞尔曲线,主要难点在于 绘制区域的计算 和 波浪效果 的实现,其余的逻辑跟上述两种进度条相似。

这里使用了 Path 类,该类在 Android 2D 绘图中是非常重要的,Path 不仅能够绘制简单图形,也可以绘制这些比较复杂的图形。也可以对多个路径进行布尔操作,类似设置 Paint 的 setXfermode() ,具体使用可以参考这篇博客:安卓自定义View进阶-Path基本操作。这里就不再赘述,有机会自己也会对 Android 自定义 View 的知识进行总结,不过,感觉应该了了无期。

继续上示意图,请叫我灵魂画手~

Android自定义View之圆形进度条总结

图中黑色的圆为我们要绘制的进度条圆,黑色的曲线为初始状态的的波浪,该波浪使用贝塞尔曲线绘制,其中奇数的点为贝塞尔曲线的起始点,偶数的点为贝塞尔曲线的控制点。例如:1——>2——>3就为一条贝塞尔曲线,1 是起点,2 是控制点,3 是终点。从图中可以看到波浪在园内圆外各一个(1—>5 和 5->9),通过对波浪在 x 轴上做平移,即图中蓝色实线,来实现波浪的动态效果,所以一个波浪的完整动画效果需要有两个波浪来实现。同理,通过控制 y 轴的偏移量,即图中蓝色虚线,可以实现波浪随进度的上涨下降。

贝塞尔曲线上起始点和控制点的计算如下:


  1. /** 
  2.  
  3. * 计算贝塞尔曲线上的起始点和控制点 
  4.  
  5. * @param waveWidth 一个完整波浪的宽度 
  6.  
  7. */ 
  8.  
  9. private Point[] getPoint(float waveWidth) { 
  10.  
  11.    Point[] points = new Point[mAllPointCount]; 
  12.  
  13.    //第1个点特殊处理,即数组的中心 
  14.  
  15.    points[mHalfPointCount] = new Point((int) (mCenterPoint.x - mRadius), mCenterPoint.y); 
  16.  
  17.    //屏幕内的贝塞尔曲线点 
  18.  
  19.    for (int i = mHalfPointCount + 1; i < mAllPointCount; i += 4) { 
  20.  
  21.        float width = points[mHalfPointCount].x + waveWidth * (i / 4 - mWaveNum); 
  22.  
  23.        points[i] = new Point((int) (waveWidth / 4 + width), (int) (mCenterPoint.y - mWaveHeight)); 
  24.  
  25.        points[i + 1] = new Point((int) (waveWidth / 2 + width), mCenterPoint.y); 
  26.  
  27.        points[i + 2] = new Point((int) (waveWidth * 3 / 4 + width), (int) (mCenterPoint.y + mWaveHeight)); 
  28.  
  29.        points[i + 3] = new Point((int) (waveWidth + width), mCenterPoint.y); 
  30.  
  31.    } 
  32.  
  33.    //屏幕外的贝塞尔曲线点 
  34.  
  35.    for (int i = 0; i < mHalfPointCount; i++) { 
  36.  
  37.        int reverse = mAllPointCount - i - 1; 
  38.  
  39.        points[i] = new Point(points[mHalfPointCount].x - points[reverse].x, 
  40.  
  41.                points[mHalfPointCount].y * 2 - points[reverse].y); 
  42.  
  43.    } 
  44.  
  45.    return points; 
  46.  
  47.  

以上,我们已经获取到绘制贝塞尔曲线所需的路径点。接下来,我们就需要来计算出绘制区域,即使用 Path 类。

Android自定义View之圆形进度条总结

紫色区域为贝塞尔曲线需要绘制的整体区域。

Android自定义View之圆形进度条总结

红色区域为上图紫色区域与圆的交集,也就是波浪要显示的区域

代码如下:


  1. //该方法必须在 Android 19以上的版本才能使用(Path.op()) 
  2.  
  3. @TargetApi(Build.VERSION_CODES.KITKAT) 
  4.  
  5. private void drawWave(Canvas canvas, Paint paint, Point[] points, float waveOffset) { 
  6.  
  7.    mWaveLimitPath.reset(); 
  8.  
  9.    mWavePath.reset(); 
  10.  
  11.    //lockWave 用于判断波浪是否随进度条上涨下降 
  12.  
  13.    float height = lockWave ? 0 : mRadius - 2 * mRadius * mPercent; 
  14.  
  15.    //moveTo和lineTo绘制出水波区域矩形 
  16.  
  17.    mWavePath.moveTo(points[0].x + waveOffset, points[0].y + height); 
  18.  
  19.   
  20.  
  21.    for (int i = 1; i < mAllPointCount; i += 2) { 
  22.  
  23.        mWavePath.quadTo(points[i].x + waveOffset, points[i].y + height, 
  24.  
  25.                points[i + 1].x + waveOffset, points[i + 1].y + height); 
  26.  
  27.    } 
  28.  
  29.    mWavePath.lineTo(points[mAllPointCount - 1].x, points[mAllPointCount - 1].y + height); 
  30.  
  31.    //不管如何移动,波浪与圆路径的交集底部永远固定,否则会造成上移的时候底部为空的情况 
  32.  
  33.    mWavePath.lineTo(points[mAllPointCount - 1].x, mCenterPoint.y + mRadius); 
  34.  
  35.    mWavePath.lineTo(points[0].x, mCenterPoint.y + mRadius); 
  36.  
  37.    mWavePath.close(); 
  38.  
  39.    mWaveLimitPath.addCircle(mCenterPoint.x, mCenterPoint.y, mRadius, Path.Direction.CW); 
  40.  
  41.    //取该圆与波浪路径的交集,形成波浪在圆内的效果 
  42.  
  43.    mWaveLimitPath.op(mWavePath, Path.Op.INTERSECT); 
  44.  
  45.    canvas.drawPath(mWaveLimitPath, paint);  

以上,就实现了水波动态的效果,当然,你也可以通过配置,来设定水波是否随进度上涨下降。为了实现更好的效果,可以设置一个浅色的水波并支持设置水波的走向(R2L 和 L2R),通过设置浅色波浪和深色波浪动画的时间,从而实现长江后浪推前浪的效果,恩,效果很自然的~自己脑补从右至左波浪的实现和贝塞尔点的计算。

对获取坐标点的代码进行优化:


  1. /** 
  2.  
  3. * 从左往右或者从右往左获取贝塞尔点 
  4.  
  5.  
  6. * @return 
  7.  
  8. */ 
  9.  
  10. private Point[] getPoint(boolean isR2L, float waveWidth) { 
  11.  
  12.    Point[] points = new Point[mAllPointCount]; 
  13.  
  14.    //第1个点特殊处理,即数组的中点 
  15.  
  16.    points[mHalfPointCount] = new Point((int) (mCenterPoint.x + (isR2L ? mRadius : -mRadius)), mCenterPoint.y); 
  17.  
  18.    //屏幕内的贝塞尔曲线点 
  19.  
  20.    for (int i = mHalfPointCount + 1; i < mAllPointCount; i += 4) { 
  21.  
  22.        float width = points[mHalfPointCount].x + waveWidth * (i / 4 - mWaveNum); 
  23.  
  24.        points[i] = new Point((int) (waveWidth / 4 + width), (int) (mCenterPoint.y - mWaveHeight)); 
  25.  
  26.        points[i + 1] = new Point((int) (waveWidth / 2 + width), mCenterPoint.y); 
  27.  
  28.        points[i + 2] = new Point((int) (waveWidth * 3 / 4 + width), (int) (mCenterPoint.y + mWaveHeight)); 
  29.  
  30.        points[i + 3] = new Point((int) (waveWidth + width), mCenterPoint.y); 
  31.  
  32.    } 
  33.  
  34.    //屏幕外的贝塞尔曲线点 
  35.  
  36.    for (int i = 0; i < mHalfPointCount; i++) { 
  37.  
  38.        int reverse = mAllPointCount - i - 1; 
  39.  
  40.        points[i] = new Point((isR2L ? 2 : 1) * points[mHalfPointCount].x - points[reverse].x, 
  41.  
  42.                points[mHalfPointCount].y * 2 - points[reverse].y); 
  43.  
  44.    } 
  45.  
  46.    //对从右向左的贝塞尔点数组反序,方便后续处理 
  47.  
  48.    return isR2L ? MiscUtil.reverse(points) : points; 
  49.  
  50.  

至此,自定义圆形进度条相关的思路已全部讲述完成。代码已全部上传至 Git ,欢迎大家 Star 和 Fork,传送门:CircleProgress。




本文作者:佚名
来源:51CTO
上一篇:QTableView 添加进度条


下一篇:正则修饰符的使用 | 手把手教你入门Python之八十七