android自定义view《二》锤各种各样的进度条

进度条或者说一个waiting的控件,基本上用到的地方太多了,各种花式进度条也很多,但是原理并不复杂。话不多说,开锤。

gifhome_480x854_5s.gif

View的坐标系

View的坐标系类似于android 的坐标系,是相对于父布局的相对坐标。


android自定义view《二》锤各种各样的进度条_第1张图片
image.png

我们在使用canvas画布绘制图像的时候,绘制的坐标都是使用相对坐标getX()和getY()。

开始绘制

旋转环形进度条

android自定义view《二》锤各种各样的进度条_第2张图片
image.png

从图种可以看到我们要实现一个圆形的进度条,需要定制外面大圆和内部的两个小圆,两圆颜色不同覆盖出现了一个圆环的样子。
首先我们来绘制一个由小球点绘制的的圆形:


android自定义view《二》锤各种各样的进度条_第3张图片
image.png

通过计算可以绘制出小球的位置,然后间隔一个刻度就可以得到小球圆了!

   val count = 360 / mMinAngle //获取小球的个数 mMinAngle是小球的间隔夹角
        for ( i in 1..count){
            val minX = cos((i*mMinAngle/180F)* PI) * mOutRadius  //使用PI
            val minY = sin((i*mMinAngle/180F)* PI  ) * mOutRadius 
            //开始画小球
            canvas!!.drawCircle((mWidth/2f+ minX).toFloat(), (mHeight/2f + minY).toFloat(),miniRadius,mPaint)
        }

然后我们给它整个属性动画他就可以旋转了。
圆环的绘制,我们先绘制一个扇形
绘制扇形之前我们需要确定这个扇形的位置,和开始绘制的角度。


android自定义view《二》锤各种各样的进度条_第4张图片
image.png

确定一个扇形的位置需要一个区域来确定,当这个矩形区域为正方形的时候则为圆形扇形,否则为椭圆。由此我们可以通过整个View的width 和height来确定扇形位置。绘制扇形的时候以X轴顺时针为正方形

  //绘制扇形区域 outRadius外部圆环的半径
     val outRect = RectF()
        outRect.set(mWidth/2 - mOutRadius,mHeight/2 - mOutRadius, mWidth/2 + mOutRadius, mHeight/2 + mOutRadius)
//以X轴顺时针为正,-90度为正上方。
 canvas.drawArc(outRect,-90f,mAngle,true,mPaint)
  //画一个内圆圈
 canvas.drawCircle(mWidth/2f,mHeight/2f,mInnerRadius,mCirclePain)

现在我们已经整好了了圆环了,只需要配置一个动画,让扇形的角度变化就行。

   fun updateProgress(){
       val animator  = ValueAnimator.ofInt(0,100)
        animator.addUpdateListener {
          progress  =  animator.animatedValue as Int
        //计算角度
        mAngle = progress * 3.6f
        //进行绘制
        postInvalidate()
        }
        animator.duration = 5000
        animator.repeatCount = ValueAnimator.INFINITE
        animator.start()
    }
 

波动进度的实现

在实现波浪线之前我们来了解一下贝塞尔曲线
贝塞尔曲线由三个点确定一条曲线,如图所示

android自定义view《二》锤各种各样的进度条_第5张图片
image.png

android已经帮我们整好了这个函数了我们只需要控制p0 ,p1,p2点就可以绘制出一条曲线了。
我们需要使用到path,通过它来画曲线,path简单来说就是路径工具,他可以绘制 点到点直接的路径
android提供绘制的函数有 quadTo(x1,y1,x2,y2) 和rQuadTo(dx1,dy1,dy1,dy2)。
区别在于rQuadTo使用的是增量,是相对上一个坐标的增量。如图所示:
android自定义view《二》锤各种各样的进度条_第6张图片
image.png

我们在使用quadTo的时候,需要将path移动到P0点,然后调用它,最后使用canvas.drawPath就能画出曲线了。
而rQuadTo的使用也很简单
我们已经知道 P0的坐标 ,则P1= P0 + 偏移量1 ,P2 = P0 +偏移量2。我们只需要传递偏移量就可以得到曲线。
android自定义view《二》锤各种各样的进度条_第7张图片
image.png

现在我们已经绘制了一个曲线了,但是要有波动效果就需要对曲线进行动画。
android自定义view《二》锤各种各样的进度条_第8张图片
image.png

如图所示:这就是我们设计稿的全部内容了,我们需要绘制一个波浪区域,然后与内部的圆形进行Xfmode相交,既可以得到绿色线的区域一个完整的波动圆了
至于有人会问为什么我们要使用三个波长,一个波长不就完事了吗?原因是我们要进行波动动画都是给P0和P1点增加一个增量,让它偏移一个距离如图所示,但是这个距离是多少呢?

android自定义view《二》锤各种各样的进度条_第9张图片
image.png

不可能的无限偏移下去吧,一般是偏移一个波长,然后不断的重复,偏移一个波长和刚开始显示的内容没有任何区别,动画结束之后重复就不会出现突兀感!但是为什么要三个呢 因为我们要使用两个波一个向左一个向右形成波浪来回荡漾的样子!赞。

 private fun getPoints() {
        //获取需要绘制的坐标数组
        //先初始化中间节点的坐标
        mLightPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN ) //图层相交模式
        mDarkPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP ) //图层相交模式
        lightPath.reset()
        darkPath.reset()
        //设置P0
        points[0] = Point(-2*mRadius,mHeight-mHeight * mProgress )
        points[1] = Point(4*mRadius,mHeight-mHeight * mProgress )


        lightPath.moveTo(points[1]!!.x+offsetX,points[1]!!.y)
        lightPath.lineTo(points[1]!!.x +offsetX,mHeight*1f)
        lightPath.lineTo(points[0]!!.x+offsetX,mHeight*1f)
        lightPath.lineTo(points[0]!!.x +offsetX,points[0]!!.y)
        for (i in 1..3){
            //画三个波
            lightPath.rQuadTo(100f,30f,200f,0f)
            lightPath.rQuadTo(100f,-30f,200f,0f)
        }
        lightPath.close()
        darkPath.moveTo(points[0]!!.x -offsetX,points[0]!!.y)
        darkPath.lineTo(points[0]!!.x - offsetX,mHeight*1f)
        darkPath.lineTo(points[1]!!.x -offsetX,mHeight*1f)
        darkPath.lineTo(points[1]!!.x - offsetX,points[1]!!.y)
        for (i in 1..3){
      
            darkPath.rQuadTo(-100f,30f,-200f,0f)
            darkPath.rQuadTo(-100f,-30f,-200f,0f)
        }
        darkPath.close()

    }

确定P0,P1的点,然后画出两个波浪区域,最后和圆形就行相交。

遮罩效果

由于我们要和圆形相交,实现遮罩效果。就需要使用PorterDuffXfermode,提到它我们就会看到如下的这张图


android自定义view《二》锤各种各样的进度条_第10张图片
image.png

但是真的是这样吗?PorterDuffXfermode的实现原理是两个像素点经过不同的模式,相交的颜色不一样。
图中的蓝色矩形和黄色的圆形都是两个不同的bimap,处于同一个图层出现相交出现的效果。直接在画布上绘制是不起作用的

 Paint paint = new Paint();
 canvas.drawBitmap(destinationImage, 0, 0, paint);//绘制底层的图
 PorterDuff.Mode mode = // choose a mode
 paint.setXfermode(new PorterDuffXfermode(mode));//相当于绘制遮挡层的图
 canvas.drawBitmap(sourceImage, 0, 0, paint);

谷歌的demo中绘制的顺序是这样的,如果你在图层中绘制的顺序不一样那么相交的效果也不一样。


android自定义view《二》锤各种各样的进度条_第11张图片
image.png

如图我们如果先绘制一个圆形,在绘制一个波浪区域,我们希望圆形区域显示,并且还要显示波浪区域和圆形相交的区域。我们需要使用 SRC_ATOP模式(取下层非交际部分与上层交际部分

        val bitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888)
        val bitmapCanvas = Canvas(bitmap)
        bitmapCanvas.drawCircle(mWidth/2f,mHeight/2f,mRadius,mCirclePaint) //先画圆
        val bitmap2 = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888)
        val bitmapCanvas2 = Canvas(bitmap2)
        val rectF = RectF(0f,0f,mWidth*1F,mHeight *1F)
        //一定要新建图层操作。
        @Suppress("DEPRECATION") val layer = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            canvas!!.saveLayer(rectF,null)
        } else {
            canvas!!.saveLayer(0f,0f,mWidth *1f,mHeight*1f,null,Canvas.ALL_SAVE_FLAG)
        }
        canvas.drawBitmap(bitmap,0f,0f,mXferModePaint)
        mXferModePaint.xfermode =  PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP ) //取下层非交际部分与上层交际部分
        //开始绘制波浪线
        getPoints()
        bitmapCanvas2.drawPath(lightPath,mLightPaint)
        bitmapCanvas2.drawPath(darkPath,mDarkPaint)
        canvas.drawBitmap(bitmap2,0f,0f,mXferModePaint)
        mXferModePaint.xfermode =  null
        canvas.restoreToCount(layer)

注意:我们在操作图层的时候,一定要使用saveLayer,不然操作画布的时候会直接将图形进行组合,不能实现图形之间的遮罩的效果。相交之后在使用restore方法进行组合显示到画布中。
其实还有一种方法是直接保存图层之后,开始绘制,不通过bitmap,但是不太好理解,我们先不讨论这种方式了。后面会详细出一次介绍mXferMode的。
到这一步我们已经绘制完毕。效果如下:


android自定义view《二》锤各种各样的进度条_第12张图片
image.png

只要加上一个动画,动态改变偏移量就能出现波动效果了。

刻度条

绘制刻度条和绘制小球的方法差不多,都是建立一个相对坐标使用正弦或余弦函数确定点,然后绘制。

android自定义view《二》锤各种各样的进度条_第13张图片
image.png

如图所示:我们只要从阿尔法开始,绘制一个135度到401度的一个充满小矩形的扇形区域就可以实现一个刻度view。
关键点在于P1和P2点的坐标怎么确定。
我们的矩形长度为,rctHeight 虚线圆的半径为mRadius,原点的坐标为(x,y)则可以得到
P1.(x + cosα * mRadius , y + sinα * mRadius) , P2(x + cosα * (mRadius + rctHeight) , Y + sinα * (mRadius + rctHeight)
我们可以先绘制,有颜色的刻度,在绘制没有颜色的刻度,在中心更新文字,一个刻度进度条就完事了!

         for (i in 0 until count){
            //开始绘制进度 从 135 到 405 间隔270度
            var angel = (135 + i * DISTANCE)* 3.14159f/180//转为π函数
            var startPoint = Point(width/2 + cos(angel) *mRadius,mHeight/2 + sin(angel)*mRadius)
            var endPoint   = Point(width/2+ cos(angel)*(mRadius + SCALE_WIDTH), mHeight/2+ sin(angel)*(mRadius + SCALE_WIDTH))
            canvas?.drawLine(startPoint.x,startPoint.y,endPoint.x,endPoint.y,mInnerPaint)
            //开始画线
         }
        for (i in count until ALL_COOUNT){
            //开始绘制进度 从 135 到 405 间隔270度
            var angel = (135 + i * DISTANCE)* 3.14159f/180//转为π函数
            var startPoint = Point(width/2 + cos(angel) *mRadius,mHeight/2 + sin(angel)*mRadius)
            var endPoint   = Point(width/2+ cos(angel)*(mRadius + SCALE_WIDTH), mHeight/2+ sin(angel)*(mRadius + SCALE_WIDTH))
            canvas?.drawLine(startPoint.x,startPoint.y,endPoint.x,endPoint.y,mOutPaint)
           }
          

效果图


android自定义view《二》锤各种各样的进度条_第14张图片
image.png

总结

1、确定相对的坐标系,这样绘制图像就有入手点
2、xferModer模式在画布中不起作用的,需要新建一个层,不然操作画布的时候会直接组合图形

你可能感兴趣的:(android自定义view《二》锤各种各样的进度条)