模仿途虎的登录进度条——带节点进度条

去年写的,一直忘了发,这几天发一下。

前段时间,项目中使用了阿里的号码认证服务(一键登录),登录样式模仿了途虎养车app的登录样式,于是照猫画虎写了个带节点的进度条。

途虎登录进度条
模仿效果

使用

 <android.support.constraint.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <com.fadai.nodeprogress.NodeProgressBar
                android:id="@+id/npb"
                android:layout_width="match_parent"
                android:layout_height="148dp"
                app:np_backgroundBarColor="#FFCCCCCC"
                app:np_circleWidth="20dp"
                app:np_progressColor="#FFFF0000"
                app:np_circleAnimDuration="600"
                app:np_lineAnimDuration="200"
                app:np_circleContentAnimDuration="400"
                app:np_progressHeight="2dp"/>
        </android.support.constraint.ConstraintLayout>
// 设置节点数
 npb.setCount(3)
 // 回调事件
 npb.progressListener = object : NodeProgressBar.OnProgressListener {
            override fun onRequestScuccess(index: Int) {
                showToast("请求成功 $index")
            }

            override fun onRequestFailure(index: Int) {
                showToast("请求失败 $index")
            }

            override fun onComplete() {
                showToast("完成")
            }
        }
// 开始动画
 npb.start()
 // 第一个耗时请求成功
 npb.setRequestStatus(true, 0)
 // 第二个耗时请求成功
 npb.setRequestStatus(true, 1)
 // 第三个耗时请求失败
 npb.setRequestStatus(true, 3)

自定义属性:

        <!--圆圈宽度-->
        <attr name="np_circleWidth" format="dimension" />
        <!--背景条颜色-->
        <attr name="np_backgroundBarColor" format="color" />
        <!--进度条高度-->
        <attr name="np_progressHeight" format="dimension" />
        <!--进度条颜色-->
        <attr name="np_progressColor" format="color" />
        <!--每条横线的动画时间-->
        <attr name="np_lineAnimDuration" format="integer" />
        <!--每个圆圈的动画时间-->
        <attr name="np_circleAnimDuration" format="integer" />
        <!--圆圈内容的动画时间-->
        <attr name="np_circleContentAnimDuration" format="integer" />

开发前

途虎登录进度条

第一眼看到途虎的这个效果图,想法就是两个节点代表两个耗时请求:请求A,请求B;

  1. 请求A开始执行,同时动画开始执行;
  2. 动画一直走,直到走到第一个圆圈;
  3. 当第一个圆圈走完一圈之后,判断请求A是否仍是请求中,如果是,继续转圈;
  4. 如果不是,判断时请求成功的话,动画绘制第一个圆圈内的对号,然后开始执行请求B,同时动画继续走后面的流程;
  5. 如果判断请求A失败的话,则动画绘制第一个圆圈内的叉号,叉号绘制完毕后,回调请求失败事件,结束。
  6. 以此类推,请求B也是一样,直到动画走完所有流程,执行完成事件的回调。结束。

emmm,整个流程并不麻烦,这个主要是动画的绘制,我这里把动画两个节点+最后一条线。

两个节点+最后一段线

红色代表第一节点,紫色代表第二节点,绿色是所有请求成功后走的最后一段线。

而每一个节点可以分为横线阶段、圆圈阶段、圈内内容阶段(对号或者叉号)。

以此类推,还可以有第三节点、第四节点...

开发

初始化Path

我们可以将所有节点的横线、圆圈、对号、叉号存进list中,然后绘制到哪个节点的时候list.get(index)取出来即可

var startY = height / 2F
        var startX = 0F
        // 每一节线的宽度=(总宽度-节点宽度*数量)/(节点数量+1)
        var progressWidth = (width - circleWidth * mCount) / (mCount + 1)

        // 移动到开始位置
        mBgPath?.moveTo(startX, startY)

        // 遍历所有节点
        for (i in 0 until mCount) {

            // 线
            var linePath = Path()
            linePath.moveTo(startX, startY)
            startX += progressWidth
            linePath?.lineTo(startX, startY)
            mBgPath?.lineTo(startX, startY)

            // 圈
            var ciclePath = Path()
            var radius = circleWidth / 2F
            var centerX1 = startX + (radius)
            var centerY1 = height / 2F
            var rectfCircle1 = RectF(startX, centerY1 - radius, startX + circleWidth, centerY1 + radius)
            ciclePath?.addArc(rectfCircle1, 180F, 359.9F)
            mBgPath?.addCircle(centerX1, centerY1, radius, Path.Direction.CW)

            // 圆圈内容 对号
            var contentTruePath = Path()
            var startContentX1 = centerX1 - circleContentWidth / 2
            var startContentY1 = centerY1 - circleContentWidth / 2
            var contentPoint11 = PointF(startContentX1, startContentY1 + circleContentWidth / 2)
            var contentPoint12 = PointF(startContentX1 + circleContentWidth / 2, startContentY1 + circleContentWidth)
            var contentPoint13 = PointF(startContentX1 + circleContentWidth, startContentY1)
            contentTruePath?.moveTo(contentPoint11.x, contentPoint11.y)
            contentTruePath?.lineTo(contentPoint12.x, contentPoint12.y)
            contentTruePath?.lineTo(contentPoint13.x, contentPoint13.y)

            // 圆圈内容 叉号
            var contentFalsePath = Path()
            var contentPoint14 = PointF(startContentX1, startContentY1)
            var contentPoint15 = PointF(startContentX1 + circleContentWidth, startContentY1 + circleContentWidth)
            var contentPoint16 = PointF(startContentX1 + circleContentWidth, startContentY1)
            var contentPoint17 = PointF(startContentX1, startContentY1 + circleContentWidth)
            contentFalsePath?.moveTo(contentPoint14.x, contentPoint14.y)
            contentFalsePath?.lineTo(contentPoint15.x, contentPoint15.y)
            contentFalsePath?.moveTo(contentPoint16.x, contentPoint16.y)
            contentFalsePath?.lineTo(contentPoint17.x, contentPoint17.y)

            mLinePathList.add(linePath)
            mCirclePathList.add(ciclePath)
            mCircleContentTruePathList.add(contentTruePath)
            mCircleContentFalsePathList.add(contentFalsePath)
            mCircleContentPathList.add(contentFalsePath)

            startX += circleWidth
        }

        // 最后一段横线
        mLineEndPath?.moveTo(startX, startY)
        mBgPath?.moveTo(startX, startY)
        startX += progressWidth
        mLineEndPath?.lineTo(startX, startY)
        mBgPath?.lineTo(startX, startY)

初始化动画

和Path储存在list中一样,每个节点的不同阶段的动画,储存在list中

  for (i in 0 until mCount) {
            // 请求状态默认为请求中
            mRequestStatusList.add(REQUEST_STATUS_REQUESTING)

            // 横线动画
            var lineAnimator = ValueAnimator.ofFloat(0F, 1F).setDuration(lineProgressTime)
            lineAnimator?.addUpdateListener {
                if (mStage == STAGE_LINE) {
                    var progress = MAX_PROGRESS * (it.getAnimatedValue() as Float)
                    mCurrentProgress = progress.toInt()
                    // 动画结束后,由横线阶段->圆圈阶段
                    if (mCurrentProgress == MAX_PROGRESS) {
                        mStage = STAGE_CIRCLE
                        onStatusChange()
                    }
                    postInvalidate()
                }
            }
            mAnimatorLineList.add(lineAnimator)

            // 圆圈动画
            var circleAnimator = ValueAnimator.ofFloat(0F, 1F).setDuration(circleTime)
            // 无限循环
            circleAnimator?.repeatCount = ValueAnimator.INFINITE
            circleAnimator?.addUpdateListener {
                if (mStage == STAGE_CIRCLE) {
                    var progress = MAX_PROGRESS * (it.getAnimatedValue() as Float)
                    mCurrentProgress = progress.toInt()

                    // 无限动画最后的进度可能不是max值
                    if (mCurrentProgress == MAX_PROGRESS || mCurrentProgress == MAX_PROGRESS - 1) {
                        // 动画一圈结束后,判断请求状态是否仍是请求中
                        if (mRequestStatusList[mNode] != REQUEST_STATUS_REQUESTING) {
                            // 不是请求中的话,则停止动画,开始圆圈内容动画
                            circleAnimator?.cancel()
                            mStage = STAGE_CIRCLE_CONTENT
                            onStatusChange()
                        }
                    }
                    postInvalidate()
                }
            }
            mAnimatorCircleList.add(circleAnimator)

            // 圆圈内容动画
            var circleContentAnimator = ValueAnimator.ofFloat(0F, 1F).setDuration(circleContentTime)
            circleContentAnimator?.addUpdateListener {
                if (mStage == STAGE_CIRCLE_CONTENT) {
                    var progress = MAX_PROGRESS * (it.getAnimatedValue() as Float)
                    mCurrentProgress = progress.toInt()
                    // 动画结束后
                    if (mCurrentProgress == MAX_PROGRESS) {
                        // 如果请求成功了,执行回调,进入下一个节点,再次进入横线阶段
                        if (mRequestStatusList[mNode] == REQUEST_STATUS_SUCCESS) {
                            progressListener?.onRequestScuccess(mNode)
                            mStage = STAGE_LINE
                            mNode++
                            onStatusChange()
                        } else {
                            // 请求失败了,状态更新为失败状态,执行回调
                            mStatus = STATUS_FAILURE
                            progressListener?.onRequestFailure(mNode)
                        }

                    }
                    postInvalidate()
                }
            }
            mAnimatorCircleContentList.add(circleContentAnimator)
        }

        // 最后一段横线动画,单独处理 
        mAnimatorEnd = ValueAnimator.ofFloat(0F, 1F).setDuration(lineProgressTime)
        mAnimatorEnd?.addUpdateListener {
            if (mNode == mCount) { // 如果当前节点超过最后一个节点
                var progress = MAX_PROGRESS * (it.getAnimatedValue() as Float)
                mCurrentProgress = progress.toInt()
                // 动画结束后,状态为完成状态,执行回调
                if (mCurrentProgress == MAX_PROGRESS) {
                    mStatus = STATUS_COMPLE
                    progressListener?.onComplete()
                }
                postInvalidate()
            }
        }

绘制进度

遍历所有节点,绘制

                for (i in 0..mNode) {
                    if (i == mCount) { // 所有阶段结束后的最后一条线
                        drawLastLine(canvas)
                    } else {// 正常阶段
                        if (i < mNode) { // 已经过去的阶段
                            drawPastNode(canvas, i)
                        } else if (i == mNode) { // 请求中的阶段
                            drawCurrentNode(i, canvas)
                        }
                    }

绘制不同阶段的进度

 when (mStage) {
            STAGE_LINE -> {
                mMeasure!!.setPath(mLinePathList[i], false)
                var path = Path()
                var start = 0F
                var stop = mMeasure!!.length * (mCurrentProgress.toFloat() / MAX_PROGRESS)
                mMeasure!!.getSegment(start, stop, path, true)
                canvas.drawPath(path, mProgressPaint)
            }
            STAGE_CIRCLE -> {
                canvas.drawPath(mLinePathList[i], mProgressPaint)
                mMeasure!!.setPath(mCirclePathList[i], false)
                var path = Path()
                var start = 0F
                var stop = mMeasure!!.length * (mCurrentProgress.toFloat() / MAX_PROGRESS)
                mMeasure!!.getSegment(start, stop, path, true)
                canvas.drawPath(path, mProgressPaint)
            }
            STAGE_CIRCLE_CONTENT -> {
                canvas.drawPath(mLinePathList[i], mProgressPaint)
                canvas.drawPath(mCirclePathList[i], mProgressPaint)

                mMeasure!!.setPath(mCircleContentPathList[i], false)
                var path = Path()
                var start = 0F
                when (mRequestStatusList[mNode]) {
                    REQUEST_STATUS_SUCCESS -> {
                        var stop = (mMeasure!!.length
                                ?: 0F) * (mCurrentProgress.toFloat() / MAX_PROGRESS)
                        mMeasure!!.getSegment(start, stop, path, true)
                        canvas.drawPath(path, mProgressPaint)
                    }
                    REQUEST_STATUS_FAILURE -> {
                        if (mCurrentProgress > 50) {// 进度后50%时
                            mMeasure!!.getSegment(0F, mMeasure!!.length
                                    ?: 0F, path, true)
                            canvas.drawPath(path, mProgressPaint)
                            mMeasure!!.nextContour()
                            var stop = (mMeasure!!.length
                                    ?: 0F) * ((mCurrentProgress.toFloat() - 50) / (MAX_PROGRESS / 2))
                            mMeasure!!.getSegment(start, stop, path, true)
                            canvas.drawPath(path, mProgressPaint)
                        } else { // 进度前50%时,只绘制叉号的一条线
                            var stop = (mMeasure!!.length
                                    ?: 0F) * (mCurrentProgress.toFloat() / (MAX_PROGRESS / 2))
                            mMeasure!!.getSegment(start, stop, path, true)
                            canvas.drawPath(path, mProgressPaint)
                        }
                    }
                }
            }
        }

最后

大概就是这样吧,纯粹贴代码也不好理解,想了解的话可以移步github:https://github.com/ifadai/NodeProgress,有问题欢迎提出

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容