feat(live-room): 왕하트 애니메이션 수정
- 기존 가운데에서 한 번 폭발 후 비 내리는 애니메이션에서 가운데 + 랜덤 위치로 총 7번 폭발 후 비 내리는 애니메이션으로 수정
This commit is contained in:
@@ -2371,22 +2371,48 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
|||||||
target.visibility = View.GONE
|
target.visibility = View.GONE
|
||||||
longPressCenterHeart = null
|
longPressCenterHeart = null
|
||||||
|
|
||||||
// 하트 파티클 폭발 오버레이 추가
|
// 중앙 1회 + 랜덤 위치 6회 순차 폭발 설정 (총 4.9초)
|
||||||
val overlay = HeartExplosionView(this, cx, cy)
|
val perDuration = 700L // ms (7회 * 0.7s = 3.5s)
|
||||||
|
val totalExplosions = 7
|
||||||
|
val totalDuration = perDuration * totalExplosions
|
||||||
val lp = FrameLayout.LayoutParams(
|
val lp = FrameLayout.LayoutParams(
|
||||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||||
FrameLayout.LayoutParams.MATCH_PARENT
|
FrameLayout.LayoutParams.MATCH_PARENT
|
||||||
)
|
)
|
||||||
root.addView(overlay, lp)
|
|
||||||
overlay.start()
|
|
||||||
|
|
||||||
// 폭발 직후 0.01~0.1초 랜덤 지연 후 하트 비/우박 애니메이션 시작
|
// 랜덤 좌표 생성 준비 (root 내부, 안전 마진 포함)
|
||||||
val rainDelay = (10L..100L).random()
|
val margin = 32f.dpToPx()
|
||||||
|
val rw = if (root.width > 0) root.width else root.measuredWidth
|
||||||
|
val rh = if (root.height > 0) root.height else root.measuredHeight
|
||||||
|
val dm = resources.displayMetrics
|
||||||
|
val fallbackW = if (rw > 0) rw else dm.widthPixels
|
||||||
|
val fallbackH = if (rh > 0) rh else dm.heightPixels
|
||||||
|
val minX = margin
|
||||||
|
val maxX = (fallbackW.toFloat() - margin).coerceAtLeast(minX + 1f)
|
||||||
|
val minY = margin
|
||||||
|
val maxY = (fallbackH.toFloat() - margin).coerceAtLeast(minY + 1f)
|
||||||
|
|
||||||
|
fun randomX(): Float = minX + Random.nextFloat() * (maxX - minX)
|
||||||
|
fun randomY(): Float = minY + Random.nextFloat() * (maxY - minY)
|
||||||
|
|
||||||
|
// 0: 중앙, 1..4: 랜덤 위치
|
||||||
|
repeat(totalExplosions) { index ->
|
||||||
|
val delay = (index * perDuration - 100).coerceAtLeast(100L)
|
||||||
|
val ex = if (index == 0) cx else randomX()
|
||||||
|
val ey = if (index == 0) cy else randomY()
|
||||||
|
handler.postDelayed({
|
||||||
|
val overlay = HeartExplosionView(this, ex, ey, perDuration)
|
||||||
|
root.addView(overlay, lp)
|
||||||
|
overlay.start()
|
||||||
|
}, delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 모든 폭발이 끝난 뒤 하트 비/우박 애니메이션 시작
|
||||||
handler.postDelayed({
|
handler.postDelayed({
|
||||||
val rainView = HeartRainView(this)
|
val rainView = HeartRainView(this)
|
||||||
root.addView(rainView, lp)
|
root.addView(rainView, lp)
|
||||||
rainView.start()
|
rainView.start()
|
||||||
}, rainDelay)
|
}, totalDuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BIG_HEART 수신 시 중앙에 잠깐 하트를 그렸다가, 같은 뷰에서 바로 폭발까지 표현하고 사라지는 뷰
|
// BIG_HEART 수신 시 중앙에 잠깐 하트를 그렸다가, 같은 뷰에서 바로 폭발까지 표현하고 사라지는 뷰
|
||||||
@@ -2506,57 +2532,57 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun startExplosion(cx: Float, cy: Float) {
|
private fun startExplosion(cx: Float, cy: Float) {
|
||||||
explosionCenterX = cx
|
// 수신자도 발신자와 동일한 7회 폭발 + 총 4.9초 후 비/우박 애니메이션을 적용
|
||||||
explosionCenterY = cy
|
val parentFL = parent as? FrameLayout ?: run {
|
||||||
phase = Phase.EXPLODE
|
(parent as? FrameLayout)?.removeView(this@HeartFloatView)
|
||||||
|
return
|
||||||
// 파티클 초기화
|
|
||||||
particles.clear()
|
|
||||||
val density = resources.displayMetrics.density
|
|
||||||
val count = 90
|
|
||||||
val minScalePx = 4f * density
|
|
||||||
val maxScalePx = 10f * density
|
|
||||||
val minSpeed = 350f * density
|
|
||||||
val maxSpeed = 1100f * density
|
|
||||||
repeat(count) {
|
|
||||||
val angle = (Math.random() * Math.PI * 2).toFloat()
|
|
||||||
val speed = minSpeed + (maxSpeed - minSpeed) * Math.random().toFloat()
|
|
||||||
val scale = minScalePx + (maxScalePx - minScalePx) * Math.random().toFloat()
|
|
||||||
val rotation = (Math.random().toFloat() * 360f) - 180f
|
|
||||||
val rotationSpeed = ((Math.random().toFloat() * 360f) - 180f)
|
|
||||||
particles += Particle(cx, cy, angle, speed, scale, rotation, rotationSpeed)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 폭발과 함께 하트비/우박 애니메이션을 부모에 요청
|
// 이 뷰는 표시를 마쳤으므로 제거하고, 폭발/비 애니메이션은 오버레이 뷰들로 진행
|
||||||
val parentFL = parent as? FrameLayout
|
parentFL.removeView(this@HeartFloatView)
|
||||||
|
|
||||||
|
val perDuration = 700L // ms (7회 * 0.7s = 4.9s)
|
||||||
|
val totalExplosions = 7
|
||||||
|
val totalDuration = perDuration * totalExplosions
|
||||||
|
|
||||||
val lp = FrameLayout.LayoutParams(
|
val lp = FrameLayout.LayoutParams(
|
||||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||||
FrameLayout.LayoutParams.MATCH_PARENT
|
FrameLayout.LayoutParams.MATCH_PARENT
|
||||||
)
|
)
|
||||||
val rainDelay = (10L..100L).random()
|
|
||||||
postDelayed({
|
|
||||||
parentFL?.let { p ->
|
|
||||||
val rainView = HeartRainView(context)
|
|
||||||
p.addView(rainView, lp)
|
|
||||||
rainView.start()
|
|
||||||
}
|
|
||||||
}, rainDelay)
|
|
||||||
|
|
||||||
// 알파 페이드 아웃은 View 자체 alpha로 처리
|
// 랜덤 좌표 생성 준비 (parent 기준, 안전 마진 포함)
|
||||||
alpha = 1f
|
val margin = 32f.dpToPx()
|
||||||
explosionAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
|
val rw = if (parentFL.width > 0) parentFL.width else parentFL.measuredWidth
|
||||||
duration = explosionDurationMs
|
val rh = if (parentFL.height > 0) parentFL.height else parentFL.measuredHeight
|
||||||
interpolator = AccelerateDecelerateInterpolator()
|
val dm = resources.displayMetrics
|
||||||
addUpdateListener { invalidate() }
|
val fallbackW = if (rw > 0) rw else dm.widthPixels
|
||||||
addListener(object : AnimatorListenerAdapter() {
|
val fallbackH = if (rh > 0) rh else dm.heightPixels
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
val minX = margin
|
||||||
// 애니메이션 종료 시 자기 자신 제거
|
val maxX = (fallbackW.toFloat() - margin).coerceAtLeast(minX + 1f)
|
||||||
(parent as? FrameLayout)?.removeView(this@HeartFloatView)
|
val minY = margin
|
||||||
}
|
val maxY = (fallbackH.toFloat() - margin).coerceAtLeast(minY + 1f)
|
||||||
})
|
|
||||||
start()
|
fun randomX(): Float = minX + Random.nextFloat() * (maxX - minX)
|
||||||
|
fun randomY(): Float = minY + Random.nextFloat() * (maxY - minY)
|
||||||
|
|
||||||
|
// 0: 중앙, 1..6: 랜덤 위치
|
||||||
|
repeat(totalExplosions) { index ->
|
||||||
|
val delay = (index * perDuration - 100).coerceAtLeast(100L)
|
||||||
|
val ex = if (index == 0) cx else randomX()
|
||||||
|
val ey = if (index == 0) cy else randomY()
|
||||||
|
parentFL.postDelayed({
|
||||||
|
val overlay = HeartExplosionView(context, ex, ey, perDuration)
|
||||||
|
parentFL.addView(overlay, lp)
|
||||||
|
overlay.start()
|
||||||
|
}, delay)
|
||||||
}
|
}
|
||||||
postInvalidateOnAnimation()
|
|
||||||
|
// 모든 폭발이 끝난 뒤 하트 비/우박 애니메이션 시작
|
||||||
|
parentFL.postDelayed({
|
||||||
|
val rainView = HeartRainView(context)
|
||||||
|
parentFL.addView(rainView, lp)
|
||||||
|
rainView.start()
|
||||||
|
}, totalDuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun drawExplosion(canvas: Canvas) {
|
private fun drawExplosion(canvas: Canvas) {
|
||||||
@@ -2590,7 +2616,8 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
|||||||
private class HeartExplosionView(
|
private class HeartExplosionView(
|
||||||
context: Context,
|
context: Context,
|
||||||
private val centerX: Float,
|
private val centerX: Float,
|
||||||
private val centerY: Float
|
private val centerY: Float,
|
||||||
|
private val durationMs: Long = 900L
|
||||||
) : View(context) {
|
) : View(context) {
|
||||||
|
|
||||||
private data class Particle(
|
private data class Particle(
|
||||||
@@ -2627,7 +2654,6 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
|||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val durationMs = 900L
|
|
||||||
private val gravity = 1400f // px/s^2
|
private val gravity = 1400f // px/s^2
|
||||||
private val alphaStart = 1f
|
private val alphaStart = 1f
|
||||||
private val alphaEnd = 0f
|
private val alphaEnd = 0f
|
||||||
|
|||||||
Reference in New Issue
Block a user