自定义一个倒计时控件

主要需求

  1. 能进行倒计时
  2. 可以随时取消、终止倒计时
  3. 流畅跳动

先看实现效果

default.gif

这是一个倒计时5s的动画,可能录制帧数导致没有完全显示,大于1秒时每秒显示,小于1秒时显示小数,带加速度先快后慢跳动

实现思路

  1. 继承View来实现我们的控件
  2. 重写OnDraw方法来绘制进度条
  3. 每秒跳动一次大进度,通过Handle定时触发,然后这一秒内,通过动画控制进度条流动
  4. 圆弧进度条主要由一个大圆+百分比圆弧构成,大圆使用描边,设置大的描边宽度实现圆环效果
  5. 然后用圆弧覆盖在上面实现进度效果
  6. 因为圆的圆心角是360° 所以把100%进度拆成360分,每1%绘制3.6°圆弧即可

实现绘制固定进度

class CounterDownView : View {

    //region 绘图参数

    private lateinit var circlePaint: Paint
    private lateinit var textPaint: Paint
    private var ringWidthDp = 12f
    private var ringBackgroundColor = Color.GRAY
    private var ringFillColor = Color.GREEN

    //endregion

    //region 定时参数

    private var targetTime = 5000L
    private var currentTime = 1000L
    private val interval = 1000
    private val handle: Handler by lazy { Handler(Looper.myLooper()!!) }
    var progressListener: ProgressListener? = null

    //endregion


    @JvmOverloads
    constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : super(
        context,
        attrs,
        defStyleAttr
    ) {
        ringWidthDp = dip2px(12f)


        circlePaint = Paint().apply {
            isAntiAlias = true //抗锯齿
            isDither = true //防抖动
            strokeWidth = ringWidthDp
            shader = null
        }

        textPaint = Paint().apply {
            isAntiAlias = true //抗锯齿
            isDither = true //防抖动
            isFakeBoldText = true
            color = Color.parseColor("#00FF90")
            textSize = dip2px(25f)
            textAlign = Paint.Align.CENTER
            strokeWidth = 0f
        }

        ringBackgroundColor = Color.WHITE

    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        val center = this.width / 2f
        val radius = center - ringWidthDp / 2f - ringWidthDp

        drawBackgroundCircle(canvas, center, radius)
        drawProgressCircle(canvas, center, radius)
        drawShowText(canvas, center)
    }

    /**
     * 绘制背景圆环
     */
    private fun drawBackgroundCircle(canvas: Canvas, center: Float, radius: Float) {
        with(circlePaint) {
            color = ringBackgroundColor
            style = Paint.Style.STROKE
        }

        canvas.drawCircle(center, center, radius, circlePaint)
    }

    /**
     * 绘制当前进度
     */
    private fun drawProgressCircle(canvas: Canvas, center: Float, radius: Float) {
        val rectStartX = center - radius
        val rectEndX = center + radius
        //圆弧的外接正方形,宽高相等
        val oval = RectF(rectStartX, rectStartX, rectEndX, rectEndX)
        circlePaint.color = ringFillColor
        //计算绘图进度,转化成圆弧的圆心角
        val ringAngle = 360f * currentTime / targetTime
        //绘制弧
        canvas.drawArc(oval, -90f, ringAngle, false, circlePaint)
    }

    private fun drawShowText(canvas: Canvas, center: Float) {
        var showText = ""
        val diff = (targetTime - currentTime) / 1000.0
        showText = if (diff < 1) { //小于1时显示小数
            String.format("%1.1f", diff)
        } else {
            diff.toInt().toString()
        }
        //获取文字边框
        val textBound = Rect()
        textPaint.getTextBounds(showText, 0, showText.length, textBound)
        //计算文字基线高度,保证垂直居中
        val fontMetrics = textPaint.fontMetricsInt
        val baseLine = center + (fontMetrics.bottom - fontMetrics.top) / 2f - fontMetrics.bottom
        canvas.drawText(showText, center, baseLine, textPaint)
    }

    /**
     * 启动倒计时
     */
    fun start() {

    }

    /**
     * 暂停倒计时
     */
    fun pause() {

    }

    /**
     * 恢复倒计时
     */
    fun resume() {

    }

    /**
     * 停止倒计时
     */
    fun stop() {

    }
    
    //倒计时跳动控制核心
    private val counterRunnable = Runnable {
    
    }

    interface ProgressListener {
        fun onTick(counterDownView: CounterDownView): Boolean
        fun onFinish()
    }

    fun dip2px(dipValue: Float): Float {
        val scale = Resources.getSystem().displayMetrics.density
        return dipValue * scale + 0.5f
    }

至此,我们已经能绘制出一个20%进度进度条了

image.png

补全倒计时动态设置跳动功能 完整代码

class CounterDownView : View {

![default.gif](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/67fcde8cb8a844da8db92a3136eeb85c~tplv-k3u1fbpfcp-watermark.image?)
    //region 绘图参数

    private lateinit var circlePaint: Paint
    private lateinit var textPaint: Paint
    private var ringWidthDp = 12f
    private var ringBackgroundColor = Color.GRAY
    private var ringFillColor = Color.GREEN
    private var valueAnimation: ValueAnimator? = null
    private var counterRunnable: Runnable? = null
    //endregion

    //region 定时参数

    private var targetTime = 5000L
    private var currentTime = 1000L
    private val interval = 1000L
    private val counterHandler: Handler by lazy { Handler(Looper.myLooper()!!) }
    var progressListener: ProgressListener? = null

    //endregion

    @JvmOverloads
    constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : super(
        context,
        attrs,
        defStyleAttr
    ) {
        ringWidthDp = dip2px(12f)

        circlePaint = Paint().apply {
            isAntiAlias = true //抗锯齿
            isDither = true //防抖动
            strokeWidth = ringWidthDp
            shader = null
        }

        textPaint = Paint().apply {
            isAntiAlias = true //抗锯齿
            isDither = true //防抖动
            isFakeBoldText = true
            color = Color.parseColor("#00FF90")
            textSize = dip2px(25f)
            textAlign = Paint.Align.CENTER
            strokeWidth = 0f
        }

        ringBackgroundColor = Color.WHITE
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        val center = this.width / 2f
        val radius = center - ringWidthDp / 2f - ringWidthDp

        drawBackgroundCircle(canvas, center, radius)
        drawProgressCircle(canvas, center, radius)
        drawShowText(canvas, center)
    }

    /**
     * 绘制背景圆环
     */
    private fun drawBackgroundCircle(canvas: Canvas, center: Float, radius: Float) {
        with(circlePaint) {
            color = ringBackgroundColor
            style = Paint.Style.STROKE
        }

        canvas.drawCircle(center, center, radius, circlePaint)
    }

    /**
     * 绘制当前进度
     */
    private fun drawProgressCircle(canvas: Canvas, center: Float, radius: Float) {
        val rectStartX = center - radius
        val rectEndX = center + radius
        //圆弧的外接正方形,宽高相等
        val oval = RectF(rectStartX, rectStartX, rectEndX, rectEndX)
        circlePaint.color = ringFillColor
        //计算绘图进度,转化成圆弧的圆心角
        val ringAngle = 360f * currentTime / targetTime
        //绘制弧
        canvas.drawArc(oval, -90f, ringAngle, false, circlePaint)
    }

    private fun drawShowText(canvas: Canvas, center: Float) {
        var showText = ""
        val diff = (targetTime - currentTime) / 1000.0
        showText = if (diff < 1) { //小于1时显示小数
            String.format("%1.1f", diff)
        } else {
            diff.toInt().toString()
        }
        //获取文字边框
        val textBound = Rect()
        textPaint.getTextBounds(showText, 0, showText.length, textBound)
        //计算文字基线高度,保证垂直居中
        val fontMetrics = textPaint.fontMetricsInt
        val baseLine = center + (fontMetrics.bottom - fontMetrics.top) / 2f - fontMetrics.bottom
        canvas.drawText(showText, center, baseLine, textPaint)
    }

    /**
     * 启动倒计时
     */
    fun start(targetTime: Long = 5000L) {
        stop()
        //重置当前已走时间为0
        currentTime = 0
        this.targetTime = targetTime
        //先重绘一次界面
        invalidate()
        resume()
    }

    /**
     * 暂停倒计时
     */
    fun pause() {
        counterRunnable?.let {
            counterHandler.removeCallbacks(it)
        }
    }

    /**
     * 恢复倒计时
     */
    fun resume() {
        counterRunnable?.let {
            counterHandler.removeCallbacks(it)
        }

        //倒计时跳动控制核心
        counterRunnable = object : Runnable {
            override fun run() {
                //到时间了,如果已有动画未完成,则先强制让他完成动画
                valueAnimation?.end()
                //回调给使用方,触发一次倒计时
                if (progressListener?.onTick(this@CounterDownView) == true) {
                    //主动结束倒计时,并保留状态
                    return
                }
                //跳动一次时间为 interval ,从现在开始到下次跳秒前,我们使用值动画完成,让进度条平滑过渡
                valueAnimation =
                    ValueAnimator.ofInt(
                        this@CounterDownView.currentTime.toInt(),
                        (this@CounterDownView.currentTime + interval).toInt()
                    )
                valueAnimation?.let { ani ->
                    ani.addUpdateListener {
                        this@CounterDownView.currentTime = it.animatedValue.toString().toLong()
                        postInvalidate()
                    }
                    ani.addListener(object : AnimatorListenerAdapter() {
                        override fun onAnimationEnd(animation: Animator?) {
                            if (this@CounterDownView.currentTime >= targetTime) { //只要超过目标时间就主动提示完成
                                progressListener?.onFinish()
                                this@CounterDownView.currentTime = 0L
                            }
                        }
                    })
                    //设置动画插值器
                    ani.interpolator = DecelerateInterpolator()
                    ani.duration = this@CounterDownView.interval //设置动画执行时间
                    ani.start()
                }
                if ((this@CounterDownView.currentTime + this@CounterDownView.interval) >= this@CounterDownView.targetTime) {
                    //结束倒计时
                    return
                } else {
                    counterHandler.postDelayed(this, this@CounterDownView.interval)
                }
            }
        }
        // 为了让界面能显示总时间,延迟600ms再开始倒计时,也可以不延迟直接开始
        counterRunnable?.let {
            counterHandler.postDelayed(it, 600)
        }
    }

    /**
     * 停止倒计时
     */
    fun stop() {
        valueAnimation?.cancel()
        pause()
        valueAnimation = null

    }

    interface ProgressListener {
        fun onTick(counterDownView: CounterDownView): Boolean
        fun onFinish()
    }

    private fun dip2px(dipValue: Float): Float {
        val scale = Resources.getSystem().displayMetrics.density
        return dipValue * scale + 0.5f
    }
}

关于动画插值器修改

上面例子使用的插值器是DecelerateInterpolator,我们还可以换成线性动画LinearInterpolator

default.gif

关于插值器的用法这里就不详细说明了
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值