作用演示

如何仿一个抖音极速版领现金的进度条动画?

不仅仅是完成作用,要封装,就封装好

看完了演示的作用,你是否在考虑,代码应该怎么完成?先不着急写代码,先想想哪些当地是要能够动态装备的。首先第一个,进度条的形状是不是要能够换?然后进度条的背景色和填充的色彩,以及动画的时长是不是也要能够装备?没错,开始位置是不是也要能够换?最好还要让速度能够一会快一会慢对吧,画笔的笔帽是不是还能够挑选平的或圆的?带着这些问题,咱们再开始写代码。

代码完成

咱们写一个自界说View,把能够动态装备的当地想好后,就能够界说自界说特点了。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="DoraProgressView">
        <attr name="dview_progressType">
            <enum name="line" value="0"/>
            <enum name="semicircle" value="1"/>
            <enum name="semicircleReverse" value="2"/>
            <enum name="circle" value="3"/>
            <enum name="circleReverse" value="4"/>
        </attr>
        <attr name="dview_progressOrigin">
            <enum name="left" value="0"/>
            <enum name="top" value="1"/>
            <enum name="right" value="2"/>
            <enum name="bottom" value="3"/>
        </attr>
        <attr format="dimension|reference" name="dview_progressWidth"/>
        <attr format="color|reference" name="dview_progressBgColor"/>
        <attr format="color|reference" name="dview_progressHoverColor"/>
        <attr format="integer" name="dview_animationTime"/>
        <attr name="dview_paintCap">
            <enum name="flat" value="0"/>
            <enum name="round" value="1"/>
        </attr>
    </declare-styleable>
</resources>

然后咱们不管三七二十一,先把自界说特点解析出来。

private fun initAttrs(context: Context, attrs: AttributeSet?, defStyleAttr: Int) {
    val a = context.obtainStyledAttributes(
        attrs,
        R.styleable.DoraProgressView,
        defStyleAttr,
        0
    )
    when (a.getInt(R.styleable.DoraProgressView_dview_progressType, PROGRESS_TYPE_LINE)) {
        0 -> progressType = PROGRESS_TYPE_LINE
        1 -> progressType = PROGRESS_TYPE_SEMICIRCLE
        2 -> progressType = PROGRESS_TYPE_SEMICIRCLE_REVERSE
        3 -> progressType = PROGRESS_TYPE_CIRCLE
        4 -> progressType = PROGRESS_TYPE_CIRCLE_REVERSE
    }
    when (a.getInt(R.styleable.DoraProgressView_dview_progressOrigin, PROGRESS_ORIGIN_LEFT)) {
        0 -> progressOrigin = PROGRESS_ORIGIN_LEFT
        1 -> progressOrigin = PROGRESS_ORIGIN_TOP
        2 -> progressOrigin = PROGRESS_ORIGIN_RIGHT
        3 -> progressOrigin = PROGRESS_ORIGIN_BOTTOM
    }
    when(a.getInt(R.styleable.DoraProgressView_dview_paintCap, 0)) {
        0 -> paintCap = Paint.Cap.SQUARE
        1 -> paintCap = Paint.Cap.ROUND
    }
    progressWidth = a.getDimension(R.styleable.DoraProgressView_dview_progressWidth, 30f)
    progressBgColor =
        a.getColor(R.styleable.DoraProgressView_dview_progressBgColor, Color.GRAY)
    progressHoverColor =
        a.getColor(R.styleable.DoraProgressView_dview_progressHoverColor, Color.BLUE)
    animationTime = a.getInt(R.styleable.DoraProgressView_dview_animationTime, 1000)
    a.recycle()
}

解析完自界说特点,切勿忘了开释TypedArray。接下来咱们考虑下一步,丈量。半圆是不是不要那么大的画板对吧,咱们在丈量的时分就要充分考虑进去。

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    progressBgPaint.strokeWidth = progressWidth
    progressHoverPaint.strokeWidth = progressWidth
    if (progressType == PROGRESS_TYPE_LINE) {
        // 线
        var left = 0f
        var top = 0f
        var right = measuredWidth.toFloat()
        var bottom = measuredHeight.toFloat()
        val isHorizontal = when(progressOrigin) {
            PROGRESS_ORIGIN_LEFT, PROGRESS_ORIGIN_RIGHT -> true
            else -> false
        }
        if (isHorizontal) {
            top = (measuredHeight - progressWidth) / 2
            bottom = (measuredHeight + progressWidth) / 2
            progressBgRect[left + progressWidth / 2, top, right - progressWidth / 2] = bottom
        } else {
            left = (measuredWidth - progressWidth) / 2
            right = (measuredWidth + progressWidth) / 2
            progressBgRect[left, top + progressWidth / 2, right] = bottom - progressWidth / 2
        }
    } else if (progressType == PROGRESS_TYPE_CIRCLE || progressType == PROGRESS_TYPE_CIRCLE_REVERSE) {
        // 圆
        var left = 0f
        val top = 0f
        var right = measuredWidth
        var bottom = measuredHeight
        progressBgRect[left + progressWidth / 2, top + progressWidth / 2, right - progressWidth / 2] =
            bottom - progressWidth / 2
    } else {
        // 半圆
        val isHorizontal = when(progressOrigin) {
            PROGRESS_ORIGIN_LEFT, PROGRESS_ORIGIN_RIGHT -> true
            else -> false
        }
        val min = measuredWidth.coerceAtMost(measuredHeight)
        var left = 0f
        var top = 0f
        var right = 0f
        var bottom = 0f
        if (isHorizontal) {
            if (measuredWidth >= min) {
                left = ((measuredWidth - min) / 2).toFloat()
                right = left + min
            }
            if (measuredHeight >= min) {
                bottom = top + min
            }
            progressBgRect[left + progressWidth / 2, top + progressWidth / 2, right - progressWidth / 2] =
                bottom - progressWidth / 2
            setMeasuredDimension(
                MeasureSpec.makeMeasureSpec(
                    (right - left).toInt(),
                    MeasureSpec.EXACTLY
                ),
                MeasureSpec.makeMeasureSpec(
                    (bottom - top + progressWidth).toInt() / 2,
                    MeasureSpec.EXACTLY
                )
            )
        } else {
            if (measuredWidth >= min) {
                right = left + min
            }
            if (measuredHeight >= min) {
                top = ((measuredHeight - min) / 2).toFloat()
                bottom = top + min
            }
            progressBgRect[left + progressWidth / 2, top + progressWidth / 2, right - progressWidth / 2] =
                bottom - progressWidth / 2
            setMeasuredDimension(
                MeasureSpec.makeMeasureSpec(
                    (right - left + progressWidth).toInt() / 2,
                    MeasureSpec.EXACTLY
                ),
                MeasureSpec.makeMeasureSpec(
                    (bottom - top).toInt(),
                    MeasureSpec.EXACTLY
                )
            )
        }
    }
}

View的onMeasure()办法是不是默许调用了一个

super.onMeasure(widthMeasureSpec, heightMeasureSpec)

它最终会调用setMeasuredDimension()办法来确定最终丈量的成果吧。假如咱们对默许的丈量不满意,咱们能够自己改,最终也调用setMeasuredDimension()办法把丈量成果承认。半圆,假如是水平的状况下,咱们的宽度就只要一半,相反假如是垂直的半圆,咱们高度就只要一半。最终咱们画仍是照旧画,只不过在最终把画到外面的部分移动到画板上显示出来。接下来便是咱们最重要的绘图环节了。

override fun onDraw(canvas: Canvas) {
    if (progressType == PROGRESS_TYPE_LINE) {
        val isHorizontal = when(progressOrigin) {
            PROGRESS_ORIGIN_LEFT, PROGRESS_ORIGIN_RIGHT -> true
            else -> false
        }
        if (isHorizontal) {
            canvas.drawLine(
                progressBgRect.left,
                measuredHeight / 2f,
                progressBgRect.right,
                measuredHeight / 2f,
                progressBgPaint)
        } else {
            canvas.drawLine(measuredWidth / 2f,
                progressBgRect.top,
                                    measuredWidth / 2f,
                progressBgRect.bottom, progressBgPaint)
        }
        if (percentRate > 0) {
            when (progressOrigin) {
                PROGRESS_ORIGIN_LEFT -> {
                    canvas.drawLine(
                        progressBgRect.left,
                        measuredHeight / 2f,
                        (progressBgRect.right) * percentRate,
                        measuredHeight / 2f,
                        progressHoverPaint
                    )
                }
                PROGRESS_ORIGIN_TOP -> {
                    canvas.drawLine(measuredWidth / 2f,
                        progressBgRect.top,
                        measuredWidth / 2f,
                        (progressBgRect.bottom) * percentRate,
                        progressHoverPaint)
                }
                PROGRESS_ORIGIN_RIGHT -> {
                    canvas.drawLine(
                        progressWidth / 2 + (progressBgRect.right) * (1 - percentRate),
                        measuredHeight / 2f,
                        progressBgRect.right,
                        measuredHeight / 2f,
                        progressHoverPaint
                    )
                }
                PROGRESS_ORIGIN_BOTTOM -> {
                    canvas.drawLine(measuredWidth / 2f,
                        progressWidth / 2 + (progressBgRect.bottom) * (1 - percentRate),
                    measuredWidth / 2f,
                        progressBgRect.bottom,
                    progressHoverPaint)
                }
            }
        }
    } else if (progressType == PROGRESS_TYPE_SEMICIRCLE) {
        if (progressOrigin == PROGRESS_ORIGIN_LEFT) {
            // PI ~ 2PI
            canvas.drawArc(progressBgRect, 180f, 180f, false, progressBgPaint)
            canvas.drawArc(
                progressBgRect,
                180f,
                angle.toFloat(),
                false,
                progressHoverPaint
            )
        } else if (progressOrigin == PROGRESS_ORIGIN_TOP) {
            canvas.translate(-progressBgRect.width() / 2, 0f)
            // 3/2PI ~ 2PI, 0 ~ PI/2
            canvas.drawArc(progressBgRect, 270f, 180f, false, progressBgPaint)
            canvas.drawArc(
                progressBgRect,
                270f,
                angle.toFloat(),
                false,
                progressHoverPaint
            )
        } else if (progressOrigin == PROGRESS_ORIGIN_RIGHT) {
            canvas.translate(0f, -progressBgRect.height() / 2)
            // 2PI ~ PI
            canvas.drawArc(progressBgRect, 0f, 180f, false, progressBgPaint)
            canvas.drawArc(
                progressBgRect,
                0f,
                angle.toFloat(),
                false,
                progressHoverPaint
            )
        } else if (progressOrigin == PROGRESS_ORIGIN_BOTTOM) {
            // PI/2 ~ 3/2PI
            canvas.drawArc(progressBgRect, 90f, 180f, false, progressBgPaint)
            canvas.drawArc(
                progressBgRect,
                90f,
                angle.toFloat(),
                false,
                progressHoverPaint
            )
        }
    } else if (progressType == PROGRESS_TYPE_SEMICIRCLE_REVERSE) {
        if (progressOrigin == PROGRESS_ORIGIN_LEFT) {
            canvas.translate(0f, -progressBgRect.height() / 2)
            // PI ~ 2PI
            canvas.drawArc(progressBgRect, 180f, -180f, false, progressBgPaint)
            canvas.drawArc(
                progressBgRect,
                180f,
                -angle.toFloat(),
                false,
                progressHoverPaint
            )
        } else if (progressOrigin == PROGRESS_ORIGIN_TOP) {
            // 3/2PI ~ PI/2
            canvas.drawArc(progressBgRect, 270f, -180f, false, progressBgPaint)
            canvas.drawArc(
                progressBgRect,
                270f,
                -angle.toFloat(),
                false,
                progressHoverPaint
            )
        } else if (progressOrigin == PROGRESS_ORIGIN_RIGHT) {
            // 2PI ~ PI
            canvas.drawArc(progressBgRect, 0f, -180f, false, progressBgPaint)
            canvas.drawArc(
                progressBgRect,
                0f,
                -angle.toFloat(),
                false,
                progressHoverPaint
            )
        } else if (progressOrigin == PROGRESS_ORIGIN_BOTTOM) {
            canvas.translate(-progressBgRect.width() / 2, 0f)
            // PI/2 ~ 2PI, 2PI ~ 3/2PI
            canvas.drawArc(progressBgRect, 90f, -180f, false, progressBgPaint)
            canvas.drawArc(
                progressBgRect,
                90f,
                -angle.toFloat(),
                false,
                progressHoverPaint
            )
        }
    } else if (progressType == PROGRESS_TYPE_CIRCLE) {
        val deltaAngle = if (progressOrigin == PROGRESS_ORIGIN_TOP) {
            90f
        } else if (progressOrigin == PROGRESS_ORIGIN_RIGHT) {
            180f
        } else if (progressOrigin == PROGRESS_ORIGIN_BOTTOM) {
            270f
        } else {
            0f
        }
        canvas.drawArc(progressBgRect, 0f, 360f, false, progressBgPaint)
        canvas.drawArc(
            progressBgRect,
            180f + deltaAngle,
            angle.toFloat(),
            false,
            progressHoverPaint
        )
    } else if (progressType == PROGRESS_TYPE_CIRCLE_REVERSE) {
        val deltaAngle = if (progressOrigin == PROGRESS_ORIGIN_TOP) {
            90f
        } else if (progressOrigin == PROGRESS_ORIGIN_RIGHT) {
            180f
        } else if (progressOrigin == PROGRESS_ORIGIN_BOTTOM) {
            270f
        } else {
            0f
        }
        canvas.drawArc(progressBgRect, 0f, 360f, false, progressBgPaint)
        canvas.drawArc(
            progressBgRect,
            180f + deltaAngle,
            -angle.toFloat(),
            false,
            progressHoverPaint
        )
    }
}

绘图除了需要Android的基础绘图常识外,还需要必定的数学计算的功底,比方根本的几何图形的点的计算你要清楚。怎么让绘制的视点变化起来呢?这个问题问的好。这个就牵扯出咱们动画的一个要害类,TypeEvaluator,这个接口能够让咱们只需要指定边界值,就能够根据动画履行的时长,来动态计算出当前的突变值。

private inner class AnimationEvaluator : TypeEvaluator<Float> {
    override fun evaluate(fraction: Float, startValue: Float, endValue: Float): Float {
        return if (endValue > startValue) {
            startValue + fraction * (endValue - startValue)
        } else {
            startValue - fraction * (startValue - endValue)
        }
    }
}

百分比突变的固定写法,是不是应该记个笔记,便利今后CP?那么现在咱们条件都成熟了,只需要将初始视点的百分比改动一下,咱们写一个改动视点百分比的办法。

fun setPercentRate(rate: Float) {
    if (animator == null) {
        animator = ValueAnimator.ofObject(
            AnimationEvaluator(),
            percentRate,
            rate
        )
    }
    animator?.addUpdateListener { animation: ValueAnimator ->
        val value = animation.animatedValue as Float
        angle =
            if (progressType == PROGRESS_TYPE_CIRCLE || progressType == PROGRESS_TYPE_CIRCLE_REVERSE) {
                (value * 360).toInt()
            } else if (progressType == PROGRESS_TYPE_SEMICIRCLE || progressType == PROGRESS_TYPE_SEMICIRCLE_REVERSE) {
                (value * 180).toInt()
            } else {
                0   // 线不需要求视点
            }
        percentRate = value
        invalidate()
    }
    animator?.interpolator = LinearInterpolator()
    animator?.setDuration(animationTime.toLong())?.start()
    animator?.addListener(object : Animator.AnimatorListener {
        override fun onAnimationStart(animation: Animator) {}
        override fun onAnimationEnd(animation: Animator) {
            percentRate = rate
            listener?.onComplete()
        }
        override fun onAnimationCancel(animation: Animator) {}
        override fun onAnimationRepeat(animation: Animator) {}
    })
}

这儿牵扯到了Animator。有start就必定不要忘了异常中止的状况,咱们能够写一个reset的办法来中止动画履行,恢复到初始状态。

fun reset() {
    percentRate = 0f
    animator?.cancel()
}

假如你不reset,想连续履行动画,则两次调用的时刻距离必定要大于动画时长,不然就应该先撤销动画。

涉及到的Android绘图常识点

咱们归纳一下完成这个自界说View需要具有的常识点。

  1. 根本图形的绘制,这儿主要是扇形
  2. 丈量和画板的平移改换
  3. 自界说特点的界说和解析
  4. Animator和动画估值器TypeEvaluator的使用

思路和创意来自于体系化的基础常识

这个控件其实并不难,主要便是动态装备一些参数,然后在计算上略微复杂一些,需要一些数学的功底。那么你为什么没有思路呢?你没有思路最或许的原因主要有以下几个或许。

  1. 自界说View的基础绘图API不了解
  2. 动画估值器使用不了解
  3. 对自界说View的根本流程不了解
  4. 看的自界说View的源码不够多
  5. 自界说View基础常识没有体系学习,导致是一些零零碎碎的常识片段
  6. 数学功底不扎实

我觉得往往不是你不会,这些基础常识点你或许都看到过很屡次,但是一到自己写就没有思路了。思路和创意来自于大量源码的阅览和大量的实践。大前提便是你得先把自界说View的这些常识点体系学习一下,先确保都见过,然后才是将它们融会贯通,用的时分信手拈来。