高仿摩拜解锁单车的加载控件

一、写在前面

最近在下班骑车回家的过程中,发现摩拜单车的解锁进度条还是挺有意思的,它是一直转,根据进度增大圆弧角度,最后有一个打钩的动画,好了,话不多说,马上就来实现一下。上面图的效果不是很好,真实效果各位自行在工程里看。(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()
    }


}
发布了14 篇原创文章 · 获赞 8 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qwe749082787/article/details/97126162
今日推荐