模仿途虎的登錄進度條——帶節點進度條

去年寫的,一直忘了發,這幾天發一下。

前段時間,項目中使用了阿里的號碼認證服務(一鍵登錄),登錄樣式模仿了途虎養車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,有問題歡迎提出

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章