Android点赞控件--仿掘金点赞七成效果
0.
前些日子偶然看到掘金推荐里的点赞效果,感觉有些酷炫,然后在一个无所事事的早上,我决定实现一个类似的功能,但是只有七成的效果,效果图如下。
image1.
这个效果我把它们分成了几个阶段:
(1)默认阶段:就是一个竖起来的大拇指
(2)收缩阶段:大拇指逐渐缩小直到消失
(3)放大阶段:圆圈由小到大
(4)圆环阶段:圆圈有中心破裂,露出大拇指
(5)卫星阶段:出现卫星,向远处逐渐偏离同时逐渐消失
2.
话不多说,上代码吧
package com.skateboard.favouriteview
import android.animation.Animator
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.*
import android.support.v4.content.ContextCompat
import android.util.AttributeSet
import android.view.View
class FavouriteView(context: Context, attrs: AttributeSet?, defStyle: Int) : View(context, attrs, defStyle)
{
private lateinit var paint: Paint
private lateinit var statellitePaint: Paint
private lateinit var path: Path
private var spaceBetweenHandAndShoulder = 5
private var state = STATE_NORMAL
private lateinit var valueAnimator: ValueAnimator
private var size = 0
private var centerX = 0f
private var centerY = 0f
private var strokeWithScaleFraction = 1f
private var statelliteOffsetFraction = 0f
private var selectedColor = Color.BLACK
private var normalColor = Color.BLACK
companion object
{
val STATE_SELECTED = 1
val STATE_CIRCLE = 2
val STATE_RING = 3
val STATE_STATELLITE = 4
val STATE_NORMAL = 0
}
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context) : this(context, null, 0)
init
{
if (attrs != null)
{
initParse(attrs)
}
initPaint()
initStatellitePaint()
initClickEvent()
}
private fun initParse(attrs: AttributeSet)
{
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.FavouriteView)
selectedColor = typedArray.getColor(R.styleable.FavouriteView_selected_color, Color.BLACK)
normalColor = typedArray.getColor(R.styleable.FavouriteView_normal_color, Color.BLACK)
typedArray.recycle()
}
private fun initPaint()
{
paint = Paint(Paint.ANTI_ALIAS_FLAG)
paint.color = normalColor
paint.style = Paint.Style.STROKE
paint.strokeWidth = 4f
path = Path()
val cornerPathEffect = CornerPathEffect(10f)
paint.pathEffect = cornerPathEffect
}
private fun initStatellitePaint()
{
statellitePaint = Paint(Paint.ANTI_ALIAS_FLAG)
statellitePaint.color = selectedColor
statellitePaint.style=Paint.Style.FILL
}
private fun initAnimator()
{
valueAnimator = ValueAnimator.ofFloat(0f, 400f)
valueAnimator.duration = 500
valueAnimator.addUpdateListener(updateListener)
valueAnimator.addListener(object : Animator.AnimatorListener
{
override fun onAnimationStart(animation: Animator?)
{
}
override fun onAnimationRepeat(animation: Animator?)
{
}
override fun onAnimationEnd(animation: Animator?)
{
scaleX = Math.max(1f, scaleX)
scaleY = Math.max(1f, scaleY)
setState(STATE_SELECTED)
}
override fun onAnimationCancel(animation: Animator?)
{
}
})
}
private val updateListener = ValueAnimator.AnimatorUpdateListener {
val time = Math.round(it.animatedValue as Float)
when
{
time <= 100.0f ->
{
scaleX = Math.max(0f, 1f - time / 100f)
scaleY = Math.max(0f, 1f - time / 100f)
}
time in 101..200 ->
{
scaleX = Math.min(1f, (time - 100) / 100f)
scaleY = Math.min(1f, (time - 100) / 100f)
setState(STATE_CIRCLE)
}
time in 201..300 ->
{
scaleX = 1f
scaleY = 1f
strokeWithScaleFraction = ((time - 200) / 100f)
setState(STATE_RING)
postInvalidate()
}
else ->
{
statelliteOffsetFraction = ((time - 300) / 100f)
setState(STATE_STATELLITE)
postInvalidate()
}
}
}
private fun setState(newState: Int)
{
if (state != newState)
{
state = newState
postInvalidate()
}
}
override fun onAttachedToWindow()
{
super.onAttachedToWindow()
initAnimator()
}
private fun initClickEvent()
{
setOnClickListener {
if (state == STATE_NORMAL)
{
startAnimate()
} else
{
setState(STATE_NORMAL)
}
}
}
private fun startAnimate()
{
if (valueAnimator.isRunning)
{
return
} else
{
valueAnimator.start()
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int)
{
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
size = Math.min(widthSize, heightSize)
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
override fun onDraw(canvas: Canvas?)
{
super.onDraw(canvas)
prepareToDraw()
if (canvas != null)
{
when (state)
{
STATE_NORMAL ->
{
resetPaintColor()
drawFinger(canvas)
}
STATE_SELECTED ->
{
resetPaintColor()
drawFinger(canvas)
}
STATE_CIRCLE ->
{
resetPaintColor()
drawCircle(canvas)
}
STATE_RING ->
{
resetPaintColor()
drawRing(canvas)
drawFinger(canvas)
}
STATE_STATELLITE ->
{
resetPaintColor()
drawStatellite(canvas)
}
}
}
}
private fun resetPaintColor()
{
when
{
state == STATE_NORMAL ->
{
paint.colorFilter = null
paint.color = normalColor
}
state != STATE_STATELLITE ->
{
paint.colorFilter = null
paint.color = selectedColor
}
else ->
{
paint.color = selectedColor
if (statelliteOffsetFraction <= 0.5f)
{
val parameter = 1f
statellitePaint.colorFilter = ColorMatrixColorFilter(floatArrayOf(1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 0f, parameter, 0f))
} else
{
val parameter = 1 - statelliteOffsetFraction
statellitePaint.colorFilter = ColorMatrixColorFilter(floatArrayOf(1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 0f, parameter, 0f))
}
}
}
}
private fun prepareToDraw()
{
path.reset()
centerX = (width / 2).toFloat()
centerY = (height / 2).toFloat()
}
private fun drawFinger(canvas: Canvas)
{
paint.style = Paint.Style.STROKE
paint.strokeWidth = 4f
val fingerWidth = (size / 2).toFloat()
val fingerHeight = (size / 2).toFloat()
val centerX = (width / 2).toFloat() + fingerWidth / 8
val centerY = (height / 2).toFloat() + fingerHeight / 8
path.addRect(centerX - fingerWidth / 2, centerY - fingerHeight / 4, centerX - fingerWidth / 3, centerY + fingerHeight / 4, Path.Direction.CW)
path.moveTo(centerX - fingerWidth / 3 + spaceBetweenHandAndShoulder, centerY - fingerHeight / 4)
path.rLineTo(fingerWidth / 8, 0f)
path.rLineTo(fingerWidth / 8, -fingerHeight / 2)
path.rLineTo(fingerWidth / 6, fingerHeight / 4)
path.rLineTo(-fingerWidth / 8, fingerHeight / 4)
path.rLineTo(fingerWidth / 2 - fingerWidth / 6 - spaceBetweenHandAndShoulder, 0f)
path.rLineTo(-fingerWidth / 8, fingerHeight / 2)
path.lineTo(centerX - fingerWidth / 3 + spaceBetweenHandAndShoulder, centerY + fingerHeight / 4)
path.close()
canvas.drawPath(path, paint)
}
private fun drawCircle(canvas: Canvas)
{
val radius = (size.toFloat()) / 3
paint.style = Paint.Style.FILL
canvas.drawCircle(centerX, centerY, radius, paint)
}
private fun drawStatellite(canvas: Canvas)
{
drawFinger(canvas)
drawSmallStatellites(canvas)
}
private fun drawRing(canvas: Canvas)
{
val radius = (size.toFloat()) / 3
paint.style = Paint.Style.STROKE
paint.strokeWidth = ((1 - strokeWithScaleFraction) * radius)
canvas.drawCircle(centerX, centerY, radius - paint.strokeWidth / 2, paint)
}
private fun drawSmallStatellites(canvas: Canvas)
{
val bigRadius = (size.toFloat()) / 3
val smallRadius = (centerY - bigRadius) / 3
val offset = size / 2 - bigRadius - 2 * smallRadius
canvas.drawCircle(centerX, centerY - bigRadius - offset * statelliteOffsetFraction, smallRadius, statellitePaint)
canvas.drawCircle(centerX + bigRadius + offset * statelliteOffsetFraction, centerY, smallRadius, statellitePaint)
canvas.drawCircle(centerX, centerY + bigRadius + offset * statelliteOffsetFraction, smallRadius, statellitePaint)
canvas.drawCircle(centerX - bigRadius - offset * statelliteOffsetFraction, centerY, smallRadius, statellitePaint)
}
}
这个控件的所有代码就在这里了,重点分一下几个关键点
3.
绘制大拇指
``
private fun drawFinger(canvas: Canvas)
{
paint.style = Paint.Style.STROKE
paint.strokeWidth = 4f
val fingerWidth = (size / 2).toFloat()
val fingerHeight = (size / 2).toFloat()
val centerX = (width / 2).toFloat() + fingerWidth / 8
val centerY = (height / 2).toFloat() + fingerHeight / 8
path.addRect(centerX - fingerWidth / 2, centerY - fingerHeight / 4, centerX - fingerWidth / 3, centerY + fingerHeight / 4, Path.Direction.CW)
path.moveTo(centerX - fingerWidth / 3 + spaceBetweenHandAndShoulder, centerY - fingerHeight / 4)
path.rLineTo(fingerWidth / 8, 0f)
path.rLineTo(fingerWidth / 8, -fingerHeight / 2)
path.rLineTo(fingerWidth / 6, fingerHeight / 4)
path.rLineTo(-fingerWidth / 8, fingerHeight / 4)
path.rLineTo(fingerWidth / 2 - fingerWidth / 6 - spaceBetweenHandAndShoulder, 0f)
path.rLineTo(-fingerWidth / 8, fingerHeight / 2)
path.lineTo(centerX - fingerWidth / 3 + spaceBetweenHandAndShoulder, centerY + fingerHeight / 4)
path.close()
canvas.drawPath(path, paint)
}
大拇指的绘制是通过path来实现的,size=Math.min(width,height)大拇指占整个size的1/2,另外还要注意的是,当然还要注意为path设置patheffect
private fun initPaint()
{
paint = Paint(Paint.ANTI_ALIAS_FLAG)
paint.color = normalColor
paint.style = Paint.Style.STROKE
paint.strokeWidth = 4f
path = Path()
val cornerPathEffect = CornerPathEffect(10f)
paint.pathEffect = cornerPathEffect
}
``
否则,你的大拇指将尖锐无比。
4.
动画开启后,根据时间来区分状态,这里我决定用valueanimator,众所周知,valueanimator是用来做动画的,但事实上他只是对传入的数值进行修改,但是并不会对view起到什么作用,真正起作用的是对valueanimator设置updatelistener,根据改变的数值修改view的属性才起到动画作用的。这正是我们需要的,所以看updatelistener吧
``
private val updateListener = ValueAnimator.AnimatorUpdateListener {
val time = Math.round(it.animatedValue as Float)
when
{
//收缩阶段
time <= 100.0f ->
{
scaleX = Math.max(0f, 1f - time / 100f)
scaleY = Math.max(0f, 1f - time / 100f)
}
//放大阶段
time in 101..200 ->
{
scaleX = Math.min(1f, (time - 100) / 100f)
scaleY = Math.min(1f, (time - 100) / 100f)
setState(STATE_CIRCLE)
}
//圆环阶段
time in 201..300 ->
{
scaleX = 1f
scaleY = 1f
strokeWithScaleFraction = ((time - 200) / 100f)
setState(STATE_RING)
postInvalidate()
}
//卫星阶段
else ->
{
statelliteOffsetFraction = ((time - 300) / 100f)
setState(STATE_STATELLITE)
postInvalidate()
}
}
}
``
5.
卫星的绘制有两点,一是一个距离的改变,二是颜色的逐渐alpha逐渐变0.
private fun drawSmallStatellites(canvas: Canvas)
{
val bigRadius = (size.toFloat()) / 3
val smallRadius = (centerY - bigRadius) / 3
val offset = size / 2 - bigRadius - 2 * smallRadius
canvas.drawCircle(centerX, centerY - bigRadius - offset * statelliteOffsetFraction, smallRadius, statellitePaint)
canvas.drawCircle(centerX + bigRadius + offset * statelliteOffsetFraction, centerY, smallRadius, statellitePaint)
canvas.drawCircle(centerX, centerY + bigRadius + offset * statelliteOffsetFraction, smallRadius, statellitePaint)
canvas.drawCircle(centerX - bigRadius - offset * statelliteOffsetFraction, centerY, smallRadius, statellitePaint)
}
距离的改变是根据statelliteOffsetFraction决定的,statelliteOffsetFraction变量这个是在updatelistener里进行不断改变的。
至于颜色的变化
paint.color = selectedColor
if (statelliteOffsetFraction <= 0.5f)
{
val parameter = 1f
statellitePaint.colorFilter = ColorMatrixColorFilter(floatArrayOf(1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 0f, parameter, 0f))
} else
{
val parameter = 1 - statelliteOffsetFraction
statellitePaint.colorFilter = ColorMatrixColorFilter(floatArrayOf(1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 0f, parameter, 0f))
}
这里使用了colormatrix。
6.
为什么说是七成效果,因为卫星的位置不一样,圆环阶段爆炸后大拇指有一个由大到正常的恢复过程,还有wrap_content以及padding等没有实现,为什么会这样?一个字,懒,懒得做。。。
附上github地址
结尾
终于写完了,关灯,睡觉。