feat(live-room): 하트를 길게(2초)간 누르면 표시 되는 왕하트(100캔) 추가, 애니메이션 제외
This commit is contained in:
@@ -245,10 +245,11 @@ class LiveRepository(
|
|||||||
authHeader = token
|
authHeader = token
|
||||||
)
|
)
|
||||||
|
|
||||||
fun likeHeart(roomId: Long, token: String) = api.likeHeart(
|
fun likeHeart(roomId: Long, heartCount: Int = 1, token: String) = api.likeHeart(
|
||||||
request = LiveRoomLikeHeartRequest(
|
request = LiveRoomLikeHeartRequest(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
container = "aos"
|
container = "aos",
|
||||||
|
heartCount = heartCount
|
||||||
),
|
),
|
||||||
authHeader = token
|
authHeader = token
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ import android.text.method.LinkMovementMethod
|
|||||||
import android.text.style.ClickableSpan
|
import android.text.style.ClickableSpan
|
||||||
import android.text.style.ForegroundColorSpan
|
import android.text.style.ForegroundColorSpan
|
||||||
import android.text.style.StyleSpan
|
import android.text.style.StyleSpan
|
||||||
|
import android.view.Gravity
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.animation.AccelerateDecelerateInterpolator
|
import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
import android.view.inputmethod.EditorInfo
|
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 -> {
|
LiveRoomChatRawMessageType.SECRET_DONATION -> {
|
||||||
val nickname = viewModel.getUserNickname(memberId.toInt())
|
val nickname = viewModel.getUserNickname(memberId.toInt())
|
||||||
val profileUrl = viewModel.getUserProfileUrl(memberId.toInt())
|
val profileUrl = viewModel.getUserProfileUrl(memberId.toInt())
|
||||||
@@ -1839,63 +1851,248 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
private fun initLikeHeartButton() {
|
private fun initLikeHeartButton() {
|
||||||
if (!isHost) {
|
if (!isHost) {
|
||||||
binding.flLikeHeart.visibility = View.VISIBLE
|
binding.flLikeHeart.visibility = View.VISIBLE
|
||||||
binding.flLikeHeart.setOnClickListener {
|
binding.flLikeHeart.isClickable = true
|
||||||
if (isAvailableLikeHeart) {
|
|
||||||
binding.flLikeHeart.isEnabled = false
|
|
||||||
viewModel.likeHeart(
|
|
||||||
roomId = roomId,
|
|
||||||
onSuccess = {
|
|
||||||
val donationRawMessage = Gson().toJson(
|
|
||||||
LiveRoomChatRawMessage(
|
|
||||||
type = LiveRoomChatRawMessageType.HEART_DONATION,
|
|
||||||
message = "",
|
|
||||||
can = 1,
|
|
||||||
signature = null,
|
|
||||||
signatureImageUrl = null,
|
|
||||||
donationMessage = null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
agora.sendRawMessageToGroup(
|
// 클릭: 기존 동작 유지 (1캔 소모)
|
||||||
rawMessage = donationRawMessage.toByteArray(),
|
binding.flLikeHeart.setOnClickListener { handleHeartClick() }
|
||||||
onSuccess = {
|
|
||||||
val nickname = viewModel.getUserNickname(
|
// 롱클릭 터치 핸들러로 위임
|
||||||
SharedPreferenceManager.userId.toInt()
|
binding.flLikeHeart.setOnTouchListener { _, event ->
|
||||||
)
|
handleHeartLongPressTouch(event)
|
||||||
handler.post {
|
}
|
||||||
addHeartMessage(nickname)
|
} else {
|
||||||
addHeartAnimation()
|
binding.flLikeHeart.visibility = View.GONE
|
||||||
lifecycleScope.launch { viewModel.addHeartDonation() }
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
onFailure = {
|
private var longPressCenterHeart: ImageView? = null
|
||||||
viewModel.refundDonation(roomId)
|
|
||||||
}
|
// 롱프레스 상태/파라미터
|
||||||
)
|
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)
|
||||||
|
|
||||||
binding.flLikeHeart.isEnabled = true
|
|
||||||
},
|
|
||||||
onFailure = {
|
|
||||||
binding.flLikeHeart.isEnabled = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
SodaDialog(
|
SodaDialog(
|
||||||
activity = this@LiveRoomActivity,
|
activity = this@LiveRoomActivity,
|
||||||
layoutInflater = layoutInflater,
|
layoutInflater = layoutInflater,
|
||||||
title = "안내",
|
title = "안내",
|
||||||
desc = "'좋아해요'는 유료 후원입니다.\n" +
|
desc = "'좋아해요'는 유료 후원입니다.\n클릭시 1캔이 소진됩니다.",
|
||||||
"클릭시 1캔이 소진됩니다.",
|
|
||||||
confirmButtonTitle = "확인",
|
confirmButtonTitle = "확인",
|
||||||
confirmButtonClick = { isAvailableLikeHeart = true }
|
confirmButtonClick = { isAvailableLikeHeart = true }
|
||||||
).show(screenWidth)
|
).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 {
|
} else {
|
||||||
binding.flLikeHeart.visibility = View.GONE
|
binding.flRoot.removeView(heart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleHeartClick() {
|
||||||
|
if (isAvailableLikeHeart) {
|
||||||
|
binding.flLikeHeart.isEnabled = false
|
||||||
|
viewModel.likeHeart(
|
||||||
|
roomId = roomId,
|
||||||
|
onSuccess = {
|
||||||
|
val donationRawMessage = Gson().toJson(
|
||||||
|
LiveRoomChatRawMessage(
|
||||||
|
type = LiveRoomChatRawMessageType.HEART_DONATION,
|
||||||
|
message = "",
|
||||||
|
can = 1,
|
||||||
|
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() }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFailure = {
|
||||||
|
viewModel.refundDonation(roomId)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.flLikeHeart.isEnabled = true
|
||||||
|
},
|
||||||
|
onFailure = {
|
||||||
|
binding.flLikeHeart.isEnabled = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
SodaDialog(
|
||||||
|
activity = this@LiveRoomActivity,
|
||||||
|
layoutInflater = layoutInflater,
|
||||||
|
title = "안내",
|
||||||
|
desc = "'좋아해요'는 유료 후원입니다.\n클릭시 1캔이 소진됩니다.",
|
||||||
|
confirmButtonTitle = "확인",
|
||||||
|
confirmButtonClick = { isAvailableLikeHeart = true }
|
||||||
|
).show(screenWidth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
compositeDisposable.add(
|
||||||
repository.likeHeart(roomId, token = "Bearer ${SharedPreferenceManager.token}")
|
repository.likeHeart(roomId, heartCount = heartCount, token = "Bearer ${SharedPreferenceManager.token}")
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
{
|
{
|
||||||
if (it.success) {
|
if (it.success) {
|
||||||
SharedPreferenceManager.can -= 1
|
SharedPreferenceManager.can -= heartCount
|
||||||
onSuccess()
|
onSuccess()
|
||||||
} else {
|
} else {
|
||||||
if (it.message != null) {
|
if (it.message != null) {
|
||||||
@@ -682,9 +682,9 @@ class LiveRoomViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun addHeartDonation() {
|
suspend fun addHeartDonation(heartCount: Int = 1) {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
_totalHeartCount.postValue(totalHeartCount.value!! + 1)
|
_totalHeartCount.postValue(totalHeartCount.value!! + heartCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,5 +35,8 @@ enum class LiveRoomChatRawMessageType {
|
|||||||
ROULETTE_DONATION,
|
ROULETTE_DONATION,
|
||||||
|
|
||||||
@SerializedName("HEART_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
|
@Keep
|
||||||
data class LiveRoomLikeHeartRequest(
|
data class LiveRoomLikeHeartRequest(
|
||||||
@SerializedName("roomId") val roomId: Long,
|
@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