Android自定義View(12) 《實(shí)現(xiàn)一個(gè)帶動(dòng)畫的單選框》

概述

今天無(wú)意發(fā)現(xiàn)MIUI的一個(gè)單選框,發(fā)現(xiàn)還挺好玩的,就抽空寫了一下,單選框具體是長(zhǎng)這個(gè)樣子的

效果

chek_view.gif

繪制的圖形

整體上繪制圖形分為3個(gè)部分

  • 1.帶陰影的未選中狀態(tài)的圓形背景
  • 2.帶陰影的選中狀態(tài)的選中狀態(tài)的圓形背景
  • 3.繪制中間的勾形路徑

用到的動(dòng)畫

首先我們考慮把動(dòng)畫分為2部分,第一部分為手指按下去的事件,此時(shí)開始進(jìn)行手指按下去的動(dòng)畫,當(dāng)松開手指時(shí)我們開始執(zhí)行松開手指的動(dòng)畫。
如果沒有通過(guò)本身點(diǎn)擊事件觸發(fā),我們則先播放按下去的動(dòng)畫,然后監(jiān)聽動(dòng)畫結(jié)束再播放松開手指的動(dòng)畫

先上代碼

package com.tx.txcustomview.view

import android.animation.Animator
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.View


/**
 * create by xu.tian
 * @date 2021/9/10
 */
class CheckView(context: Context?, attrs: AttributeSet?) : View(context, attrs){
    // 當(dāng)前選中狀態(tài)
    var checked = false
    // 創(chuàng)建畫筆對(duì)象
    var paint = Paint()
    // 圓心x坐標(biāo)
    var centerX = 0f
    // 圓心y坐標(biāo)
    var centerY = 0f
    // 圓半徑
    var radius = 0f
    // 實(shí)際繪制的真實(shí)圓半徑
    var drawRadius = 0f

    // 按下去執(zhí)行的動(dòng)畫
    lateinit var pressAnimator: ValueAnimator
    // 按下去動(dòng)畫執(zhí)行的時(shí)間
    var pressAnimDuration = 100L
    // 當(dāng)前的按下去的動(dòng)畫值0~100
    var pressCurrentValue = 0F
    // 按下去時(shí)的縮放值
    var pressScale = 0.8f
    
    // 松手執(zhí)行的動(dòng)畫
    lateinit var upAnimator : ValueAnimator
    // 松手動(dòng)畫執(zhí)行的時(shí)間
    var upAnimDuration = 300L
    // 當(dāng)前的松手的動(dòng)畫值0~100
    var upCurrentValue = 0F
    // 判斷是否是外部設(shè)置
    var isSet = false

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        setLayerType(LAYER_TYPE_SOFTWARE,paint)
        if (!checked){
            drawUnchecked(canvas)
        }else{
            drawChecked(canvas)
        }
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        this.centerX = (w/2).toFloat()
        this.centerY = (h/2).toFloat()
        this.radius = centerX*9/10
        this.drawRadius = radius
        initPressAnimator()
        initUpAnimator()
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        when(event?.action){
            MotionEvent.ACTION_DOWN -> actionPress()
            MotionEvent.ACTION_UP -> actionUp()
        }
        return true
    }

    private fun initPressAnimator(){
        pressAnimator = ValueAnimator.ofFloat(0f,100f)
        pressAnimator.duration = pressAnimDuration
        pressAnimator.addUpdateListener { valueAnimator ->
            pressCurrentValue = valueAnimator.animatedValue as Float
            drawRadius = radius*(1-(pressCurrentValue/100)*(1-pressScale))
            postInvalidate()
        }
        pressAnimator.addListener(object : Animator.AnimatorListener{
            override fun onAnimationStart(p0: Animator?) {

            }

            override fun onAnimationEnd(p0: Animator?) {
                Log.d("CheckView", "pressAnimator onAnimationEnd --->$pressCurrentValue")
                if (isSet){
                    upAnimator.start()
                }
            }

            override fun onAnimationCancel(p0: Animator?) {

            }

            override fun onAnimationRepeat(p0: Animator?) {

            }
        })

    }
    private fun initUpAnimator(){
        upAnimator = ValueAnimator.ofFloat(0f,100f)
        upAnimator.duration = upAnimDuration
        upAnimator.addUpdateListener { valueAnimator ->
            upCurrentValue = valueAnimator.animatedValue as Float
            drawRadius = radius*pressScale+(1-pressScale)*(upCurrentValue/100)*radius
            postInvalidate()
        }
        upAnimator.addListener(object : Animator.AnimatorListener{
            override fun onAnimationStart(p0: Animator?) {
            }

            override fun onAnimationEnd(p0: Animator?) {
                if (!checked){
                    pressCurrentValue = 0f
                    upCurrentValue = 0f
                }else{
                    pressCurrentValue = 100f
                    upCurrentValue = 100f
                }
                upAnimator.cancel()
                Log.d("CheckView", "upAnimator onAnimationEnd --->$upCurrentValue")
            }

            override fun onAnimationCancel(p0: Animator?) {

            }

            override fun onAnimationRepeat(p0: Animator?) {

            }
        })

    }

    private fun actionPress(){
        isSet = false
        pressAnimator.start()
    }

    private fun actionUp() {
        checked = !checked
        upAnimator.start()
    }

    private fun drawChecked(canvas: Canvas){
        // 繪制選中時(shí)的圓形背景
        paint.color = Color.BLUE
        paint.style = Paint.Style.FILL
        canvas.drawCircle(centerX,centerY,drawRadius,paint)
        // 繪制中間的勾
        var path = Path()
        path.moveTo(centerX-drawRadius/2,centerY-drawRadius/10);
        path.lineTo(centerX-drawRadius/15,centerY+drawRadius/3);
        path.lineTo(centerX+drawRadius/2,centerY-drawRadius/3);
        var dstPah = Path()
        var pathMeasure = PathMeasure()
        pathMeasure.setPath(path,false)
        pathMeasure.getSegment(0f,pathMeasure.length*(upCurrentValue/100),dstPah,true)
        paint.style = Paint.Style.STROKE
        paint.strokeWidth = drawRadius / 6
        paint.color = Color.WHITE
        paint.strokeCap = Paint.Cap.ROUND
        paint.strokeJoin = Paint.Join.ROUND
        canvas.drawPath(dstPah,paint)
    }

    private fun drawUnchecked(canvas: Canvas){
        // 繪制未選中狀態(tài)的背景
        paint.style = Paint.Style.FILL
        paint.color = Color.GRAY
        paint.setShadowLayer(5f,5f,5f,Color.parseColor("#4D000000"))
        paint.alpha = 150
        canvas.drawCircle(centerX,centerY,drawRadius,paint)
    }

    fun setStatusChecked(checked : Boolean){
        this.checked  = checked
        isSet = true
        pressAnimator.start()
    }
    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        pressAnimator.cancel()
        upAnimator.cancel()
    }
}

思路

我們利用兩個(gè)ValueAnimator來(lái)控制動(dòng)畫進(jìn)行中我們繪制的背景的半徑以及畫筆的粗細(xì)等,我們先確認(rèn)在繪制過(guò)程中哪些值需要變化

  • 繪制的背景的半徑
    因?yàn)檫@里我們使用的勾形路徑和畫筆寬度都是根據(jù)半徑?jīng)Q定的,所以我們只需要在動(dòng)畫變化的過(guò)程中確認(rèn)好半徑大小就沒問(wèn)題了。我們每次在按下去的時(shí)候相當(dāng)于縮放,然后在松開手指的時(shí)候切換狀態(tài)并播放動(dòng)畫

核心代碼

這里我們只看最重要的繪制部分以及使用動(dòng)畫值的部分
首先是繪制未選中狀態(tài)圓形的背景的代碼

private fun drawUnchecked(canvas: Canvas){
        // 繪制未選中狀態(tài)的背景
        paint.style = Paint.Style.FILL
        paint.color = Color.GRAY
        paint.setShadowLayer(5f,5f,5f,Color.parseColor("#4D000000"))
        paint.alpha = 150
        canvas.drawCircle(centerX,centerY,drawRadius,paint)
    }

這里的drawRadius是我們實(shí)際畫的圓的半徑
其次是選中狀態(tài)的圖形

 private fun drawChecked(canvas: Canvas){
        // 繪制選中時(shí)的圓形背景
        paint.color = Color.BLUE
        paint.style = Paint.Style.FILL
        canvas.drawCircle(centerX,centerY,drawRadius,paint)
        // 繪制中間的勾
        var path = Path()
        path.moveTo(centerX-drawRadius/2,centerY-drawRadius/10);
        path.lineTo(centerX-drawRadius/15,centerY+drawRadius/3);
        path.lineTo(centerX+drawRadius/2,centerY-drawRadius/3);
        var dstPah = Path()
        var pathMeasure = PathMeasure()
        pathMeasure.setPath(path,false)
        pathMeasure.getSegment(0f,pathMeasure.length*(upCurrentValue/100),dstPah,true)
        paint.style = Paint.Style.STROKE
        paint.strokeWidth = drawRadius / 6
        paint.color = Color.WHITE
        paint.strokeCap = Paint.Cap.ROUND
        paint.strokeJoin = Paint.Join.ROUND
        canvas.drawPath(dstPah,paint)
    }

這個(gè)勾的坐標(biāo)是真的難畫,和UI小姐姐看了iconfont的幾個(gè)圖標(biāo)才找來(lái)了靈感畫出了這個(gè)看起來(lái)還不錯(cuò)的~
其次是按下去的動(dòng)畫的部分

 private fun initPressAnimator(){
        pressAnimator = ValueAnimator.ofFloat(0f,100f)
        pressAnimator.duration = pressAnimDuration
        pressAnimator.addUpdateListener { valueAnimator ->
            pressCurrentValue = valueAnimator.animatedValue as Float
            drawRadius = radius*(1-(pressCurrentValue/100)*(1-pressScale))
            postInvalidate()
        }
        pressAnimator.addListener(object : Animator.AnimatorListener{
            override fun onAnimationStart(p0: Animator?) {

            }

            override fun onAnimationEnd(p0: Animator?) {
                Log.d("CheckView", "pressAnimator onAnimationEnd --->$pressCurrentValue")
                if (isSet){
                    upAnimator.start()
                }
            }

            override fun onAnimationCancel(p0: Animator?) {

            }

            override fun onAnimationRepeat(p0: Animator?) {

            }
        })
    }

注意這里的drawRaidus的計(jì)算公式

drawRadius = radius*(1-(pressCurrentValue/100)*(1-pressScale))

在動(dòng)畫值從0~100的過(guò)程中,drawRadius最終會(huì)變成我們所需要的縮放后的值
然后是松開手的動(dòng)畫

  private fun initUpAnimator(){
        upAnimator = ValueAnimator.ofFloat(0f,100f)
        upAnimator.duration = upAnimDuration
        upAnimator.addUpdateListener { valueAnimator ->
            upCurrentValue = valueAnimator.animatedValue as Float
            drawRadius = radius*pressScale+(1-pressScale)*(upCurrentValue/100)*radius
            postInvalidate()
        }
        upAnimator.addListener(object : Animator.AnimatorListener{
            override fun onAnimationStart(p0: Animator?) {
            }

            override fun onAnimationEnd(p0: Animator?) {
                if (!checked){
                    pressCurrentValue = 0f
                    upCurrentValue = 0f
                }else{
                    pressCurrentValue = 100f
                    upCurrentValue = 100f
                }
                upAnimator.cancel()
                Log.d("CheckView", "upAnimator onAnimationEnd --->$upCurrentValue")
            }

            override fun onAnimationCancel(p0: Animator?) {

            }

            override fun onAnimationRepeat(p0: Animator?) {

            }
        })
}

這里其實(shí)也只是計(jì)算了一下drawRadius的值

drawRadius = radius*pressScale+(1-pressScale)*(upCurrentValue/100)*radius

這個(gè)和按下去就是反過(guò)來(lái)的,從縮放后的值逐漸變到原來(lái)的大小,最后記得執(zhí)行完兩個(gè)動(dòng)畫后把狀態(tài)值重置~

總結(jié)

整體來(lái)說(shuō)是比較簡(jiǎn)單的一個(gè)控件,但是如果需要更細(xì)致的狀態(tài)切換那么還是需要再下點(diǎn)功夫的~
See you ~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容