一步步实现自定义View之圆形进度条

目前基于Kotlin做了三种圆形进度条,首先看一下这三种进度条的效果图吧(项目地址戳这里):
这里写图片描述
第一个自定义View,我把它取名为CircleLevelView,效果看着是一段圆弧的动画显示进度。第二个取名为CircleVerticalView,利用二阶贝塞尔曲线实现波浪动画,并在竖直方向上有上升动画。第三个是CirclePointView,
在一个圆的边上画出一百个小圆,对应进度显示不同的小圆颜色。
下面分别来看一下这三种自定义View的实现吧
1.CircleLevelView
1.1实现原理
这里写图片描述
由上图所示,CircleLevelView主要由三部分组成,大小不一的两个圆,和红线包裹圆弧形成的扇形区域。它们的层级关系是小圆在最上层,扇形在中间,大圆在底部。对扇形区域加以动画,就形成了gif图中第一个的效果。

1.2具体实现
关于自定义属性的引入、view的测量等步骤这里不再多做叙述,只讲一下onDraw方法和动画部分的操作。

//先画大圆
canvas!!.drawCircle(centerX,centerY,mExternalRadius,mExternalPaint)
//再画扇形部分
canvas!!.drawArc(rectF,mStartAngle,mCurrentAngle,true,mPercentPaint)
//最后画小圆
canvas!!.drawCircle(centerX,centerY,mInnerRadius,mInnerPaint)

重点在扇形部分,mStartAngle是开始的角度,mCurrentAngle是当前的角度。mStartAngle是一个定值。我这里对外提供了一个方法,用于在java代码中设置开始和结束的角度。mCurrentAngle是基于mStartAngle和mEndAngle变化的。

/*
* 设置初始的两个角度
* */
fun setAngle(angleStart: Float,angleEnd: Float) {
    mStartAngle = angleStart
    mEndAngle = angleEnd
    setPercentAnimation()
}

下面看一下mCurrentAngle是怎么变化的

/*
* 设置动画
* */
private fun setPercentAnimation() {
    val offSet = mEndAngle - mStartAngle
    val valueAnimator = ValueAnimator.ofFloat(0f,offSet)
    valueAnimator.addUpdateListener {
        mCurrentAngle = valueAnimator.getAnimatedValue() as Float
        invalidate()
    }
    valueAnimator.duration = mAnimationDuration
    valueAnimator.start()

}

由结束的角度和初始的角度我们可以知道角度的变化量offSet ,我们让valueAnimator 的值从0到offSet 变化,将这个不断变化的值赋值给mCurrentAngle ,再刷新View。
在onDraw方法中的

canvas!!.drawArc(rectF,mStartAngle,mCurrentAngle,true,mPercentPaint)

drawArc方法的第二个参数是开始的角度,第三个参数是sweepAngle,扇形从开始角度旋转的角度。我们可以看出这个旋转的角度是不断变化的,这就是gif中动画效果的由来。

至此,这个CircleLevelView大体上就完成了,后面再在中间加上文字,并完善一下细节(提供各种自定义属性,对外方法)就好了。

2.CircleVerticalView

2.1实现原理
(原理图是别人的)
这里写图片描述

首先我们需要利用Path的画二阶贝塞尔方法画出sin函数的一半,然后利用for循环画出n个sin函数的一半,再讲view的左下和右下这两个点与之前画出的曲线形成闭环。
至于要画多少个sin函数的一半,我是这么做的,例如sin函数的一半的宽度设置为itemWidth,首先view中显示的个数m = view的 width / itemWidth,再往左侧加三个,总个数为m + 3。
为了形成波浪在水平线上移动的效果,我们需要给它一个从0到offset的动态偏移量,这个偏移量体现在点的x坐标上。
sin函数的波峰和波谷则通过循环的奇偶数来确定高度。至于垂直方向上的高度上升(形成水位上升的效果),则可以在
点的y坐标上叫一个垂直的动态偏移量即可。

2.2具体实现
核心代码:在onDraw方法中

//将画布裁剪成圆形
mCirclePath!!.addCircle(width.div(2).toFloat(),height.div(2).toFloat(),mViewRadius,Path.Direction.CW)
canvas!!.clipPath(mCirclePath)
canvas.drawPaint(mCirclePaint)
canvas.drawCircle(width.div(2).toFloat(),height.div(2).toFloat(),mViewRadius,mCirclePaint)
//核心代码
canvas.drawPath(drawWave(),mWavePaint)

我们需要首先将画布裁剪成圆形,接着就是绘制这个动态的二阶贝塞尔曲线了。

我们看一下drawWave()方法

/*
* 画波浪
* */
private fun drawWave(): Path {
    //半个波长,也就是sin的二分之一
    val itemWidth = mWaveWidth.div(2)
    //屏幕中需要出现的半波长个数
    val itemNumber = (mWidth.div(mWaveWidth) * 2).toInt() + 1
    val path = Path()
    //起始坐标
    path.moveTo(-itemWidth * 3,mHeight)

    for (i in -3 until itemNumber){
        val startX = itemWidth * i
        path.quadTo(startX+itemWidth.div(2)+mOffset,
                getWaveHeight(i),
                startX + itemWidth + mOffset,
                mHeight - mProgressHeight)
    }

    path.lineTo(mWidth,mHeight)
    path.lineTo(0f,mHeight)
    path.close()

    return path
}
/*
* 得到波浪在Y轴上的高度(最高点和最低点)
* */
private fun getWaveHeight(i: Int): Float{
    if(i % 2 == 0){
        return mHeight - mWaveHeight - mProgressHeight
    }else{
        return mHeight + mWaveHeight - mProgressHeight
    }
}

至于这两个动态偏移量(水平和垂直),我们当然是通过动画来获取了:

/*
* 开启动画
* */
private fun startVerticalAnimation() {
    val verticalAnimator = ObjectAnimator.ofFloat(0f,mVerticalPercent)
    val horizontalAnimator = ObjectAnimator.ofFloat(0f,mWaveWidth)
    horizontalAnimator.repeatCount = ObjectAnimator.INFINITE
    horizontalAnimator.interpolator = LinearInterpolator()

    verticalAnimator.addUpdateListener {
        mProgressHeight = verticalAnimator.getAnimatedValue() as Float
        invalidate()
    }

    horizontalAnimator.addUpdateListener {
        mOffset = horizontalAnimator.getAnimatedValue() as Float
        invalidate()
    }

    horizontalAnimator.duration = mHorizontalDuration.toLong()
    verticalAnimator.duration = mVerticalDuration.toLong()
    horizontalAnimator.start()
    verticalAnimator.start()

}

其他细节在这里就不再叙述了,有兴趣的朋友可以看文章开始的链接哟

3.CirclePointView

3.1实现原理
(别人的图)
这里写图片描述

我们需要这样画100个上图中的蓝色圆,同时每个圆之间需要有间隔,实际上整个大圆一圈相当于分布着200个圆。

(别人的图)
这里写图片描述
我们重点看一下单个圆怎么画吧。上面提到过200个圆,每个圆占到的度数就是360 / 200 = 1.8度。
通过两个等式(结合上面两个图看):
1. R0 = R1 + r
(R0是大圆半径,以view中心为圆心,R1是小圆圆心距离view中心的距离)
2.sin(0.9) = r / R1

通过这两个等式,我们可以得出:
r = sin(0.9) * R0 / ( 1 + sin(0.9))

在这里,我将view的宽高设置为一样,R0为它们的一半。
每画一个圆后,就将画布旋转360 / 100 = 3.6 度,就可以继续画下一个圆了。
下面看一下具体实现吧。

3.2具体实现

在onDraw方法中:

mOuterCircleRadius = mWidth.div(2)
//按照公式应该是0.9,360/200 = 1.8 每个圆占1.8度,1.8/2 = 0.9画出来圆点间隔有点大,就用了1.0
//Math.sin的参数需要度数所代表的弧度
val sin1 = Math.sin(Math.toRadians(1.0))
//根据上面求出的公式得到小圆的半径
mPointRadius = (sin1 * mOuterCircleRadius / (1 + sin1)).toFloat()

mPointPaint!!.color = mSelectedColor

for (index in 0 until mPercent){
    canvas!!.drawCircle(mOuterCircleRadius,mPointRadius,mPointRadius,mPointPaint)
    canvas.rotate(3.6f,mOuterCircleRadius,mOuterCircleRadius)
}

mPointPaint!!.color = mDefaultColor
for (index in mPercent until 100){
    canvas!!.drawCircle(mOuterCircleRadius,mPointRadius,mPointRadius,mPointPaint)
    canvas.rotate(3.6f,mOuterCircleRadius,mOuterCircleRadius)
}

画圆分为两个部分,首先画相应进度所代表的圆,再画接下来默认的圆。

如果还想加上点动画效果,那也是很简单的,用之前两个自定义view使用过的类似方法就好。

最后按照惯例,举手之劳点个star吧(猛戳这里

猜你喜欢

转载自blog.csdn.net/ckwccc/article/details/80774948