Android--圆形倒计时

3,207 阅读5分钟
“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情

Android--圆形倒计时

需求: 之前接受到一个需求根据开始时间与结束时间,与当前时间做比对展示一个倒计时的动画效果。 先上效果图,毕竟无图无真相:

Android Emulator - Pixel_2_API_31_2_5554 2022-09-03 12-15-46 00_00_00-00_00_30.gif

总共三种状态:未开始、进行中、已结束

image.png image.png image.png

一、分析

  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/…