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

一、写在前面

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


}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
摩拜共享单车是一种新型的交通工具,它为人们提供了便捷的出行方式。随着摩拜共享单车的普及,对其数据进行分析也成为了一种热门的研究方向。本文将从摩拜共享单车的数据分析角度出发,介绍摩拜共享单车的数据分析方法。 一、数据采集 摩拜共享单车的数据采集主要包括两个方面,一是从单车上采集数据,二是从用户端采集数据。 从单车上采集数据主要包括以下几个方面: 1. GPS定位数据:GPS定位数据可以用来确定单车的位置和移动轨迹。 2. 用户骑行数据:用户骑行数据可以用来确定单车的使用情况,如骑行时间、路程、速度等。 3. 故障数据:故障数据可以用来确定单车的状态,如是否需要维修。 从用户端采集数据主要包括以下几个方面: 1. 注册信息:注册信息可以用来确定用户的基本信息,如年龄、性别、职业等。 2. 使用数据:使用数据可以用来确定用户的使用情况,如骑行时长、骑行距离、骑行地点等。 3. 评价数据:评价数据可以用来确定用户对单车的满意度,如评分、评论等。 二、数据存储 摩拜共享单车的数据存储主要包括两个方面,一是将采集的数据存储在服务器上,二是将数据可视化。 将采集的数据存储在服务器上可以方便数据的管理和分析。可以使用数据库来存储数据,如MySQL、MongoDB等。数据存储时需要注意数据的结构和类型,以便后续的数据分析和处理。 将数据可视化可以帮助我们更好地理解数据。可以使用数据可视化工具来实现数据可视化,如Tableau、PowerBI等。数据可视化可以帮助我们从多个角度分析数据,如数据的趋势、分布、关系等。 三、数据分析 摩拜共享单车的数据分析主要包括以下几个方面: 1. 用户行为分析:用户行为分析可以帮助我们了解用户的使用习惯和需求,以便更好地满足用户的需求。可以从用户使用时间、地点、频率等方面进行分析。 2. 单车状态分析:单车状态分析可以帮助我们了解单车的健康状况和使用情况,以便更好地维护和管理单车。可以从单车使用时间、故障率、维修情况等方面进行分析。 3. 区域分析:区域分析可以帮助我们了解单车的使用情况和需求分布,以便更好地规划单车的投放和管理。可以从单车使用量、用户分布、交通情况等方面进行分析。 四、数据应用 摩拜共享单车的数据应用主要包括以下几个方面: 1. 运营管理:运营管理可以帮助我们更好地管理和维护单车,以便更好地满足用户的需求。可以从单车投放、维修、调度等方面进行管理。 2. 营销策略:营销策略可以帮助我们更好地推广单车和吸引用户,以便更好地提升单车的使用率和收益。可以从用户需求、市场竞争等方面进行分析。 3. 交通规划:交通规划可以帮助我们更好地规划城市交通和提升出行效率,以便更好地满足人们的出行需求。可以从交通状况、用户需求等方面进行分析。 综上所述,摩拜共享单车的数据分析可以帮助我们更好地了解用户需求和单车状态,以便更好地管理和运营单车。同时,摩拜共享单车的数据分析也可以为城市交通规划和营销策略提供参考。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值