1. 程式人生 > >Android 自定義CheckAnimView,支付寶支付成功打勾對號動畫,kotlin編寫

Android 自定義CheckAnimView,支付寶支付成功打勾對號動畫,kotlin編寫

CheckAnimView是什麼東西呢,顧名思義就是選擇器,帶動畫效果的View,此View全由程式碼生成圖形。

使用場景:1、可以當作酷炫的選擇器。2、也可以用於展示結果,比如:支付結果,操作成功等

接下來看一下效果:

顯示效果

 控制元件由四種圖形組合成動畫:邊框(空心圓),背景(實心圓),打勾的線條,星星的線條。並且四種圖形可以獨立存在,根據需求新增,只需要在xml或者程式碼中設定即可,非常方便。

圖中的虛線支援橫向與縱向顯示,將在後面的部落格寫到。

如何使用呢,請看程式碼示例:

第一種效果

<org.quick.component.widget.CheckAnimView
        android:id="@+id/checkAniView0"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="?attr/selectableItemBackgroundBorderless"
        app:durationBg="400"
        app:durationTick="400"
        app:focusColorBg="@color/colorPrimary"
        app:focusColorStar="@color/colorWhite"
        app:focusColorTick="@color/colorWhite"
        app:focusDrawType="drawTick|drawStar|drawBg"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@+id/checkAniView1"
        app:layout_constraintTop_toTopOf="parent"
        app:normalDrawType="drawCir|drawTick" />
<declare-styleable name="CheckAnimView">
        <attr name="checked" format="boolean" />

        <attr name="sizeCir" format="dimension" />
        <attr name="sizeTick" format="dimension" />
        <attr name="sizeStar" format="dimension" />
        <attr name="durationBg" format="integer" />
        <attr name="durationCir" format="integer" />
        <attr name="durationTick" format="integer" />
        <attr name="durationStar" format="integer" />

        <attr name="focusColorCir" format="color" />
        <attr name="focusColorTick" format="color" />
        <attr name="focusColorBg" format="color" />
        <attr name="focusColorStar" format="color" />
        <attr name="focusDrawType">
            <flag name="drawBg" value="0x01" />
            <flag name="drawCir" value="0x02" />
            <flag name="drawTick" value="0x04" />
            <flag name="drawStar" value="0x08" />
        </attr>

        <attr name="normalColorCir" format="color" />
        <attr name="normalColorTick" format="color" />
        <attr name="normalColorBg" format="color" />
        <attr name="normalDrawType">
            <flag name="drawBg" value="0x01" />
            <flag name="drawCir" value="0x02" />
            <flag name="drawTick" value="0x04" />
        </attr>
    </declare-styleable>

重點屬性說明:

bg:背景     star:閃爍的星星     tick:打勾的線條     cir:圓圈邊框

focusDrawType:該屬性用於選中時,取得焦點狀態下需要顯示繪製的內容,提供三種選擇:drawCir(畫邊框)drawTick(打勾 )drawBg(畫背景)drawStar(畫星星)

normalDrawType:除了沒有星星以外,其餘同上

duration:使用者可以設定不同圖形的動畫時間

check:使用者可以設定預設的選中狀態,選中時將執行動畫,未選中時沒有動畫的。

使用很簡單,接下來看下實現思路吧:

繪製順序很簡單,就是先畫誰而已。

核心知識點就是利用屬性動畫,得到動畫的進度,然後畫出來,比如繪製長度為10的path,動畫執行進度為0~1,我們使用線段總長度去乘以進度就得到一個結果,線段長慢慢在增大,從1到10。如此就能畫出慢慢延長的線段了。只需要動態設定PathEffect

ObjectAnimator.ofFloat(this, "phaseTick", 0.0f, 1.0f)
private fun setPhaseCir(phase: Float) {
        paintCir.pathEffect = DashPathEffect(floatArrayOf(lengthCir, lengthCir), lengthCir - phase * lengthCir)
        postInvalidate()
    }

每次設定後就進行了一次繪製,如此才能畫出動態的線段。

這些知識在網上有許多相關教程的,這裡就不贅述了。

原始碼使用kotlin編寫:

/**
 * 選中動畫
 * @author chris Zou
 * @date 2018-09-07
 * @from
 */
class CheckAnimView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) {

    private var pathCir: Path = Path()
    private var pathTick: Path = Path()
    private var paintCir: Paint = Paint()
    private var paintTick: Paint = Paint()
    private var paintStar: Paint = Paint()
    private var paintBg: Paint = Paint()
    private var lengthCir: Float = 0.toFloat()
    private var lengthTick: Float = 0.toFloat()
    private var backgroundScale = 0f/*背景實心圓進度*/
    private var isDrawCirTemp = true
    private var isDrawTickTemp = true
    private var isDrawStarTemp = true
    private var isDrawBgTemp = true
    private var isDefaultSizeStar = false
    private var onCheckedChangeListener: ((isCheck: Boolean) -> Unit)? = null
    private val animatorStar: ValueAnimator
    private val starList = mutableListOf<Star>()
    private lateinit var sizeF: RectF/*繪製範圍*/

    val animatorTick: ValueAnimator
    val animatorCir: ValueAnimator
    val animatorBg: ValueAnimator

    var focusDrawType = 0
    var normalDrawType = 0
    var sizeCir = 10f
    var sizeTick = sizeCir
    var sizeStar = -1f

    var durationBg: Long = 300
    var durationCir: Long = 300
    var durationTick: Long = 300
    var durationStar: Long = 1000

    var focusColorCir = Color.GRAY
    var focusColorTick = focusColorCir
    var focusColorBg = Color.TRANSPARENT
    var focusColorStar = Color.TRANSPARENT

    var normalColorCir = Color.GRAY
    var normalColorTick = normalColorCir
    var normalColorBg = Color.TRANSPARENT

    private var isCheck: Boolean = false


    enum class TYPE(var value: Int) {
        FOCUS_BG(0x01), FOCUS_CIR(0x02), FOCUS_TICK(0x04), FOCUS_STAR(0x08),
        NORMAL_BG(0x01), NORMAL_CIR(0x02), NORMAL_TICK(0x04)
    }

    init {
        if (attrs != null) {
            val ta = context.obtainStyledAttributes(attrs, R.styleable.CheckAnimView)
            isCheck = ta.getBoolean(R.styleable.CheckAnimView_checked, false)
            sizeCir = ta.getDimension(R.styleable.CheckAnimView_sizeCir, 10f)
            sizeTick = ta.getDimension(R.styleable.CheckAnimView_sizeTick, 10f)
            sizeStar = ta.getDimension(R.styleable.CheckAnimView_sizeStar, -1f)

            durationBg = ta.getInt(R.styleable.CheckAnimView_durationBg, 300).toLong()
            durationCir = ta.getInt(R.styleable.CheckAnimView_durationCir, 300).toLong()
            durationTick = ta.getInt(R.styleable.CheckAnimView_durationTick, 400).toLong()
            durationStar = ta.getInt(R.styleable.CheckAnimView_durationStar, 1000).toLong()

            focusColorCir = ta.getColor(R.styleable.CheckAnimView_focusColorCir, Color.GRAY)
            focusColorTick = ta.getColor(R.styleable.CheckAnimView_focusColorTick, focusColorCir)
            focusColorBg = ta.getColor(R.styleable.CheckAnimView_focusColorBg, Color.TRANSPARENT)
            focusColorStar = ta.getColor(R.styleable.CheckAnimView_focusColorStar, focusColorTick)
            focusDrawType = ta.getInt(R.styleable.CheckAnimView_focusDrawType, TYPE.FOCUS_BG.value + TYPE.FOCUS_CIR.value + TYPE.FOCUS_TICK.value + TYPE.FOCUS_STAR.value)

            normalColorCir = ta.getColor(R.styleable.CheckAnimView_normalColorCir, Color.GRAY)
            normalColorTick = ta.getColor(R.styleable.CheckAnimView_normalColorTick, normalColorCir)
            normalColorBg = ta.getColor(R.styleable.CheckAnimView_normalColorBg, Color.TRANSPARENT)
            normalDrawType = ta.getInt(R.styleable.CheckAnimView_normalDrawType, TYPE.NORMAL_BG.value + TYPE.NORMAL_CIR.value + TYPE.NORMAL_TICK.value)
            ta.recycle()
        }
        isDefaultSizeStar = sizeStar == -1f
        paintCir.color = focusColorCir
        paintCir.strokeWidth = sizeCir
        paintCir.isAntiAlias = true
        paintCir.style = Paint.Style.STROKE

        paintTick.color = focusColorTick
        paintTick.strokeWidth = sizeTick
        paintTick.isAntiAlias = true
        paintTick.style = Paint.Style.STROKE

        paintBg.color = focusColorBg
        paintBg.isAntiAlias = true
        paintBg.style = Paint.Style.FILL_AND_STROKE

        paintStar.color = focusColorStar
        paintStar.isAntiAlias = true
        paintStar.style = Paint.Style.FILL_AND_STROKE

        /*星星*/
        animatorStar = ValueAnimator.ofFloat(0f, 1f, 0f)
        animatorStar.repeatCount = Animation.INFINITE
        animatorStar.duration = durationStar
        animatorStar.interpolator = LinearInterpolator()
        animatorStar.addUpdateListener {
            var flag = false
            starList.forEach { star ->
                if (flag) {
                    star.size = it.animatedValue.toString().toFloat() * sizeStar
                    star.alpha = (it.animatedValue.toString().toFloat() * 255).toInt()
                } else {
                    star.size = sizeStar - it.animatedValue.toString().toFloat() * sizeStar
                    star.alpha = (255 - it.animatedValue.toString().toFloat() * 255).toInt()
                }
                flag = !flag
                postInvalidate()
            }
        }
        /*打勾*/
        animatorTick = ObjectAnimator.ofFloat(this, "phaseTick", 0.0f, 1.0f)
        animatorTick.duration = durationTick
        animatorTick.addListener(object : Animator.AnimatorListener {
            override fun onAnimationRepeat(animation: Animator?) = Unit

            override fun onAnimationEnd(animation: Animator?) {
                if (isCheck && focusDrawType and TYPE.FOCUS_STAR.value == TYPE.FOCUS_STAR.value) {
                    isDrawStarTemp = true
                    animatorStar.start()
                }
            }

            override fun onAnimationCancel(animation: Animator?) = Unit

            override fun onAnimationStart(animation: Animator?) {

            }
        })
        /*畫圈*/
        animatorCir = ObjectAnimator.ofFloat(this, "phaseCir", 0.0f, 1.0f)
        animatorCir.duration = durationCir
        animatorCir.addListener(object : Animator.AnimatorListener {
            override fun onAnimationRepeat(animation: Animator?) = Unit

            override fun onAnimationEnd(animation: Animator?) {
                when {
                    focusDrawType and TYPE.FOCUS_TICK.value == TYPE.FOCUS_TICK.value -> {
                        isDrawTickTemp = true
                        animatorTick.start()
                    }
                    isCheck && focusDrawType and TYPE.FOCUS_STAR.value == TYPE.FOCUS_STAR.value -> {
                        isDrawStarTemp = true
                        animatorStar.start()
                    }
                }
            }

            override fun onAnimationCancel(animation: Animator?) = Unit

            override fun onAnimationStart(animation: Animator?) {
            }
        })
        /*背景*/
        animatorBg = ObjectAnimator.ofFloat(this, "backGroundScale", 0.0f, 1.0f)
        animatorBg.duration = durationBg
        animatorBg.addListener(object : Animator.AnimatorListener {
            override fun onAnimationRepeat(animation: Animator?) = Unit

            override fun onAnimationEnd(animation: Animator?) {
                when {
                    focusDrawType and TYPE.FOCUS_CIR.value == TYPE.FOCUS_CIR.value -> {
                        isDrawCirTemp = true
                        animatorCir.start()
                    }

                    focusDrawType and TYPE.FOCUS_TICK.value == TYPE.FOCUS_TICK.value -> {
                        isDrawTickTemp = true
                        animatorTick.start()
                    }
                    isCheck && focusDrawType and TYPE.FOCUS_STAR.value == TYPE.FOCUS_STAR.value -> {
                        isDrawStarTemp = true
                        animatorStar.start()
                    }
                }
            }

            override fun onAnimationCancel(animation: Animator?) = Unit

            override fun onAnimationStart(animation: Animator?) {
            }
        })
    }


    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        measureLocation()
    }

    private fun measureLocation() {
        if (isAnimIng()) animCancel()

        configStyle()

        val padding = sizeCir / 2
        val temp = Math.abs((height - width) / 2.0f)
        sizeF = RectF(if (height > width) 0f + padding else temp + padding, if (height > width) temp + padding else 0f + padding, if (height > width) width.toFloat() - padding else width - temp - padding, if (height > width) height - temp - padding else height.toFloat() - padding)

        val widthDistance = sizeF.right - sizeF.left
        val heightDistance = (sizeF.bottom - sizeF.top)

        pathCir = Path()
        pathCir.addOval(sizeF, Path.Direction.CCW)
//        pathCir.moveTo(rectF.centerX(), rectF.top)
//        pathCir.quadTo(rectF.left, rectF.top, rectF.left, rectF.centerY())
//        pathCir.quadTo(rectF.left, rectF.bottom, rectF.centerX(), rectF.bottom)
//        pathCir.quadTo(rectF.right, rectF.bottom, rectF.right, rectF.centerY())
//        pathCir.quadTo(rectF.right, rectF.top, rectF.centerX(), rectF.top)
        lengthCir = PathMeasure(pathCir, false).length

        pathTick = Path()
        pathTick.moveTo(sizeF.left + widthDistance / 4f, sizeF.centerY())
        pathTick.lineTo(sizeF.centerX(), sizeF.bottom - heightDistance / 3f)
        pathTick.lineTo(sizeF.centerX() + widthDistance / 4f, sizeF.top + heightDistance / 4f)
        lengthTick = PathMeasure(pathTick, false).length

        if (isDefaultSizeStar) sizeStar = if (sizeF.centerX() * 0.125f > 30) 30f else sizeF.centerX() * 0.1f

        starList.clear()
        starList.add(Star(sizeF.centerX() + widthDistance / 4f + sizeStar * 0f, sizeF.top + heightDistance / 4f + sizeStar * 4))
        starList.add(Star(sizeF.centerX() + widthDistance / 4f - sizeStar * 2f, sizeF.top + heightDistance / 4f - sizeStar))
        starList.add(Star(sizeF.left + widthDistance / 4f + sizeStar * 1.125f, sizeF.centerY() - sizeStar * 2))
        starList.add(Star(sizeF.centerX() - sizeStar * 1f, sizeF.bottom - heightDistance / 3f + sizeStar * 1.5f))

        if (isCheck) {
            isDrawCirTemp = false
            isDrawTickTemp = false
            isDrawStarTemp = false
            isDrawBgTemp = false
            when {
                focusDrawType and TYPE.FOCUS_BG.value == TYPE.FOCUS_BG.value -> {
                    isDrawBgTemp = true
                    animatorBg.start()
                }
                focusDrawType and TYPE.FOCUS_CIR.value == TYPE.FOCUS_CIR.value -> {
                    isDrawCirTemp = true
                    animatorCir.start()
                }
                focusDrawType and TYPE.FOCUS_TICK.value == TYPE.FOCUS_TICK.value -> {
                    isDrawTickTemp = true
                    animatorTick.start()
                }
                isCheck && focusDrawType and TYPE.FOCUS_STAR.value == TYPE.FOCUS_STAR.value -> {
                    isDrawStarTemp = true
                    animatorStar.start()
                }
            }
        } else postInvalidate()
    }

    private fun configStyle() {
        if (isCheck) {
            paintTick.color = focusColorTick
            paintCir.color = focusColorCir
            paintBg.color = focusColorBg
        } else {
            paintTick.color = normalColorTick
            paintCir.color = normalColorCir
            paintBg.color = normalColorBg
            animatorStar.cancel()
        }
    }

    private fun setPhaseCir(phase: Float) {
        paintCir.pathEffect = DashPathEffect(floatArrayOf(lengthCir, lengthCir), lengthCir - phase * lengthCir)
        postInvalidate()
    }

    private fun setPhaseTick(phase: Float) {
        paintTick.pathEffect = DashPathEffect(floatArrayOf(lengthTick, lengthTick), lengthTick - phase * lengthTick)
        postInvalidate()
    }

    private fun setBackGroundScale(progress: Float) {
        backgroundScale = progress
        postInvalidate()
    }

    fun setOnCheckedChangeListener(onCheckedChangeListener: ((isCheck: Boolean) -> Unit)) {
        this.onCheckedChangeListener = onCheckedChangeListener
        setOnClickListener {
            isCheck = !isCheck
            animCancel()
            onCheckedChangeListener.invoke(isCheck)
            measureLocation()
        }
    }

    fun animCancel() {
        animatorStar.cancel()
        animatorBg.cancel()
        animatorCir.cancel()
        animatorTick.cancel()

        paintTick.pathEffect = DashPathEffect(floatArrayOf(lengthTick, lengthTick), 0f)
        paintCir.pathEffect = DashPathEffect(floatArrayOf(lengthCir, lengthCir), 0f)
    }

    fun isCheck() = this.isCheck

    fun setCheck(isCheck: Boolean) {
        animCancel()
        this.isCheck = isCheck
        measureLocation()
    }

    @SuppressLint("DrawAllocation")
    public override fun onDraw(c: Canvas) {
        super.onDraw(c)
        if (isCheck) {
            if (focusDrawType and TYPE.FOCUS_BG.value == TYPE.FOCUS_BG.value && isDrawBgTemp && focusColorBg != Color.TRANSPARENT) c.drawCircle(sizeF.centerX(), sizeF.centerY(), (sizeF.right - sizeF.left) / 2f * backgroundScale, paintBg)
            if (focusDrawType and TYPE.FOCUS_CIR.value == TYPE.FOCUS_CIR.value && isDrawCirTemp && focusColorCir != focusColorBg) c.drawPath(pathCir, paintCir)
            if (focusDrawType and TYPE.FOCUS_TICK.value == TYPE.FOCUS_TICK.value && isDrawTickTemp && focusColorTick != focusColorBg) c.drawPath(pathTick, paintTick)
        } else {
            if (normalDrawType and TYPE.NORMAL_BG.value == TYPE.NORMAL_BG.value && normalColorBg != Color.TRANSPARENT) c.drawCircle(sizeF.centerX(), sizeF.centerY(), (sizeF.right - sizeF.left) / 2f * backgroundScale, paintBg)
            if (normalDrawType and TYPE.NORMAL_CIR.value == TYPE.NORMAL_CIR.value && normalColorCir != Color.TRANSPARENT) c.drawPath(pathCir, paintCir)
            if (normalDrawType and TYPE.NORMAL_TICK.value == TYPE.NORMAL_TICK.value && normalColorTick != Color.TRANSPARENT) c.drawPath(pathTick, paintTick)
        }
        pathCir.close()
        if (focusDrawType and TYPE.FOCUS_STAR.value == TYPE.FOCUS_STAR.value && focusColorStar != Color.TRANSPARENT && focusColorStar != focusColorBg && isDrawStarTemp) starList.forEach { drawStar(c, it) }
    }

    private fun drawStar(c: Canvas, star: Star) {
        val path = Path()
        val starX = star.centerX
        val starY = star.centerY - star.size
        path.moveTo(starX, starY)
        path.quadTo(starX - star.size * 0.25f, starY + star.size * 0.75f, starX - star.size, starY + star.size)
        path.quadTo(starX - star.size * 0.25f, starY + star.size * 1.25f, starX, starY + star.size * 2f)
        path.quadTo(starX + star.size * 0.25f, starY + star.size * 1.25f, starX + star.size, starY + star.size)
        path.quadTo(starX + star.size * 0.25f, starY + star.size * 0.75f, starX, starY)
        paintStar.alpha = star.alpha
        c.drawPath(path, paintStar)
        path.reset()
        path.close()
    }

    fun isAnimIng() = animatorBg.isStarted || animatorCir.isStarted || animatorTick.isStarted

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        if (isCheck) measureLocation()
    }

    override fun onDetachedFromWindow() {
        animatorStar.cancel()
        super.onDetachedFromWindow()
    }

    class Star(var centerX: Float, var centerY: Float, var size: Float = 0f, var alpha: Int = 0)
}