feat(live-room): 왕하트 애니메이션 수정
- 기존 가운데에서 한 번 폭발 후 비 내리는 애니메이션에서 가운데 + 랜덤 위치로 총 7번 폭발 후 비 내리는 애니메이션으로 수정
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user