0%

拉环下拉弹动动画效果


最近工作忙项目比较急,很少有时间更新blog了,不过好习惯还是应该保持,时间挤挤还是有的。项目中遇到一个需求,实现一个拉环动画,有一根拉伸可以往下拉动并在放手的时候回弹然后上下抖动,就跟我们生活中拖拽一根带了拉环的橡皮筋效果是一样的。最终我是用属性动画去实现这个需求,感觉不算复杂效果也还行,在此记录实现过程,欢迎相互交流~


最终效果图gif:
image

自定义View

实现AnimatorUpdateListener和Animator.AnimatorListener接口

1
2
3
4
5
6
7
8
9
class RingView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr), AnimatorUpdateListener, Animator.AnimatorListener{
override fun onAnimationUpdate(animation: ValueAnimator) {}
override fun onAnimationRepeat(animation: Animator) {}
override fun onAnimationCancel(animation: Animator) {}
override fun onAnimationStart(animation: Animator) {}
override fun onAnimationEnd(animation: Animator) {}
}

初始化

初始化paint,animator,设置1秒后开始动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
init {
paint.color = resources.getColor(R.color.colorPrimary)
paint.isAntiAlias = true
postDelayed({ startAnimation() }, 1000)
}
private fun initAnimator(start: Int, end: Int) {
animator = ValueAnimator.ofInt(start, end)
animator?.duration = 1000
animator?.interpolator = accelerateInterpolator
animator?.addUpdateListener(this)
animator?.addListener(this)
}

private fun startAnimation() {
initAnimator(startY, endY)
isDown = true
animator?.start()
}

绘制

绘制拉环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private val bgRing = BitmapFactory.decodeResource(resources, R.mipmap.bg_ring)
// 拉环绘制尺寸
private val targetWidth = (bgRing.width * 1.4).toInt()
private val targetHeight = (bgRing.height * 1.4).toInt()
private val radius = targetWidth / 2
// 拉环坐标
private var xPos = 0
private var yPos = 0
private val srcRingRect = Rect(0, 0, bgRing.width, bgRing.height)
private val dstRingRect = Rect()

private fun drawRing(canvas: Canvas) {
dstRingRect.left = xPos
dstRingRect.top = yPos
dstRingRect.right = xPos + targetWidth
dstRingRect.bottom = yPos + targetHeight
canvas.drawBitmap(bgRing, srcRingRect, dstRingRect, paint)
}

绘制弹力绳

1
2
3
4
5
6
private val strokeWidth = 6f  // 绳子粗细
private var ratio = 0f // 弹性变化比例
private fun drawLine(canvas: Canvas) {
paint.strokeWidth = strokeWidth - 3 * ratio
canvas.drawLine((xPos + radius).toFloat(), 0f, (xPos + radius).toFloat(), (yPos + 2).toFloat(), paint)
}

重写onDraw

初始化上下轻微抖动的范围和最大拖拽长度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 上下轻微抖动的范围
private var startY = 0
private var endY = 0
private var maxDragY = 0 // 最大拖拽长度
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (animator == null) {
startY = canvas.height / 2 - radius / 2
endY = canvas.height / 2
yPos = startY
maxDragY = endY + endY / 2
xPos = canvas.width / 2 - radius
}
drawLine(canvas)
drawRing(canvas)
}

下拉后自动回弹

利用加速插值器和减速插值器实现惯性向下运动,减速向上回弹,监听动画结束事件,更新运动方向标志位,下落时设置为加速插值器,回弹时设置为减速插值器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private var isDown = true
private var stopAnimate = false // 是否停止动画
private val accelerateInterpolator = AccelerateInterpolator()
private val decelerateInterpolator = DecelerateInterpolator()
override fun onAnimationEnd(animation: Animator) {
if (!stopAnimate) {
val vAnimation = animation as ValueAnimator
isDown = !isDown
if (isDown) {
vAnimation.setIntValues(startY, endY)
vAnimation.interpolator = accelerateInterpolator
} else {
vAnimation.setIntValues(endY, startY)
vAnimation.interpolator = decelerateInterpolator
}
vAnimation.start()
}
}

拖拽逻辑

重写onTouchEvent,监听ACTION_DOWN/ACTION_UP/ACTION_MOVE事件
手指落下时:判断当前触摸位置坐标是否在拉环矩形区域内,如果是则记录开始拖拽位置的Y坐标,停止当前动画,记录上一次手指移动的Y坐标
手指移动时:记录手指当前Y坐标,如果是拖拽状态且拖拽距离大于10,计算当前拖拽的插值,如果插值大于0则为向下拖拽,继续判断当前Y坐标是否小于最大拖拽Y坐标位置,如果是计算拉环当前运动的目标位置,计算当前拖拽位置到最大拖拽位置的百分比,修正拉环的Y坐标,实现拖拽时移动缓慢的阻尼效果,达到最大拖拽位置时百分比为1,更新拖拽状态为false,刷新视图,更新最后一次拖拽的Y坐标
手指抬起时:更新拖拽状态为false,更新最后一次拖拽的Y坐标为0,触发回弹动画,设置为减速插值器,刷新动画标志位,向下标志位为false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
isDrag = dstRingRect.plus(padding).contains(event.x.toInt(), event.y.toInt())
if (isDrag) {
lastDragY = event.y
if (animator != null && animator!!.isRunning) {
stopAnimate = true
animator?.cancel()
}
lastMoveY = (yPos + radius).toFloat()
}
}
MotionEvent.ACTION_UP -> {
// 弹回去
back()
lastMoveY = 0f
isDrag = false
}
MotionEvent.ACTION_MOVE -> {
curDragY = event.y
if (isDrag && curDragY - lastDragY > 10) {
val diffY = curDragY - lastMoveY
Log.i(TAG, "diffY: $diffY" )
if (diffY > 0){
if (curDragY < maxDragY){
val targetY = curDragY.toInt()
Log.i(TAG, "maxLength: ${maxDragY - lastDragY}" )
ratio = (targetY - lastDragY) / (maxDragY - lastDragY)
yPos = (yPos + diffY * (1 - ratio)).toInt()
Log.i(TAG, "add: ${diffY * (1 - ratio)}, diffY:${diffY} ratio:${(1 - ratio)}" )
} else {
ratio = 1f
isDrag = false
}

invalidate()
}
lastMoveY = curDragY
}
}
}
return true
}

private fun back() {
if (animator != null && !animator!!.isRunning) {
animator?.setIntValues(yPos, startY)
animator?.interpolator = decelerateInterpolator
stopAnimate = false
animator?.start()
isDown = false
}
}

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
class RingView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr), AnimatorUpdateListener, Animator.AnimatorListener {
private val bgRing = BitmapFactory.decodeResource(resources, R.mipmap.bg_ring)
// 拉环绘制尺寸
private val targetWidth = (bgRing.width * 1.4).toInt()
private val targetHeight = (bgRing.height * 1.4).toInt()
private val radius = targetWidth / 2
// 拉环坐标
private var xPos = 0
private var yPos = 0
private val strokeWidth = 6f // 绳子粗细
private val paint = Paint()
private var animator: ValueAnimator? = null
private var isDown = true

// 上下轻微抖动的范围
private var startY = 0
private var endY = 0
private var maxDragY = 0// 最大拖拽长度
private val accelerateInterpolator = AccelerateInterpolator()
private val decelerateInterpolator = DecelerateInterpolator()
private val srcRingRect = Rect(0, 0, bgRing.width, bgRing.height)
private val dstRingRect = Rect()
private var isDrag = false// 是否拖拽
private var curDragY = 0f // 当前拖拽Y坐标
private var lastDragY = 0f // 开始拖拽Y坐标
private var stopAnimate = false // 是否停止动画
private var lastMoveY = 0f // 上一次手指移动Y坐标
private var ratio = 0f // 弹性变化比例
private var padding = 20 // 触摸范围增量
init {
paint.color = resources.getColor(R.color.colorPrimary)
paint.isAntiAlias = true
postDelayed({ startAnimation() }, 1000)
}

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (animator == null) {
startY = canvas.height / 2 - radius / 2
endY = canvas.height / 2
yPos = startY
maxDragY = endY + endY / 2
xPos = canvas.width / 2 - radius
}
drawLine(canvas)
drawRing(canvas)
}

private fun drawRing(canvas: Canvas) {
dstRingRect.left = xPos
dstRingRect.top = yPos
dstRingRect.right = xPos + targetWidth
dstRingRect.bottom = yPos + targetHeight
canvas.drawBitmap(bgRing, srcRingRect, dstRingRect, paint)
}

override fun onAnimationUpdate(animation: ValueAnimator) {
yPos = animation.animatedValue as Int
ratio = (yPos - startY).toFloat() / (maxDragY - startY)
invalidate()
}

private fun drawLine(canvas: Canvas) {
paint.strokeWidth = strokeWidth - 3 * ratio
canvas.drawLine((xPos + radius).toFloat(), 0f, (xPos + radius).toFloat(), (yPos + 2).toFloat(), paint)
}

private fun initAnimator(start: Int, end: Int) {
animator = ValueAnimator.ofInt(start, end)
animator?.duration = 1000
animator?.interpolator = accelerateInterpolator
animator?.addUpdateListener(this)
animator?.addListener(this)
}

private fun startAnimation() {
initAnimator(startY, endY)
isDown = true
animator?.start()
}

override fun onAnimationRepeat(animation: Animator) {}
override fun onAnimationStart(animation: Animator) {}
override fun onAnimationEnd(animation: Animator) {
if (!stopAnimate) {
val vAnimation = animation as ValueAnimator
isDown = !isDown
if (isDown) {
vAnimation.setIntValues(startY, endY)
vAnimation.interpolator = accelerateInterpolator
} else {
vAnimation.setIntValues(endY, startY)
vAnimation.interpolator = decelerateInterpolator
}
vAnimation.start()
}
}

override fun onAnimationCancel(animation: Animator) {}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
isDrag = dstRingRect.plus(padding).contains(event.x.toInt(), event.y.toInt())
if (isDrag) {
lastDragY = event.y
if (animator != null && animator!!.isRunning) {
stopAnimate = true
animator?.cancel()
}
lastMoveY = (yPos + radius).toFloat()
}
}
MotionEvent.ACTION_UP -> {
// 弹回去
back()
lastMoveY = 0f
isDrag = false
}
MotionEvent.ACTION_MOVE -> {
curDragY = event.y
if (isDrag && curDragY - lastDragY > 10) {
val diffY = curDragY - lastMoveY
Log.i(TAG, "diffY: $diffY" )
if (diffY > 0){
if (curDragY < maxDragY){
val targetY = curDragY.toInt()
Log.i(TAG, "maxLength: ${maxDragY - lastDragY}" )
ratio = (targetY - lastDragY) / (maxDragY - lastDragY)
yPos = (yPos + diffY * (1 - ratio)).toInt()
Log.i(TAG, "add: ${diffY * (1 - ratio)}, diffY:${diffY} ratio:${(1 - ratio)}" )
} else {
ratio = 1f
isDrag = false
}

invalidate()
}
lastMoveY = curDragY
}
}
}
return true
}

private fun back() {
if (animator != null && !animator!!.isRunning) {
animator?.setIntValues(yPos, startY)
animator?.interpolator = decelerateInterpolator
stopAnimate = false
animator?.start()
isDown = false
}
}

}

xml中引入

1
2
3
4
<com.example.testapp.RingView
android:layout_width="match_parent"
android:layout_height="match_parent"
/>