“我报名参加金石方案1期应战——瓜分10万奖池,这是我的第1篇文章,点击检查活动详情”

Android–圆形倒计时

需求: 之前接受到一个需求依据开端时刻与完毕时刻,与当时时刻做比对展现一个倒计时的动画效果。 先上效果图,究竟无图无本相:

总共三种状况:未开端、进行中、已完毕

Android--圆形倒计时
Android--圆形倒计时
Android--圆形倒计时

一、剖析

  1. 首要它是一个圆圈,这个好画
  2. 怎么让它动起来,依据途径去展现动画,涉及到PathMeasure 该类途径动画的运用
  3. 依据时刻核算份额展现动画的位置

二、依据剖析

2.1、自定义view应该都懂,主要是ondraw()办法

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    when (mCircleStyleType) {
        CircleAnimatorViewStyleType.STYLE_TYPE_NOT_START -> {
            // 参数分别为 (文本 基线x 基线y 画笔),依据中心点以及文本基准线调整文本的中心
            mCenterBaseline?.let {
                canvas.drawText(
                    mHintText, mViewWidthCenterX,
                    it, mTextPaint
                )
            }
            canvas.drawCircle(
                mViewWidthCenterX,
                mViewHeightCenterY, mCircleRadius, mPaintBg
            )
        }
        CircleAnimatorViewStyleType.STYLE_TYPE_RUNNING -> {
            // 参数分别为 (文本 基线x 基线y 画笔),依据中心点以及文本基准线调整文本的中心
            mCenterBaseline?.let {
                canvas.drawText(
                    mRunningTimeHintText, mViewWidthCenterX.toFloat(),
                    it - mRunningTimeHintTextDistance, mTextPaint
                )
                canvas.drawText(
                    mRunningHintText, mViewWidthCenterX.toFloat(),
                    it + mRunningTimeHintTextDistance, mTextRunningHintPaint
                )
            }
            canvas.drawPath(mAnimaPath, mPaint)
            drawAnimationCircle(canvas)
        }
        CircleAnimatorViewStyleType.STYLE_TYPE_END -> {
            // 参数分别为 (文本 基线x 基线y 画笔),依据中心点以及文本基准线调整文本的中心
            mCenterBaseline?.let {
                canvas.drawText(
                    mHintText, mViewWidthCenterX,
                    it, mTextPaint
                )
            }
            canvas.drawCircle(
                mViewWidthCenterX,
                mViewHeightCenterY, mCircleRadius, mPaint
            )
        }
    }
}

上面三个办法便是对应前面说的三种状况:未开端、进行中、已完毕。

canvas.drawCircle()办法咱们画一个圆, canvas.drawText()制作圆中心的案牍,

未开端与已完毕状况都是画了一个圆,然后在圆中心写案牍,这个信任咱们都会

主要讲进行中这一状况的动画进程: 那该怎么讲解呢?,那就从运用开端讲起:

2.2、运用

xml如下:

<LinearLayout
    android:id="@+id/constraintlayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical">
    <com.example.myanimator.circleanimator.CircleAnimatorViewFinish
        android:id="@+id/cir_anima_finnish"
        android:layout_width="wrap_content"
        android:background="#17CC7D"
        app:progressWidth="8dp"
        app:circleRadius="40dp"
        app:isNeedAnimation="true"
        android:layout_marginTop="50dp"
        android:layout_height="wrap_content"/>
    <Button
        android:id="@+id/start_anima"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
</LinearLayout>

activity如下:

class CircleActivity : AppCompatActivity() {
    private lateinit var mBind: ActivityCircleScrollerBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mBind = DataBindingUtil.setContentView(this, R.layout.activity_circle_scroller)
        mBind.startAnima.setOnClickListener(){mBind.cirAnimaFinnish.setCircleStyleStart(System.currentTimeMillis()+3000,System.currentTimeMillis()+6*1000L)
        }
    }
}

上面主要是setCircleStyleStart()办法,传入你想要的开端时刻与完毕时刻即可

/**依据课程时段主动选择动画类型*/
fun setCircleStyleStart(
    startTime: Long,
    endTime: Long
) {
    mCircleStartTime = startTime
    mCircleEndTime = endTime
    val currentTime = System.currentTimeMillis()
    when {
        currentTime < startTime -> {//未开端
            setCircleStyleTypeNotStart(startTime, endTime)
        }
        currentTime in startTime until endTime -> {//进行中
            setCircleStyleTypeRunning(startTime, endTime)
        }
        currentTime >= endTime -> {//已完毕
            setCircleStyleTypeEnd(startTime, endTime)
        }
    }
}

setCircleStyleStart()办法主要是三种状况,是与当时时刻作比较,制作不同的状况。 主要看setCircleStyleTypeRunning(startTime, endTime)办法。

setCircleStyleTypeRunning(startTime, endTime)办法如下:

 /**进行中*/
    private fun setCircleStyleTypeRunning(
        startTime: Long, endTime: Long
    ) {
        setCircleCircleStyleType(CircleAnimatorViewStyleType.STYLE_TYPE_RUNNING, startTime, endTime)
//        LogUtil.d(
//            TAG,
//            "setCircleStyleTypeRunning:周期: ${endTime - startTime},已运转周期${System.currentTimeMillis() - startTime}"
//        )
        setCircleValueAnimatorStart(
            startTime,
            endTime,
            endTime - startTime,
            System.currentTimeMillis() - startTime
        )
    }
/**
 *设置圆圈展现的样式
 */
private fun setCircleCircleStyleType(circleStyleType: CircleAnimatorViewStyleType, startTime: Long, endTime: Long) {
    mCircleStyleType = circleStyleType
    Log.e(TAG, "setCircleCircleStyleType type $circleStyleType")
    when (mCircleStyleType) {
        CircleAnimatorViewStyleType.STYLE_TYPE_NOT_START -> {
            mHintText = "未开端"
            setStartTimeJobCollect(startTime, endTime)
        }
        CircleAnimatorViewStyleType.STYLE_TYPE_END -> {
            mHintText = "已完毕"
            mObserverAnimatorEndListener?.invoke()
        }
    }
    //避免之前是形式二 有动画
    circleValueAnimator?.cancel()
    mRunningFlowJob?.cancel()
    mPaintBg.strokeWidth = mProgressWidth
    postInvalidate()
}

setCircleCircleStyleType 办法撤销之前是进行中的时分撤销之前的计时动画, 所以进行中主要是看 setCircleValueAnimatorStart( startTime, endTime, endTime - startTime, System.currentTimeMillis() - startTime)办法

/**
 *
 * 该办法只要 mCircleStyleType == 2时才会收效
 *@param duration  总周期
 *@param runningDuration 已运转周期
 */
private fun setCircleValueAnimatorStart(
    startTime: Long, endTime: Long,
    duration: Long,
    runningDuration: Long
) {
    if (mCircleStyleType == CircleAnimatorViewStyleType.STYLE_TYPE_RUNNING) {
        if (runningDuration >= duration) {
            setCircleStyleTypeEnd(startTime, endTime)
            return
        }
        //避免同样巨细,掩盖的不全
        mPaintBg.strokeWidth = mProgressWidth - 2
        setAnimator(duration, runningDuration)
    }
}

上面的办法主要 已运转周期大于总周期,那便是走周期完毕的ui状况

mPaintBg.strokeWidth = mProgressWidth - 2这段代码为什么要减2呢? 由于咱们的动画是首要灰色的是布景色,绿色的动画是咱们画的途径动画setAnimator办法。

setAnimator办法便是咱们的进行中的中心办法。

private fun setAnimator(
    duration: Long,
    runningDuration: Long
) {
    if (mPathMeasure == null) {
        init()
    }
    //倒计时时刻与案牍,核算剩下时刻
    mRunningRemainTime =
        (mCircleEndTime - mCircleStartTime) - (System.currentTimeMillis() - mCircleStartTime)
    mRunningTimeHintText = TimeFormatUtils.secToTime(mRunningRemainTime.toInt() / 1000)
    //收集倒计时的文本
    setRunningTextJobCollect()
    var runningProgress = 0f
    //避免周期越界
    if (runningDuration <= 0) {
        runningDurationLength = mPathMeasure!!.length
    } else {
        //动画圆倒计时,核算已运转的长度
        runningDurationLength = runningDuration / duration * mPathMeasure!!.length
        runningProgress = (runningDuration.toFloat() / duration.toFloat()).toFloat()
    }
    circleValueAnimator = ValueAnimator.ofFloat(0f, 1f)
    circleValueAnimator?.let { circleValueAnimator ->
        circleValueAnimator.repeatCount = 0
        //设置动画的总周期,为你设置的开端完毕时刻的周期
        circleValueAnimator.duration = duration.toLong()
        circleValueAnimator.interpolator = LinearInterpolator()
        circleValueAnimator.addUpdateListener { animation ->
            mCurAnimValue = (animation.animatedValue as Float) + runningProgress
            if (mCurAnimValue <= 1f) {
                postInvalidate()
            } else {
                circleValueAnimator.cancel()
            }
        }
        circleValueAnimator.addListener(object : AnimatorListenerAdapter() {
            override fun onAnimationEnd(animation: Animator) {
                super.onAnimationEnd(animation)
                setCircleStyleTypeEnd(mCircleStartTime, mCircleEndTime)
            }
        })
        //避免android 5.0 属性动画失效
        ValueAnimatorUtil.resetDurationScaleIfDisable()
        circleValueAnimator.start()
    }
}

1.首要获取当时剩下的时刻mRunningRemainTime。 2.TimeFormatUtils.secToTime(mRunningRemainTime.toInt() / 1000)办法将剩下的时刻转化为时分秒。 3.setRunningTextJobCollect(),收集运转的倒计时案牍 4.runningProgress已运转周期在总的周期中所占的份额,对应动画的总周期的0-1f 5.addUpdateListener监听动画的进展更新mPathMeasure的长度 6.调用 postInvalidate()办法,走onDraw()办法 7. ValueAnimatorUtil.resetDurationScaleIfDisable()办法避免动画5.0失效

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    when (mCircleStyleType) {
    ....
        CircleAnimatorViewStyleType.STYLE_TYPE_RUNNING -> {
            // 参数分别为 (文本 基线x 基线y 画笔),依据中心点以及文本基准线调整文本的中心
            mCenterBaseline?.let {
                canvas.drawText(//倒计时案牍
                    mRunningTimeHintText, mViewWidthCenterX.toFloat(),
                    it - mRunningTimeHintTextDistance, mTextPaint
                )
                canvas.drawText(//"间隔完毕"的案牍
                    mRunningHintText, mViewWidthCenterX.toFloat(),
                    it + mRunningTimeHintTextDistance, mTextRunningHintPaint
                )
            }
            //灰色圈
            canvas.drawPath(mAnimaPath, mPaint)
            //绿色圈
            drawAnimationCircle(canvas)
        }
         ...
        }
    }
}

上面只看运转中: 动画的中心为drawAnimationCircle,mPathMeasure的用法是先掩盖蓝色圈然后,渐渐褪去,顺时针画的圆弧,使用mPathMeasure 后退制作,模仿逆时针

    /**先掩盖蓝色圈然后,渐渐褪去,顺时针画的圆弧,使用mPathMeasure 后退制作,模仿逆时针*/
    private fun drawAnimationCircle(canvas: Canvas) {
        if (mPathMeasure == null) {
            return
        }
        endLength = (mPathMeasure!!.length * (1 - mCurAnimValue))
        mDstPath.reset()
//        LogUtil.d(
//            TAG,
//            "drawAnimationCircle-endLength: " + endLength + "圆制作 动画进展" + mCurAnimValue + "mPathMeasurelength:" + mPathMeasure!!.getLength()
//        )
        mPathMeasure!!.getSegment(0f, endLength, mDstPath, true)
        canvas.drawPath(mDstPath, mPaintBg)
    }

//灰色圈 canvas.drawPath(mAnimaPath, mPaint) //绿色圈 drawAnimationCircle(canvas)

上面适当所以先画了个灰色圆圈,再画了个绿色圆圈,然后依据动画进展,逐渐减少绿色圆圈的掩盖程度。

总结

1.依据传入的开端,完毕时刻,与当时时刻做比照,判断处于哪种运转状况 2.将开端时刻与当时时刻做比照获取已运转时刻,与当时的倒计时的总周期作比照 3.开启一个动画使用动画的周期进行ui的制作刷新 4.画两个圆,一个是布景圆灰色,绿色圆咱们的动画圆运用mPathMeasure进行制作的

代码如下:

/**
 * @author tgw
 * @date 2021/12/10
 * @describe 倒计时圆动画--
 */
class CircleAnimatorViewFinish(context: Context, attrs: AttributeSet?) : View(context, attrs),
    CoroutineScope {
    override val coroutineContext: CoroutineContext
        get() = Job() + Dispatchers.Main
    companion object {
        private const val TAG = "CircleAnimatorView"
        fun dip2px(context: Context, dpValue: Float): Float {
            val scale = context.resources.displayMetrics.density
            return dpValue * scale + 0.5f
        }
        /**
         * 定义几个圆圈类型的常量
         *  动画类型,1 未开端,2进行中,3已完毕
         */
//        private const val STYLE_TYPE_NOT_START = 1
//        private const val STYLE_TYPE_RUNNING = 2
//        private const val STYLE_TYPE_END = 3
    }
    private lateinit var mAnimaPath: Path
    private lateinit var mDstPath: Path
    private var circleValueAnimator: ValueAnimator? = null
    private var mPaint: Paint
    private var mPaintBg: Paint
    private var mTextPaint: Paint
    //运转中特有画笔,文字要小
    private var mTextRunningHintPaint: Paint
    //整个控件的巨细规模 也是圆的规模,也是文字的规模
    private var circleRectF: RectF? = null
    private var mPathMeasure: PathMeasure? = null
    //动画进展
    private var mCurAnimValue = 0f
    private var endLength = 0f
    private var mProgressWidth = 0f
    private var mCircleRadius = 0f
    //画笔颜色
    private var mBgColor = Color.parseColor("#FF1FB5AB")
    private var mProgressBgColor = Color.parseColor("#FFEFEFEF")
    private var mRunningHintTextColor = Color.parseColor("#FF999999")
    //控件巨细
    private var mViewHeight = 0
    private var mViewWidth = 0
    //控件中心点
    private var mViewWidthCenterX = 0F
    private var mViewHeightCenterY = 0F
    //2进行中 动画圆倒计时,核算已运转的长度
    private var runningDurationLength: Float = 0f
    //动画类型,1 未开端,2进行中,3已完毕
    private var mCircleStyleType:CircleAnimatorViewStyleType = CircleAnimatorViewStyleType.STYLE_TYPE_NOT_START
    //讲堂开端时刻
    private var mCircleStartTime = 0L
    //讲堂完毕时刻
    private var mCircleEndTime = 0L
    //动画类型,1 未开端,3已完毕的案牍与居中基准线
    private var mHintTextSize = 36f
    private var mHintText = ""
    private var mCenterBaseline: Float? = 0f
    //动画类型,进行中
    private var mRunningRemainTime = 0L //适当于倒计时
    private var mRunningTimeHintText = ""
    private var mRunningTimeHintTextDistance = 0f  //文本相距间隔
    private var mRunningHintText = "间隔完毕"
    //协程流
    private var mRunningFlowJob: Job? = null  //进行中的倒计时案牍
    private var mStartTimeFlowJob: Job? = null //未开端,到时刻后转化为进行中
    //已完毕的回调监听
    var mObserverAnimatorEndListener: (() -> Unit)? = null
    init {
        setLayerType(LAYER_TYPE_SOFTWARE, null)
        if (attrs != null) {
            val typedArray =
                getContext().obtainStyledAttributes(attrs, R.styleable.CircleAnimatorView)
            mBgColor = typedArray.getColor(R.styleable.CircleAnimatorView_bgColor, mBgColor)
            mProgressBgColor = typedArray.getColor(
                R.styleable.CircleAnimatorView_animationProgressColor,
                mProgressBgColor
            )
            mProgressWidth = dip2px(
                context,
                typedArray.getDimension(
                    R.styleable.CircleAnimatorView_progressWidth,
                    mProgressWidth
                )
            )
            mCircleRadius =
            dip2px(
                    context, typedArray.getDimension(
                        R.styleable.CircleAnimatorView_circleRadius,
                        mCircleRadius
                    )
                )
            mHintTextSize = dip2px(
                context, typedArray.getDimension(
                    R.styleable.CircleAnimatorView_circleTextHintSize, mHintTextSize
                )
            )
            typedArray.recycle()
        }
        //适当于蓝色圆圈掩盖灰色圆圈,然后蓝色圈圈渐渐消散
        mPaint = Paint()
        mPaint.isAntiAlias = true // 去除锯齿
        mPaint.strokeWidth = mProgressWidth
        mPaint.style = Paint.Style.STROKE
        mPaint.color = mProgressBgColor
        //适当于蓝色圆圈
        mPaintBg = Paint()
        mPaintBg.isAntiAlias = true // 去除锯齿
        mPaintBg.strokeWidth = mProgressWidth - 2
        mPaintBg.style = Paint.Style.STROKE
        mPaintBg.color = mBgColor
        mPaintBg.strokeCap = Paint.Cap.ROUND
        mTextPaint = Paint() // 创立每个形式都有的文字画笔
        mTextPaint.color = Color.BLACK // 设置颜色
        mTextPaint.isAntiAlias = true // 去除锯齿
        mTextPaint.style = Paint.Style.FILL // 设置样式
        mTextPaint.textSize = mHintTextSize // 设置字体巨细
        mTextPaint.textAlign = Paint.Align.CENTER
        //间隔讲堂完毕案牍的文字画笔
        mTextRunningHintPaint = Paint() // 创立每个形式都有的文字画笔
        mTextRunningHintPaint.color = mRunningHintTextColor // 设置颜色
        mTextRunningHintPaint.isAntiAlias = true // 去除锯齿
        mTextRunningHintPaint.style = Paint.Style.FILL // 设置样式
        mTextRunningHintPaint.textSize = ScreenUtils.dip2px(context, 24f) // 设置字体巨细
        mTextRunningHintPaint.textAlign = Paint.Align.CENTER
    }
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        mViewWidth = measureWidthOrHeight(widthMeasureSpec)
        mViewHeight = measureWidthOrHeight(heightMeasureSpec)
        setMeasuredDimension(mViewWidth, mViewHeight)
        init()
    }
    private fun measureWidthOrHeight(measureSpec: Int): Int {
        var result = 0
        //获取当时View的测量形式
        val mode = MeasureSpec.getMode(measureSpec)
        //精准形式获取当时Viwe测量后的值,如果是最大值形式,会获取父View的巨细.
        val size = MeasureSpec.getSize(measureSpec)
        if (mode == MeasureSpec.EXACTLY) {
            //当测量形式为精准形式,回来设定的值
            result = size
        } else {
            //设置为WrapContent的默许巨细,圆的直径加上画笔宽度
            result = (mCircleRadius * 2 + mProgressWidth).toInt()
            if (mode == MeasureSpec.AT_MOST) {
                //当形式为最大值的时分,默许巨细和父类View的巨细进行比照,回来最小的值
                result = Math.min(result, size)
            }
        }
        return result
    }
    private fun init() {
        val path = mCircleRadius * 2
        val left = (mViewWidth - mCircleRadius * 2) / 2
        val top = (mViewHeight - mCircleRadius * 2) / 2
        mViewWidthCenterX = (mViewWidth / 2).toFloat()
        mViewHeightCenterY = (mViewHeight / 2).toFloat()
        //画圆
        circleRectF = RectF(left, top, path + left, path + top)
        mAnimaPath = Path()
        mDstPath = Path()
//        mAnimaPath.addArc(circleRectF, -90f, 359f)
        mAnimaPath.arcTo(circleRectF!!, -90f, 359f, true)
        mPathMeasure = PathMeasure(mAnimaPath, true)
        //画居中文本,核算基准线
        // 参数分别为 (文本 基线x 基线y 画笔),依据中心点以及文本基准线调整文本的中心
        val fontMetrics: Paint.FontMetrics = mTextPaint.fontMetrics
        val distance = (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom
        mCenterBaseline = circleRectF?.centerY()?.plus(distance)
        //运转时 两行文本相距间隔
        mRunningTimeHintTextDistance = ScreenUtils.dip2px(context, 30f)
    }
    /**依据课程时段主动选择动画类型*/
    fun setCircleStyleStart(
        startTime: Long,
        endTime: Long
    ) {
        mCircleStartTime = startTime
        mCircleEndTime = endTime
        val currentTime = System.currentTimeMillis()
        when {
            currentTime < startTime -> {
                setCircleStyleTypeNotStart(startTime, endTime)
            }
            currentTime in startTime until endTime -> {
                setCircleStyleTypeRunning(startTime, endTime)
            }
            currentTime >= endTime -> {
                setCircleStyleTypeEnd(startTime, endTime)
            }
        }
    }
    /**未开端*/
    private fun setCircleStyleTypeNotStart(startTime: Long, endTime: Long) {
        val currentTime = System.currentTimeMillis()
        if (currentTime < startTime) {
            setCircleCircleStyleType(CircleAnimatorViewStyleType.STYLE_TYPE_NOT_START, startTime, endTime)
        }
    }
    /**进行中*/
    private fun setCircleStyleTypeRunning(
        startTime: Long, endTime: Long
    ) {
        setCircleCircleStyleType(CircleAnimatorViewStyleType.STYLE_TYPE_RUNNING, startTime, endTime)
//        LogUtil.d(
//            TAG,
//            "setCircleStyleTypeRunning:周期: ${endTime - startTime},已运转周期${System.currentTimeMillis() - startTime}"
//        )
        setCircleValueAnimatorStart(
            startTime,
            endTime,
            endTime - startTime,
            System.currentTimeMillis() - startTime
        )
    }
    /**已完毕*/
    private fun setCircleStyleTypeEnd(startTime: Long, endTime: Long) {
        val currentTime = System.currentTimeMillis()
        if (currentTime >= endTime) {
            setCircleCircleStyleType(CircleAnimatorViewStyleType.STYLE_TYPE_END, startTime, endTime)
        }
    }
    /**
     *
     * 该办法只要 mCircleStyleType == 2时才会收效
     *@param duration  总周期
     *@param runningDuration 已运转周期
     */
    private fun setCircleValueAnimatorStart(
        startTime: Long, endTime: Long,
        duration: Long,
        runningDuration: Long
    ) {
        if (mCircleStyleType == CircleAnimatorViewStyleType.STYLE_TYPE_RUNNING) {
            if (runningDuration >= duration) {
                setCircleStyleTypeEnd(startTime, endTime)
                return
            }
            //避免同样巨细,掩盖的不全
            mPaintBg.strokeWidth = mProgressWidth - 2
            setAnimator(duration, runningDuration)
        }
    }
    /**
     *设置圆圈展现的样式
     */
    private fun setCircleCircleStyleType(circleStyleType: CircleAnimatorViewStyleType, startTime: Long, endTime: Long) {
        mCircleStyleType = circleStyleType
        Log.e(TAG, "setCircleCircleStyleType type $circleStyleType")
        when (mCircleStyleType) {
            CircleAnimatorViewStyleType.STYLE_TYPE_NOT_START -> {
                mHintText = "未开端"
                setStartTimeJobCollect(startTime, endTime)
            }
            CircleAnimatorViewStyleType.STYLE_TYPE_END -> {
                mHintText = "已完毕"
                mObserverAnimatorEndListener?.invoke()
            }
        }
        //避免之前是形式二 有动画
        circleValueAnimator?.cancel()
        mRunningFlowJob?.cancel()
        mPaintBg.strokeWidth = mProgressWidth
        postInvalidate()
    }
    /**进入时是未开端 课程到开端时刻后 主动ui调整*/
    private fun setStartTimeJobCollect(startTime: Long, endTime: Long) {
        mStartTimeFlowJob?.cancel()
        /**辅助进入的时分为未开端,到了时刻点将为开端*/
        val countDownStartTimeFlow = flow<Boolean> {
            while (mCircleStyleType == CircleAnimatorViewStyleType.STYLE_TYPE_NOT_START) {
                delay(1000)
                if (System.currentTimeMillis() > startTime) {
                    emit(true)
                } else {
                    emit(false)
                }
            }
        }.flowOn(Dispatchers.IO)
        mStartTimeFlowJob = launch(Dispatchers.Main) {
            countDownStartTimeFlow.collect {
                if (it) {
                    setCircleStyleTypeRunning(startTime, endTime)
                    mStartTimeFlowJob?.cancel()
                }
            }
        }
    }
    private fun setAnimator(
        duration: Long,
        runningDuration: Long
    ) {
        if (mPathMeasure == null) {
            init()
        }
        //倒计时时刻与案牍,核算剩下时刻
        mRunningRemainTime =
            (mCircleEndTime - mCircleStartTime) - (System.currentTimeMillis() - mCircleStartTime)
        mRunningTimeHintText = TimeFormatUtils.secToTime(mRunningRemainTime.toInt() / 1000)
        //收集倒计时的文本
        setRunningTextJobCollect()
        var runningProgress = 0f
        if (runningDuration <= 0) {
            runningDurationLength = mPathMeasure!!.length
        } else {
            //动画圆倒计时,核算已运转的长度
            runningDurationLength = runningDuration / duration * mPathMeasure!!.length
            runningProgress = (runningDuration.toFloat() / duration.toFloat()).toFloat()
        }
        circleValueAnimator = ValueAnimator.ofFloat(0f, 1f)
        circleValueAnimator?.let { circleValueAnimator ->
            circleValueAnimator.repeatCount = 0
            circleValueAnimator.duration = duration.toLong()
            circleValueAnimator.interpolator = LinearInterpolator()
            circleValueAnimator.addUpdateListener { animation ->
                mCurAnimValue = (animation.animatedValue as Float) + runningProgress
                if (mCurAnimValue <= 1f) {
                    postInvalidate()
                } else {
                    circleValueAnimator.cancel()
                }
            }
            circleValueAnimator.addListener(object : AnimatorListenerAdapter() {
                override fun onAnimationEnd(animation: Animator) {
                    super.onAnimationEnd(animation)
                    setCircleStyleTypeEnd(mCircleStartTime, mCircleEndTime)
                }
            })
            //避免android 5.0 属性动画失效
            ValueAnimatorUtil.resetDurationScaleIfDisable()
            circleValueAnimator.start()
        }
    }
    /**
     *运转时动画的倒计时案牍
     */
    private fun setRunningTextJobCollect() {
        mRunningFlowJob?.cancel()
        var time = 0
        val countDownRunningStyleTextHint = flow<String> {
            while (mCircleStyleType == CircleAnimatorViewStyleType.STYLE_TYPE_RUNNING) {
                delay(500)
                mRunningRemainTime =
                    (mCircleEndTime - mCircleStartTime) - (System.currentTimeMillis() - mCircleStartTime)
                time = mRunningRemainTime.toInt() / 1000
                emit(TimeFormatUtils.secToTime(time))
            }
        }.flowOn(Dispatchers.IO)
        mRunningFlowJob = launch {
            countDownRunningStyleTextHint.collect {
                mRunningTimeHintText = it
            }
        }
    }
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        when (mCircleStyleType) {
            CircleAnimatorViewStyleType.STYLE_TYPE_NOT_START -> {
                // 参数分别为 (文本 基线x 基线y 画笔),依据中心点以及文本基准线调整文本的中心
                mCenterBaseline?.let {
                    canvas.drawText(
                        mHintText, mViewWidthCenterX,
                        it, mTextPaint
                    )
                }
                canvas.drawCircle(
                    mViewWidthCenterX,
                    mViewHeightCenterY, mCircleRadius, mPaintBg
                )
            }
            CircleAnimatorViewStyleType.STYLE_TYPE_RUNNING -> {
                // 参数分别为 (文本 基线x 基线y 画笔),依据中心点以及文本基准线调整文本的中心
                mCenterBaseline?.let {
                    canvas.drawText(
                        mRunningTimeHintText, mViewWidthCenterX.toFloat(),
                        it - mRunningTimeHintTextDistance, mTextPaint
                    )
                    canvas.drawText(
                        mRunningHintText, mViewWidthCenterX.toFloat(),
                        it + mRunningTimeHintTextDistance, mTextRunningHintPaint
                    )
                }
                canvas.drawPath(mAnimaPath, mPaint)
                drawAnimationCircle(canvas)
            }
            CircleAnimatorViewStyleType.STYLE_TYPE_END -> {
                // 参数分别为 (文本 基线x 基线y 画笔),依据中心点以及文本基准线调整文本的中心
                mCenterBaseline?.let {
                    canvas.drawText(
                        mHintText, mViewWidthCenterX,
                        it, mTextPaint
                    )
                }
                canvas.drawCircle(
                    mViewWidthCenterX,
                    mViewHeightCenterY, mCircleRadius, mPaint
                )
            }
        }
    }
    /**先掩盖蓝色圈然后,渐渐褪去,顺时针画的圆弧,使用mPathMeasure 后退制作,模仿逆时针*/
    private fun drawAnimationCircle(canvas: Canvas) {
        if (mPathMeasure == null) {
            return
        }
        endLength = (mPathMeasure!!.length * (1 - mCurAnimValue))
        mDstPath.reset()
//        LogUtil.d(
//            TAG,
//            "drawAnimationCircle-endLength: " + endLength + "圆制作 动画进展" + mCurAnimValue + "mPathMeasurelength:" + mPathMeasure!!.getLength()
//        )
        mPathMeasure!!.getSegment(0f, endLength, mDstPath, true)
        canvas.drawPath(mDstPath, mPaintBg)
    }
    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        onDestroy()
    }
    fun onDestroy() {
        mRunningFlowJob?.cancel()
        mStartTimeFlowJob?.cancel()
        circleValueAnimator?.cancel()
        coroutineContext.cancel()
        mObserverAnimatorEndListener = null
    }
    enum class CircleAnimatorViewStyleType{
        //未开端
        STYLE_TYPE_NOT_START ,
        //进行中
        STYLE_TYPE_RUNNING ,
        //已完毕
        STYLE_TYPE_END ,
    }
}
/**
 * @author tgw
 * @date 2021/12/13
 * @describe  避免 Android5.0 动画无效
 */
public class ValueAnimatorUtil {
    /**
     * 如果动画被禁用,则重置动画缩放时长
     */
    public static void resetDurationScaleIfDisable() {
        if (getDurationScale() == 0)
            resetDurationScale();
    }
    /**
     * 重置动画缩放时长
     */
    public static void resetDurationScale() {
        try {
            getField().setFloat(null, 1);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    private static float getDurationScale() {
        try {
            return getField().getFloat(null);
        } catch (Exception e) {
            e.printStackTrace();
            return -1;
        }
    }
    @NonNull
    private static Field getField() throws NoSuchFieldException {
        Field field = ValueAnimator.class.getDeclaredField("sDurationScale");
        field.setAccessible(true);
        return field;
    }
}
/**
 * @author tgw
 * @date 2021/7/26
 * @describe
 */
object TimeFormatUtils {
    val YY_MM_DD_HH_MM_SS = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH)
    val HH_MM = SimpleDateFormat("HH:mm", Locale.ENGLISH)
    val YY_MM_DD = SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
    val HH_MM_SS = SimpleDateFormat("HH:mm:ss", Locale.ENGLISH)
    /*
     * 将时刻戳转化为时刻
     */
    fun stampToDate(time: Long, format: SimpleDateFormat): String? {
        val res: String
        val date = Date(time)
        res = format.format(date)
        return res
    }
    /*
     * 将时刻转化为时刻戳
     */
    @Throws(ParseException::class)
    fun dateToStamp(time: String?, format: SimpleDateFormat): String? {
        val date = format.parse(time)
        val ts = date.time
        return ts.toString()
    }
    /**将秒换为时分秒00:00:00*/
    fun secToTime(time: Int): String {
        var timeStr: String = ""
        var hour = 0
        var minute = 0
        var second = 0
        if (time <= 0) {
            return "00:00:00"
        } else {
            minute = time / 60
            if (minute < 60) {
                second = time % 60
                timeStr = "00:" + unitFormat(minute) + ":" + unitFormat(second)
            } else {
                hour = minute / 60
                if (hour > 99) {
                    return "99:59:59"
                }
                minute %= 60
                second = time - hour * 3600 - minute * 60
                timeStr = unitFormat(hour) + ":" + unitFormat(minute) + ":" + unitFormat(second)
            }
        }
        return timeStr
    }
    /**案牍补0*/
    private fun unitFormat(i: Int): String {
        var retStr: String? = null
        retStr = if (i >= 0 && i < 10) "0" + Integer.toString(i) else "" + i
        return retStr
    }
}

参阅:

Android自定义view入门与实战

Android 5.0 动画失效:blog.csdn.net/u011387817/…