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

繪制的圖形
整體上繪制圖形分為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 ~