Android 自定義CheckAnimView,支付寶支付成功打勾對號動畫,kotlin編寫
阿新 • • 發佈:2018-12-31
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)
}