抖音——时下最火的app之一,发布过程中有个按住拍的呼吸效果,效果如下所示:
上面两个按钮,都是采用属性动画进行控制的,但实现细节稍有不同,左上采用的是StateListAnimator,只需要考虑跟随手指动就可以了;右下是在onTouch里面控制动画开启or关闭。
demo采用了自定义View的方式,重点有几点:
var myX: Float = 0f
var myY: Float = 0f
var xDiff: Float = 0f
var yDiff: Float = 0f
override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
myX = x
myY = y
xDiff = event?.rawX - x
yDiff = event?.rawY - y
}
MotionEvent.ACTION_MOVE -> {
x = event?.rawX - xDiff
y = event?.rawY - yDiff
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
//回到最初的位置
animate().x(myX).y(myY)
}
}
return super.onTouchEvent(event)
}
手指按下时,记录View的x、y值,move时改变x和y值,松开时,回到最初的位置。
var innerRaduisFactor: Float = 0f
//内部半径因子,[0,1)
set(value) {
field = value
invalidate()
}
val showText = "按住拍"
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
if (innerRaduisFactor >= MIN_INNER_RADIUS_FACTOR) {
paint.style = Paint.Style.STROKE
val left = measuredWidth * (1 - innerRaduisFactor) / 2
val right = left + (width / 2 - left) * 2
paint.strokeWidth = measuredWidth * (1 - innerRaduisFactor) / 2
canvas?.drawArc(left, left, right, right, 0f, 360f, false, paint)
} else {
paint.style = Paint.Style.FILL
canvas?.drawCircle(width / 2.0f, height / 2.0f, width / 2.0f, paint)
paint.textSize = 40f
paint.color = Color.WHITE
val textX = (width - paint.measureText(showText)) / 2.0f
val textY = height / 2 + Math.abs(paint.fontMetrics.ascent + paint.fontMetrics.descent) / 2
canvas?.drawText(showText, 0, showText.length, textX, textY, paint)
paint.color = Color.RED
}
}
innerRadiusFactor设置时,调用invalidate触发onDraw(),这样属性动画就可以使用innerRadiusFactor属性了。
关于StateListAnimator,可以参考让View跟随状态动起来——StateListAnimator。本例子中的代码主要是在state_pressed=true状态下,尺寸扩大1.5,然后innerRadiusFactor在[0.75,0.9]之间无限回荡;在state_pressed=false状态下,恢复到最初状态。
val breathAnimator: StateListAnimator by lazy {
val pressedOuterAnim = AnimatorSet().apply {
play(ObjectAnimator.ofFloat(this@BreathView, SCALE_X, 1.0f, 1.5f))
.with(ObjectAnimator.ofFloat(this@BreathView, SCALE_Y, 1.0f, 1.5f))
}
val pressedInnerAnim = ObjectAnimator.ofFloat(this, "innerRaduisFactor", MAX_INNER_RADIUS_FACTOR, MIN_INNER_RADIUS_FACTOR).apply {
repeatMode = ValueAnimator.REVERSE
repeatCount = ValueAnimator.INFINITE
duration = 1000
}
val pressedAnim = AnimatorSet().apply {
play(pressedOuterAnim).before(pressedInnerAnim)
}
val normalOuterAnim = AnimatorSet().apply {
play(ObjectAnimator.ofFloat(this@BreathView, SCALE_X, 1.0f))
.with(ObjectAnimator.ofFloat(this@BreathView, SCALE_Y, 1.0f))
}
val normalInnerAnim = ObjectAnimator.ofFloat(this, "innerRaduisFactor", 0f)
val normalAnim = AnimatorSet().apply {
play(normalOuterAnim).before(normalInnerAnim)
}
StateListAnimator().apply {
addState(intArrayOf(android.R.attr.state_pressed), pressedAnim)
addState(intArrayOf(-android.R.attr.state_pressed), normalAnim)
}
}
init {
isClickable = true
stateListAnimator = breathAnimator
}
在init{}中给View设置stateListAnimator,需要注意的是isClickable必须得设为true,不然移动和动画都是无效的。
首先依然是定义属性动画,这个和StateListAnimator类似的,只不过那里是将两个动画组合在了一起,这里需要拆分开来。然后在onTouch()方法里面开启和关闭,onTouch()方法如下:
override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
myX = x
myY = y
xDiff = event?.rawX - x
yDiff = event?.rawY - y
normalAnimator.cancel()
pressedAnimtor.start()
}
MotionEvent.ACTION_MOVE -> {
x = event?.rawX - xDiff
y = event?.rawY - yDiff
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
animate().x(myX).y(myY)
pressedAnimtor.cancel()
normalAnimator.start()
}
}
return super.onTouchEvent(event)
}
在down事件中开启innerRadiusFactor缩放的动画,在up或cancel事件中关闭动画。
另外需要注意的是,在onDetachFromWindow()中关闭动画,
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
pressedAnimtor.cancel()
normalAnimator.cancel()
}
以上就是我这边想到的两种方式实现,其实本质都还是属性动画。
关于代码,可以参考BreathView和BreathView2。
实际上,BreathView可以做的更精细化些,比如加入一些自定义属性,这样可定制更高些,这里只是为了模仿抖音的效果,因此就没有做的很细致。感兴趣的朋友可以对其做的精细化些。
关注我的技术公众号,不定期会有技术文章推送,不敢说优质,但至少是我自己的学习心得。微信扫一扫下方二维码即可关注: