feat(live-room): 왕하트 애니메이션 수정

- 기존 가운데에서 한 번 폭발 후 비 내리는 애니메이션에서 가운데 + 랜덤 위치로 총 7번 폭발 후 비 내리는 애니메이션으로 수정
This commit is contained in:
2025-11-17 16:57:27 +09:00
parent 868b2d309a
commit bbb7858508

View File

@@ -2371,22 +2371,48 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
target.visibility = View.GONE
longPressCenterHeart = null
// 하트 파티클 폭발 오버레이 추가
val overlay = HeartExplosionView(this, cx, cy)
// 중앙 1회 + 랜덤 위치 6회 순차 폭발 설정 (총 4.9초)
val perDuration = 700L // ms (7회 * 0.7s = 3.5s)
val totalExplosions = 7
val totalDuration = perDuration * totalExplosions
val lp = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
// 랜덤 좌표 생성 준비 (root 내부, 안전 마진 포함)
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)
}
// 폭발 직후 0.01~0.1초 랜덤 지연 후 하트 비/우박 애니메이션 시작
val rainDelay = (10L..100L).random()
// 모든 폭발이 끝난 뒤 하트 비/우박 애니메이션 시작
handler.postDelayed({
val rainView = HeartRainView(this)
root.addView(rainView, lp)
rainView.start()
}, rainDelay)
}, totalDuration)
}
// BIG_HEART 수신 시 중앙에 잠깐 하트를 그렸다가, 같은 뷰에서 바로 폭발까지 표현하고 사라지는 뷰
@@ -2506,57 +2532,57 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
}
private fun startExplosion(cx: Float, cy: Float) {
explosionCenterX = cx
explosionCenterY = cy
phase = Phase.EXPLODE
// 파티클 초기화
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)
// 수신자도 발신자와 동일한 7회 폭발 + 총 4.9초 후 비/우박 애니메이션을 적용
val parentFL = parent as? FrameLayout ?: run {
(parent as? FrameLayout)?.removeView(this@HeartFloatView)
return
}
// 폭발과 함께 하트비/우박 애니메이션을 부모에 요청
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(
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로 처리
alpha = 1f
explosionAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
duration = explosionDurationMs
interpolator = AccelerateDecelerateInterpolator()
addUpdateListener { invalidate() }
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
// 애니메이션 종료 시 자기 자신 제거
(parent as? FrameLayout)?.removeView(this@HeartFloatView)
// 랜덤 좌표 생성 준비 (parent 기준, 안전 마진 포함)
val margin = 32f.dpToPx()
val rw = if (parentFL.width > 0) parentFL.width else parentFL.measuredWidth
val rh = if (parentFL.height > 0) parentFL.height else parentFL.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..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)
}
})
start()
}
postInvalidateOnAnimation()
// 모든 폭발이 끝난 뒤 하트 비/우박 애니메이션 시작
parentFL.postDelayed({
val rainView = HeartRainView(context)
parentFL.addView(rainView, lp)
rainView.start()
}, totalDuration)
}
private fun drawExplosion(canvas: Canvas) {
@@ -2590,7 +2616,8 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
private class HeartExplosionView(
context: Context,
private val centerX: Float,
private val centerY: Float
private val centerY: Float,
private val durationMs: Long = 900L
) : View(context) {
private data class Particle(
@@ -2627,7 +2654,6 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
close()
}
private val durationMs = 900L
private val gravity = 1400f // px/s^2
private val alphaStart = 1f
private val alphaEnd = 0f