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)
}

欢迎大家关注QuickAndroid开发库,此库持续更新中

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值