fix(chat-room): 채팅 아이템 UI, 메시지 입력 창 UI

- 채팅 아이템이 화면을 벗어나는 버그 수정
- 메시지 입력창 글자크기 14sp, rounded corner 32dp
This commit is contained in:
2025-08-15 00:29:56 +09:00
parent 3d727f07fa
commit dd7251f18b
8 changed files with 154 additions and 101 deletions

View File

@@ -35,6 +35,20 @@ sealed class ChatListItem {
class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
// 타이핑 인디케이터 표시용 정보(캐릭터 이름/프로필)
private var typingName: String? = null
private var typingProfileUrl: String? = null
fun setTypingInfo(name: String?, profileUrl: String?) {
typingName = name
typingProfileUrl = profileUrl
// 표시 중이면 인디케이터 UI를 갱신
if (isRecyclerViewAttached) {
val idx = findTypingIndicatorIndex()
if (idx >= 0) notifyItemChanged(idx)
}
}
// 테스트/비연결 환경에서 notify* 호출로 인한 NPE 방지용 플래그
private var isRecyclerViewAttached: Boolean = false
@@ -287,7 +301,7 @@ class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
}
is TypingIndicatorViewHolder -> {
holder.bind()
holder.bind(typingName, typingProfileUrl)
}
}
}
@@ -300,8 +314,10 @@ class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
) : RecyclerView.ViewHolder(binding.root) {
fun bind(data: ChatMessage, showTime: Boolean, isGrouped: Boolean) {
binding.tvMessage.text = data.message
// 화면 너비의 65%를 최대 폭으로 적용
binding.tvMessage.maxWidth = (itemView.resources.displayMetrics.widthPixels * 0.65f).toInt()
binding.tvTime.text = formatMessageTime(data.createdAt)
binding.tvTime.visibility = if (showTime) View.VISIBLE else View.INVISIBLE
binding.tvTime.visibility = if (showTime) View.VISIBLE else View.GONE
// 상태에 따른 시각적 피드백: 전송중은 약간 투명, 실패는 더 투명
val alpha = when (data.status) {
@@ -365,8 +381,9 @@ class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
) : RecyclerView.ViewHolder(binding.root) {
fun bind(data: ChatMessage, displayName: String?, isGrouped: Boolean, showTime: Boolean) {
binding.tvMessage.text = data.message
binding.tvMessage.maxWidth = (itemView.resources.displayMetrics.widthPixels * 0.65f).toInt()
binding.tvTime.text = formatMessageTime(data.createdAt)
binding.tvTime.visibility = if (showTime) View.VISIBLE else View.INVISIBLE
binding.tvTime.visibility = if (showTime) View.VISIBLE else View.GONE
// 그룹화: isGrouped가 true면 프로필/이름 숨김
binding.ivProfile.visibility = if (isGrouped) View.INVISIBLE else View.VISIBLE
@@ -438,7 +455,12 @@ class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val binding: ItemChatTypingIndicatorBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind() {
fun bind(name: String?, profileUrl: String?) {
// 이름/프로필 표시
binding.tvName.text = name ?: ""
binding.tvName.visibility = View.VISIBLE
binding.ivProfile.visibility = View.VISIBLE
loadProfileImage(binding.ivProfile, profileUrl)
startTypingAnimation()
}
@@ -492,7 +514,7 @@ class ChatMessageAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) {
super.onViewAttachedToWindow(holder)
if (holder is TypingIndicatorViewHolder) {
holder.bind()
holder.bind(typingName, typingProfileUrl)
}
}

View File

@@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.chat.talk.room
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.graphics.Rect
import android.text.Editable
import android.text.TextWatcher
import android.view.View
@@ -18,6 +19,7 @@ import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.chat.character.detail.CharacterType
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityChatRoomBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
@@ -85,6 +87,9 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
fun setCharacterInfo(info: CharacterInfo) {
characterInfo = info
bindHeader(info)
if (this::chatAdapter.isInitialized) {
chatAdapter.setTypingInfo(info.name, info.profileImageUrl)
}
}
/** 5.3: 헤더 바인딩 구현 (프로필, 이름, 타입 배지) */
@@ -122,6 +127,37 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
}
binding.rvMessages.layoutManager = layoutManager
binding.rvMessages.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.left = 13.3f.dpToPx().toInt()
outRect.right = 13.3f.dpToPx().toInt()
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.top = 0
outRect.bottom = 10f.dpToPx().toInt()
}
chatAdapter.itemCount - 1 -> {
outRect.top = 10f.dpToPx().toInt()
outRect.bottom = 0
}
else -> {
outRect.top = 10f.dpToPx().toInt()
outRect.bottom = 10f.dpToPx().toInt()
}
}
}
})
chatAdapter = ChatMessageAdapter().apply {
// RecyclerView에 연결하기 전에 안정 ID를 활성화해야 함
setHasStableIds(true)
@@ -133,6 +169,12 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
}
binding.rvMessages.adapter = chatAdapter
// 현재 보유 중인 캐릭터 프로필/이름을 타이핑 인디케이터에도 반영
chatAdapter.setTypingInfo(
characterInfo?.name,
characterInfo?.profileImageUrl
)
// 상단 도달 시 이전 메시지 로드
binding.rvMessages.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {