一、写在前面
最近在下班骑车回家的过程中,发现摩拜单车的解锁进度条还是挺有意思的,它是一直转,根据进度增大圆弧角度,最后有一个打钩的动画,好了,话不多说,马上就来实现一下。上面图的效果不是很好,真实效果各位自行在工程里看。(ps小白,随便做了个gif)
二、动画分析
老样子,在做什么事情之前要先分析一波,计划一波,才能成事。首先,我们观察,这个效果首先有一个圆环,这个圆环占控件的蛮大比重;然后,根据进度的不同,一部分圆弧会被填满。无论进度是多少,它都是按照一定的速度在旋转的,可以看出,旋转速度和进度进度没有关系的;最后,在进度到达100的时候,会有一个过渡动画,动画结束后显示一个勾勾,如果中间有什么错误发生,就会显示一个叉叉。(再次说明,上面的gif图真的很糙,真实效果比这个好太多,跟摩拜大佬的一模一样)
三、开始做
1、测量
因为我们的控件主体部分是一个圆,我们首先要确定它的半径,和圆心的位置,还要圆环的宽度。
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
mViewD = if (height > width) width.toFloat() else height.toFloat()
mCirclePointSize = mViewD / 2 / 3
mResultPointSize = mViewD / 2 / 5
mCircleR = mViewD / 2 - mCirclePointSize
}
mViewD为圆环的外直径,我们取长、宽中最小的。mCirclePointSize为圆环的画笔大小,mResultPointSize为最后勾勾叉叉的画笔的大小,这里我根据控件大小,做个一个相对适合的大小。
2、画圆环和进度弧
canvas?.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), mCircleR, mCirclePaint)
mSweepAngle = mProgress / 100.0f * 360
canvas?.drawArc(width / 2 - mCircleR, mCirclePointSize, width - mCirclePointSize, height - mCirclePointSize,0f, mSweepAngle, false, mProcessCirclePaint)
这里我们画了个圆,然后根据进度画上了圆弧。
3、旋转动画
mRotateAnimation = RotateAnimation(0f, 360f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f)
mRotateAnimation.let {
it.duration = 600
it.repeatCount = -1
it.interpolator = LinearInterpolator()
}
4、进度条满后的动画
这里的动画采用的是先画一个实心圆,然后画一个白色的圆不断缩小,达到平滑过渡的效果。然后勾勾采用不同的透明度不断重绘,实现渐出的效果。思路是这样,直接上代码。
canvas?.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), mViewD / 2, mFinishCirclePaint)
mCircleCounter = (mCircleCounter + mResultPointSize).toInt()
if (mCircleR > mCircleCounter) {
mFinishCirclePaint.color = mCircleColor
canvas?.drawCircle(
width / 2.toFloat(),
height / 2.toFloat(),
mCircleR - mCircleCounter,
mFinishCirclePaint
)
postInvalidate()
} else {
if (isError) {
mPath.reset()
mPath.moveTo(width / 2 - mCircleR / 2, height / 2 - mCircleR / 2)
mPath.lineTo(width / 2 + mCircleR / 2, height / 2 + mCircleR / 2)
mPath.moveTo(width / 2 + mCircleR / 2, height / 2 - mCircleR / 2)
mPath.lineTo(width / 2 - mCircleR / 2, height / 2 + mCircleR / 2)
} else {
mPath.reset()
mPath.moveTo(width / 2 - mViewD / 4, height / 2.toFloat())
mPath.lineTo(width / 2 - 5.toFloat(), height / 2 + mViewD / 4 - 10 - 2)
mPath.lineTo(width / 2 + mViewD / 4, height / 2 - mViewD / 6 - 2)
}
if (mAlphaCounter < 255) {
mFinishResultPaint.alpha = mAlphaCounter
mAlphaCounter += 100
canvas?.drawPath(mPath, mFinishResultPaint)
postInvalidate()
} else {
mFinishResultPaint.alpha = 255
canvas?.drawPath(mPath, mFinishResultPaint)
}
}
勾勾叉叉使用一个路径去画的,各位可以自己发挥,画的比我好看一点。
监听器回调设置在下面源码里,这里不详细介绍了,各位可以根据自己的业务逻辑去加。
四、源码
package com.breo.luson.breo.widget
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.os.Build
import android.util.AttributeSet
import android.view.View
import android.view.animation.Animation
import android.view.animation.LinearInterpolator
import android.view.animation.RotateAnimation
import androidx.annotation.IntRange
/**
* Created by chenqc on 2019/7/23 09:25
*/
class CircleLoadingView : View {
//背景圆环的画笔
private lateinit var mCirclePaint: Paint
//进度圆环的画笔
private lateinit var mProcessCirclePaint: Paint
//完成后背景的的画笔
private lateinit var mFinishCirclePaint: Paint
//画勾勾和叉叉的画笔
private lateinit var mFinishResultPaint: Paint
private var mCirclePointSize: Float = 30f
private var mResultPointSize: Float = 18f
private var mCircleR: Float = 0f
private var mViewD: Float = 0f
private var mCircleColor: Int = Color.parseColor("#ffffff")
private var mProgressCircleColor: Int = Color.parseColor("#ff0000")
private var mProgress: Int = 0
private var mSweepAngle: Float = 0f
private lateinit var mRotateAnimation: RotateAnimation
private lateinit var mPath: Path
private var isStarAnimation: Boolean = false
private var isError: Boolean = false
//结束时的动画计数器
private var mCircleCounter: Int = 0
//结束时的动画计数器
private var mAlphaCounter: Int = 0
private var mCircleLoadingStatusListener: OnCircleLoadingStatusListener? =null
constructor(context: Context) : super(context) {
init()
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
init()
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init()
}
private fun init() {
mCirclePaint = Paint(Paint.ANTI_ALIAS_FLAG)
mCirclePaint.let {
it.color = mCircleColor
it.style = Paint.Style.STROKE
it.strokeWidth = mCirclePointSize
}
mProcessCirclePaint = Paint(Paint.ANTI_ALIAS_FLAG)
mProcessCirclePaint.let {
it.color = mProgressCircleColor
it.style = Paint.Style.STROKE
it.strokeWidth = mCirclePointSize + 5
}
mFinishCirclePaint = Paint(Paint.ANTI_ALIAS_FLAG)
mFinishCirclePaint.let {
it.color = mProgressCircleColor
it.style = Paint.Style.FILL
}
mFinishResultPaint = Paint(Paint.ANTI_ALIAS_FLAG)
mFinishResultPaint.let {
it.color = mCircleColor
it.style = Paint.Style.STROKE
it.strokeWidth = mResultPointSize
}
mRotateAnimation = RotateAnimation(0f, 360f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f)
mRotateAnimation.let {
it.duration = 600
it.repeatCount = -1
it.interpolator = LinearInterpolator()
}
mPath = Path()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
mViewD = if (height > width) width.toFloat() else height.toFloat()
mCirclePointSize = mViewD / 2 / 3
mResultPointSize = mViewD / 2 / 5
mCircleR = mViewD / 2 - mCirclePointSize
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), mCircleR, mCirclePaint)
if (mProgress < 100 && !isError) {
mSweepAngle = mProgress / 100.0f * 360
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
canvas?.drawArc(
width / 2 - mCircleR, mCirclePointSize, width - mCirclePointSize, height - mCirclePointSize,
0f, mSweepAngle, false, mProcessCirclePaint
)
}
if (!isStarAnimation) {
startAnimation(mRotateAnimation)
isStarAnimation = true
}
} else {
if (isStarAnimation) {
clearAnimation()
}
mFinishCirclePaint.color = mProgressCircleColor
canvas?.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), mViewD / 2, mFinishCirclePaint)
mCircleCounter = (mCircleCounter + mResultPointSize).toInt()
if (mCircleR > mCircleCounter) {
mFinishCirclePaint.color = mCircleColor
canvas?.drawCircle(
width / 2.toFloat(),
height / 2.toFloat(),
mCircleR - mCircleCounter,
mFinishCirclePaint
)
postInvalidate()
} else {
if (isError) {
mPath.reset()
mPath.moveTo(width / 2 - mCircleR / 2, height / 2 - mCircleR / 2)
mPath.lineTo(width / 2 + mCircleR / 2, height / 2 + mCircleR / 2)
mPath.moveTo(width / 2 + mCircleR / 2, height / 2 - mCircleR / 2)
mPath.lineTo(width / 2 - mCircleR / 2, height / 2 + mCircleR / 2)
} else {
mPath.reset()
mPath.moveTo(width / 2 - mViewD / 4, height / 2.toFloat())
mPath.lineTo(width / 2 - 5.toFloat(), height / 2 + mViewD / 4 - 10 - 2)
mPath.lineTo(width / 2 + mViewD / 4, height / 2 - mViewD / 6 - 2)
}
if (mAlphaCounter < 255) {
mFinishResultPaint.alpha = mAlphaCounter
mAlphaCounter += 100
canvas?.drawPath(mPath, mFinishResultPaint)
postInvalidate()
} else {
mFinishResultPaint.alpha = 255
canvas?.drawPath(mPath, mFinishResultPaint)
if (isError) {
mCircleLoadingStatusListener?.onError()
} else {
mCircleLoadingStatusListener?.onFinish()
}
}
}
}
}
fun setProgress(@IntRange(from = 0, to = 100) progress: Int) {
if (progress in 0..100) {
mProgress = progress
postInvalidate()
}
}
fun setOnCircleLoadingStatusListener(listener: OnCircleLoadingStatusListener) {
mCircleLoadingStatusListener = listener
}
fun setError() {
isError = true
invalidate()
}
fun reStart(){
isStarAnimation = true
isError=false
mPath .reset()
mProgress = 0
startAnimation(mRotateAnimation)
}
interface OnCircleLoadingStatusListener {
fun onFinish()
fun onError()
}
}