feat(live-room): 하트를 길게(2초)간 누르면 표시 되는 왕하트(100캔) 추가, 애니메이션 제외
This commit is contained in:
@@ -245,10 +245,11 @@ class LiveRepository(
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun likeHeart(roomId: Long, token: String) = api.likeHeart(
|
||||
fun likeHeart(roomId: Long, heartCount: Int = 1, token: String) = api.likeHeart(
|
||||
request = LiveRoomLikeHeartRequest(
|
||||
roomId = roomId,
|
||||
container = "aos"
|
||||
container = "aos",
|
||||
heartCount = heartCount
|
||||
),
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
@@ -23,7 +23,9 @@ import android.text.method.LinkMovementMethod
|
||||
import android.text.style.ClickableSpan
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.view.inputmethod.EditorInfo
|
||||
@@ -1687,6 +1689,16 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
}
|
||||
}
|
||||
|
||||
LiveRoomChatRawMessageType.BIG_HEART_DONATION -> {
|
||||
handler.post {
|
||||
addHeartMessage(nickname)
|
||||
addHeartAnimation()
|
||||
lifecycleScope.launch {
|
||||
viewModel.addHeartDonation(heartCount = message.can)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LiveRoomChatRawMessageType.SECRET_DONATION -> {
|
||||
val nickname = viewModel.getUserNickname(memberId.toInt())
|
||||
val profileUrl = viewModel.getUserProfileUrl(memberId.toInt())
|
||||
@@ -1839,10 +1851,156 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun initLikeHeartButton() {
|
||||
if (!isHost) {
|
||||
binding.flLikeHeart.visibility = View.VISIBLE
|
||||
binding.flLikeHeart.setOnClickListener {
|
||||
binding.flLikeHeart.isClickable = true
|
||||
|
||||
// 클릭: 기존 동작 유지 (1캔 소모)
|
||||
binding.flLikeHeart.setOnClickListener { handleHeartClick() }
|
||||
|
||||
// 롱클릭 터치 핸들러로 위임
|
||||
binding.flLikeHeart.setOnTouchListener { _, event ->
|
||||
handleHeartLongPressTouch(event)
|
||||
}
|
||||
} else {
|
||||
binding.flLikeHeart.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private var longPressCenterHeart: ImageView? = null
|
||||
|
||||
// 롱프레스 상태/파라미터
|
||||
private var isLongPressTriggered = false
|
||||
private var isTrackingLongPress = false
|
||||
private var pressStartTime = 0L
|
||||
private var isLongPressBlockedByAvailability = false
|
||||
|
||||
// 롱프레스 트리거까지의 총 시간(2초 유지 시 BIG HEART)
|
||||
private val longPressDurationMs = 2000L
|
||||
|
||||
private val longPressScaleDurationMs get() = longPressDurationMs
|
||||
private val longPressScaleStart = 1.0f
|
||||
private val longPressScaleMax = 4.0f
|
||||
|
||||
// 롱프레스 진행 중 스케일 업데이트 러너블
|
||||
private val longPressScaleUpdateRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
if (!isTrackingLongPress) return
|
||||
if (isLongPressBlockedByAvailability) return
|
||||
val elapsed = System.currentTimeMillis() - pressStartTime
|
||||
|
||||
// 스케일은 더 빠르게 커지도록 별도의 duration 기준으로 계산(최대치 도달 후 유지)
|
||||
val scaleFraction = (elapsed.coerceAtMost(longPressScaleDurationMs)
|
||||
.toFloat() / longPressScaleDurationMs.toFloat())
|
||||
val scale =
|
||||
longPressScaleStart + (longPressScaleMax - longPressScaleStart) * scaleFraction
|
||||
updateCenterHeartScale(scale)
|
||||
|
||||
// 2초 유지 시 트리거 실행
|
||||
if (elapsed >= longPressDurationMs && !isLongPressTriggered) {
|
||||
isLongPressTriggered = true
|
||||
isTrackingLongPress = false
|
||||
removeCenterHeartForLongPress(withFade = true)
|
||||
triggerBigHeartDonation()
|
||||
return
|
||||
}
|
||||
|
||||
if (isTrackingLongPress && !isLongPressTriggered) {
|
||||
// 다음 프레임 업데이트(약 60fps)
|
||||
handler.postDelayed(this, 16L)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 롱프레스 터치 처리 로직 (initLikeHeartButton에서 위임)
|
||||
private fun handleHeartLongPressTouch(event: MotionEvent): Boolean {
|
||||
return when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
// 이용 가능하지 않으면 롱프레스를 즉시 취소하고 안내 팝업 노출
|
||||
if (!isAvailableLikeHeart) {
|
||||
isLongPressTriggered = false
|
||||
isTrackingLongPress = false
|
||||
isLongPressBlockedByAvailability = true
|
||||
removeCenterHeartForLongPress(withFade = false)
|
||||
|
||||
SodaDialog(
|
||||
activity = this@LiveRoomActivity,
|
||||
layoutInflater = layoutInflater,
|
||||
title = "안내",
|
||||
desc = "'좋아해요'는 유료 후원입니다.\n클릭시 1캔이 소진됩니다.",
|
||||
confirmButtonTitle = "확인",
|
||||
confirmButtonClick = { isAvailableLikeHeart = true }
|
||||
).show(screenWidth)
|
||||
return true
|
||||
}
|
||||
|
||||
isLongPressTriggered = false
|
||||
isTrackingLongPress = true
|
||||
pressStartTime = System.currentTimeMillis()
|
||||
// 중앙 하트 생성 및 초기 스케일 설정
|
||||
showCenterHeartForLongPress()
|
||||
updateCenterHeartScale(longPressScaleStart)
|
||||
handler.post(longPressScaleUpdateRunnable)
|
||||
true
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
// 이용 불가로 인해 즉시 안내를 띄운 케이스: 클릭 위임 없이 종료
|
||||
if (isLongPressBlockedByAvailability) {
|
||||
isLongPressBlockedByAvailability = false
|
||||
return true
|
||||
}
|
||||
|
||||
val wasTriggered = isLongPressTriggered
|
||||
isTrackingLongPress = false
|
||||
// 중앙 하트 제거
|
||||
removeCenterHeartForLongPress(withFade = !wasTriggered)
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun showCenterHeartForLongPress() {
|
||||
if (longPressCenterHeart != null) return
|
||||
val size = 33.3f.dpToPx().toInt()
|
||||
val lp = FrameLayout.LayoutParams(size, size).apply { gravity = Gravity.CENTER }
|
||||
val heart = ImageView(this).apply {
|
||||
setImageResource(R.drawable.ic_heart_pink)
|
||||
layoutParams = lp
|
||||
scaleX = 1f
|
||||
scaleY = 1f
|
||||
alpha = 1f
|
||||
}
|
||||
binding.flRoot.addView(heart)
|
||||
longPressCenterHeart = heart
|
||||
}
|
||||
|
||||
private fun updateCenterHeartScale(scale: Float) {
|
||||
longPressCenterHeart?.let {
|
||||
it.scaleX = scale
|
||||
it.scaleY = scale
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeCenterHeartForLongPress(withFade: Boolean = false) {
|
||||
val heart = longPressCenterHeart ?: return
|
||||
longPressCenterHeart = null
|
||||
if (withFade) {
|
||||
heart.animate()
|
||||
.alpha(0f)
|
||||
.setDuration(150L)
|
||||
.withEndAction { binding.flRoot.removeView(heart) }
|
||||
.start()
|
||||
} else {
|
||||
binding.flRoot.removeView(heart)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleHeartClick() {
|
||||
if (isAvailableLikeHeart) {
|
||||
binding.flLikeHeart.isEnabled = false
|
||||
viewModel.likeHeart(
|
||||
@@ -1887,15 +2045,54 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
activity = this@LiveRoomActivity,
|
||||
layoutInflater = layoutInflater,
|
||||
title = "안내",
|
||||
desc = "'좋아해요'는 유료 후원입니다.\n" +
|
||||
"클릭시 1캔이 소진됩니다.",
|
||||
desc = "'좋아해요'는 유료 후원입니다.\n클릭시 1캔이 소진됩니다.",
|
||||
confirmButtonTitle = "확인",
|
||||
confirmButtonClick = { isAvailableLikeHeart = true }
|
||||
).show(screenWidth)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.flLikeHeart.visibility = View.GONE
|
||||
|
||||
private fun triggerBigHeartDonation() {
|
||||
if (isAvailableLikeHeart) {
|
||||
binding.flLikeHeart.isEnabled = false
|
||||
viewModel.likeHeart(
|
||||
roomId = roomId,
|
||||
heartCount = 100,
|
||||
onSuccess = {
|
||||
val donationRawMessage = Gson().toJson(
|
||||
LiveRoomChatRawMessage(
|
||||
type = LiveRoomChatRawMessageType.BIG_HEART_DONATION,
|
||||
message = "",
|
||||
can = 100,
|
||||
signature = null,
|
||||
signatureImageUrl = null,
|
||||
donationMessage = null
|
||||
)
|
||||
)
|
||||
|
||||
agora.sendRawMessageToGroup(
|
||||
rawMessage = donationRawMessage.toByteArray(),
|
||||
onSuccess = {
|
||||
val nickname = viewModel.getUserNickname(
|
||||
SharedPreferenceManager.userId.toInt()
|
||||
)
|
||||
handler.post {
|
||||
addHeartMessage(nickname)
|
||||
addHeartAnimation()
|
||||
lifecycleScope.launch { viewModel.addHeartDonation(100) }
|
||||
}
|
||||
},
|
||||
onFailure = {
|
||||
viewModel.refundDonation(roomId)
|
||||
}
|
||||
)
|
||||
|
||||
binding.flLikeHeart.isEnabled = true
|
||||
},
|
||||
onFailure = {
|
||||
binding.flLikeHeart.isEnabled = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -567,15 +567,15 @@ class LiveRoomViewModel(
|
||||
)
|
||||
}
|
||||
|
||||
fun likeHeart(roomId: Long, onSuccess: () -> Unit, onFailure: () -> Unit) {
|
||||
fun likeHeart(roomId: Long, heartCount: Int = 1, onSuccess: () -> Unit, onFailure: () -> Unit) {
|
||||
compositeDisposable.add(
|
||||
repository.likeHeart(roomId, token = "Bearer ${SharedPreferenceManager.token}")
|
||||
repository.likeHeart(roomId, heartCount = heartCount, token = "Bearer ${SharedPreferenceManager.token}")
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
if (it.success) {
|
||||
SharedPreferenceManager.can -= 1
|
||||
SharedPreferenceManager.can -= heartCount
|
||||
onSuccess()
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
@@ -682,9 +682,9 @@ class LiveRoomViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun addHeartDonation() {
|
||||
suspend fun addHeartDonation(heartCount: Int = 1) {
|
||||
mutex.withLock {
|
||||
_totalHeartCount.postValue(totalHeartCount.value!! + 1)
|
||||
_totalHeartCount.postValue(totalHeartCount.value!! + heartCount)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,5 +35,8 @@ enum class LiveRoomChatRawMessageType {
|
||||
ROULETTE_DONATION,
|
||||
|
||||
@SerializedName("HEART_DONATION")
|
||||
HEART_DONATION
|
||||
HEART_DONATION,
|
||||
|
||||
@SerializedName("BIG_HEART_DONATION")
|
||||
BIG_HEART_DONATION
|
||||
}
|
||||
|
||||
@@ -6,5 +6,6 @@ import com.google.gson.annotations.SerializedName
|
||||
@Keep
|
||||
data class LiveRoomLikeHeartRequest(
|
||||
@SerializedName("roomId") val roomId: Long,
|
||||
@SerializedName("container") val container: String
|
||||
@SerializedName("container") val container: String,
|
||||
@SerializedName("heartCount") val heartCount: Int = 1,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user