进度条或者说一个waiting的控件,基本上用到的地方太多了,各种花式进度条也很多,但是原理并不复杂。话不多说,开锤。
View的坐标系
View的坐标系类似于android 的坐标系,是相对于父布局的相对坐标。
我们在使用canvas画布绘制图像的时候,绘制的坐标都是使用相对坐标getX()和getY()。
开始绘制
旋转环形进度条
从图种可以看到我们要实现一个圆形的进度条,需要定制外面大圆和内部的两个小圆,两圆颜色不同覆盖出现了一个圆环的样子。
首先我们来绘制一个由小球点绘制的的圆形:
通过计算可以绘制出小球的位置,然后间隔一个刻度就可以得到小球圆了!
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)
}
然后我们给它整个属性动画他就可以旋转了。
圆环的绘制,我们先绘制一个扇形
绘制扇形之前我们需要确定这个扇形的位置,和开始绘制的角度。
确定一个扇形的位置需要一个区域来确定,当这个矩形区域为正方形的时候则为圆形扇形,否则为椭圆。由此我们可以通过整个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已经帮我们整好了这个函数了我们只需要控制p0 ,p1,p2点就可以绘制出一条曲线了。
我们需要使用到path,通过它来画曲线,path简单来说就是路径工具,他可以绘制 点到点直接的路径
android提供绘制的函数有 quadTo(x1,y1,x2,y2) 和rQuadTo(dx1,dy1,dy1,dy2)。
区别在于rQuadTo使用的是增量,是相对上一个坐标的增量。如图所示:
我们在使用quadTo的时候,需要将path移动到P0点,然后调用它,最后使用canvas.drawPath就能画出曲线了。
而rQuadTo的使用也很简单
我们已经知道 P0的坐标 ,则P1= P0 + 偏移量1 ,P2 = P0 +偏移量2。我们只需要传递偏移量就可以得到曲线。
现在我们已经绘制了一个曲线了,但是要有波动效果就需要对曲线进行动画。
如图所示:这就是我们设计稿的全部内容了,我们需要绘制一个波浪区域,然后与内部的圆形进行Xfmode相交,既可以得到绿色线的区域一个完整的波动圆了!
至于有人会问为什么我们要使用三个波长,一个波长不就完事了吗?原因是我们要进行波动动画都是给P0和P1点增加一个增量,让它偏移一个距离如图所示,但是这个距离是多少呢?
不可能的无限偏移下去吧,一般是偏移一个波长,然后不断的重复,偏移一个波长和刚开始显示的内容没有任何区别,动画结束之后重复就不会出现突兀感!但是为什么要三个呢 因为我们要使用两个波一个向左一个向右形成波浪来回荡漾的样子!赞。
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,提到它我们就会看到如下的这张图
但是真的是这样吗?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中绘制的顺序是这样的,如果你在图层中绘制的顺序不一样那么相交的效果也不一样。
如图我们如果先绘制一个圆形,在绘制一个波浪区域,我们希望圆形区域显示,并且还要显示波浪区域和圆形相交的区域。我们需要使用 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的。
到这一步我们已经绘制完毕。效果如下:
只要加上一个动画,动态改变偏移量就能出现波动效果了。
刻度条
绘制刻度条和绘制小球的方法差不多,都是建立一个相对坐标使用正弦或余弦函数确定点,然后绘制。
如图所示:我们只要从阿尔法开始,绘制一个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)
}
效果图
总结
1、确定相对的坐标系,这样绘制图像就有入手点
2、xferModer模式在画布中不起作用的,需要新建一个层,不然操作画布的时候会直接组合图形