feat(widget): 캐릭터 채팅 썸네일 컴포넌트를 추가한다

This commit is contained in:
2026-05-21 11:22:23 +09:00
parent c58f03be08
commit c32f9cdd9f
15 changed files with 1164 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
package kr.co.vividnext.sodalive.v2.widget.characterchatthumbnail
import java.util.Locale
import kotlin.math.floor
object CharacterChatCountFormatter {
fun format(count: Long): String {
val safeCount = count.coerceAtLeast(0)
if (safeCount < 10_000) return safeCount.toString()
if (safeCount >= 100_000) return "${safeCount / 10_000}"
val value = safeCount / 10_000.0
return if (safeCount % 10_000L == 0L) {
"${safeCount / 10_000}"
} else {
String.format(Locale.KOREAN, "%.1f만", floor(value * 10) / 10)
}
}
}

View File

@@ -0,0 +1,15 @@
package kr.co.vividnext.sodalive.v2.widget.characterchatthumbnail
data class CharacterChatThumbnailItem(
val characterId: Long,
val imageUrl: String,
val characterName: String,
val characterDescription: String,
val chatMessageCount: Long,
val hasOriginal: Boolean,
val originalTitle: String
) {
val shouldShowOriginalTitle: Boolean = hasOriginal
val shouldShowChatCountBadge: Boolean = chatMessageCount >= 100
}

View File

@@ -0,0 +1,72 @@
package kr.co.vividnext.sodalive.v2.widget.characterchatthumbnail
import android.content.Context
import android.graphics.Outline
import android.util.AttributeSet
import android.view.View
import android.view.ViewOutlineProvider
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import kr.co.vividnext.sodalive.R
class CharacterChatThumbnailView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
private var image: ImageView? = null
private var chatCountBadge: View? = null
private var chatCountText: TextView? = null
private var characterNameText: TextView? = null
private var characterDescriptionText: TextView? = null
private var originalTitleText: TextView? = null
private var currentItem: CharacterChatThumbnailItem? = null
private var clickListener: ((CharacterChatThumbnailItem) -> Unit)? = null
override fun onFinishInflate() {
super.onFinishInflate()
image = findViewById(R.id.iv_character_chat_thumbnail_image)
chatCountBadge = findViewById(R.id.ll_character_chat_count_badge)
chatCountText = findViewById(R.id.tv_character_chat_count)
characterNameText = findViewById(R.id.tv_character_chat_name)
characterDescriptionText = findViewById(R.id.tv_character_chat_description)
originalTitleText = findViewById(R.id.tv_character_chat_original_title)
clipToOutline = true
outlineProvider = roundedCardOutlineProvider()
}
fun bind(item: CharacterChatThumbnailItem) {
currentItem = item
requireNotNull(chatCountText).text = CharacterChatCountFormatter.format(item.chatMessageCount)
requireNotNull(characterNameText).text = item.characterName
requireNotNull(characterDescriptionText).text = item.characterDescription
requireNotNull(originalTitleText).apply {
text = item.originalTitle
visibility = if (item.shouldShowOriginalTitle) View.VISIBLE else View.INVISIBLE
}
requireNotNull(chatCountBadge).visibility = if (item.shouldShowChatCountBadge) View.VISIBLE else View.GONE
applyClickState(item)
}
fun imageView(): ImageView = requireNotNull(image)
fun setOnCharacterClick(listener: ((CharacterChatThumbnailItem) -> Unit)?) {
clickListener = listener
currentItem?.let(::applyClickState)
}
private fun applyClickState(item: CharacterChatThumbnailItem) {
val listener = clickListener
setOnClickListener(if (listener == null) null else View.OnClickListener { listener(item) })
isClickable = listener != null
}
private fun roundedCardOutlineProvider() = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
val radius = resources.getDimension(R.dimen.radius_14)
outline.setRoundRect(0, 0, view.width, view.height, radius)
}
}
}