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) {

View File

@@ -2,7 +2,7 @@
<!-- AI 메시지용 배경: color_111111 10% 투명, 코너: 4dp 16dp 16dp 16dp (TL, TR, BR, BL) -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#1A111111" />
<solid android:color="#33111111" />
<corners
android:topLeftRadius="4dp"
android:topRightRadius="16dp"

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 입력창 배경: 기존 bg_round_corner_999_263238.xml 활용 -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/bg_round_corner_999_263238" />
</selector>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#263238" />
<corners android:radius="32dp" />
</shape>

View File

@@ -191,18 +191,22 @@
<EditText
android:id="@+id/et_message"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:background="@drawable/bg_chat_input"
android:fontFamily="@font/pretendard_regular"
android:hint="@string/chat_input_placeholder"
android:imeOptions="actionSend|flagNoEnterAction"
android:importantForAutofill="no"
android:inputType="textMultiLine"
android:maxLength="200"
android:maxLines="4"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:minHeight="48dp"
android:paddingHorizontal="16dp"
android:paddingVertical="8dp"
android:textColor="#FFFFFFFF"
android:textColorHint="#80FFFFFF"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/iv_send"
app:layout_constraintStart_toStartOf="parent"

View File

@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
<?xml version="1.0" encoding="utf-8"?><!--
AI 메시지 아이템 레이아웃 (3.3)
- 왼쪽 정렬된 메시지 버블
- 프로필 이미지, 이름(조건부), 메시지, 시간(오른쪽, 조건부)
@@ -9,11 +8,7 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="6dp"
android:paddingBottom="6dp">
android:layout_height="wrap_content">
<!-- 프로필 이미지 (그룹 첫 메시지에서만 보임) -->
<ImageView
@@ -21,36 +16,36 @@
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginEnd="8dp"
android:contentDescription="@string/a11y_ai_profile_image"
android:scaleType="centerCrop"
android:src="@drawable/ic_placeholder_profile"
android:contentDescription="@string/a11y_ai_profile_image"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
app:layout_constraintTop_toTopOf="@id/message_group" />
<!-- 메시지 그룹: 이름 + 메시지 버블 -->
<LinearLayout
android:id="@+id/message_group"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:orientation="vertical"
app:layout_constraintStart_toEndOf="@id/iv_profile"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@id/tv_time"
app:layout_constraintHorizontal_bias="0"
android:layout_marginEnd="8dp">
app:layout_constraintStart_toEndOf="@id/iv_profile"
app:layout_constraintTop_toTopOf="parent">
<!-- 보낸이 이름 (그룹의 첫 메시지에서만 보임) -->
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/white"
android:textSize="12sp"
android:includeFontPadding="false"
android:layout_marginBottom="2dp"
android:ellipsize="end"
android:fontFamily="@font/pretendard_regular"
android:includeFontPadding="false"
android:maxLines="1"
android:ellipsize="end" />
android:textColor="@android:color/white"
android:textSize="12sp" />
<!-- 메시지 버블 컨테이너 -->
<androidx.constraintlayout.widget.ConstraintLayout
@@ -58,27 +53,26 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_chat_ai_message"
android:paddingStart="14dp"
android:paddingEnd="14dp"
android:paddingTop="10dp"
android:paddingBottom="10dp">
android:paddingHorizontal="10dp"
android:paddingVertical="8dp">
<TextView
android:id="@+id/tv_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/white"
android:textSize="15sp"
android:lineSpacingExtra="2dp"
android:includeFontPadding="false"
android:hyphenationFrequency="normal"
android:breakStrategy="balanced"
android:ellipsize="end"
android:fontFamily="@font/pretendard_regular"
android:hyphenationFrequency="normal"
android:includeFontPadding="false"
android:lineSpacingExtra="2dp"
android:maxLines="1000"
app:layout_constraintTop_toTopOf="parent"
android:textColor="@android:color/white"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
@@ -87,12 +81,13 @@
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:fontFamily="@font/pretendard_regular"
android:includeFontPadding="false"
android:textColor="@color/color_777777"
android:textSize="12sp"
android:includeFontPadding="false"
app:layout_constraintBottom_toBottomOf="@id/message_group"
app:layout_constraintStart_toEndOf="@id/message_group"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginStart="8dp" />
app:layout_constraintStart_toEndOf="@id/message_group" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
<?xml version="1.0" encoding="utf-8"?><!--
타이핑 인디케이터 아이템 레이아웃 (3.4)
- AI 메시지 레이아웃과 동일한 좌측 정렬 구조
- 메시지 영역 대신 3개 점(•••) 애니메이션 표시
@@ -9,10 +8,6 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="6dp"
android:paddingBottom="6dp"
android:contentDescription="@string/a11y_typing">
<!-- 프로필 이미지 (AI) -->
@@ -21,35 +16,35 @@
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginEnd="8dp"
android:contentDescription="@string/a11y_ai_profile_image"
android:scaleType="centerCrop"
android:src="@drawable/ic_placeholder_profile"
android:contentDescription="@string/a11y_ai_profile_image"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
app:layout_constraintTop_toTopOf="@id/message_group" />
<!-- 이름 + 타이핑 버블 그룹 -->
<LinearLayout
android:id="@+id/message_group"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="56dp"
android:orientation="vertical"
app:layout_constraintStart_toEndOf="@id/iv_profile"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="56dp">
app:layout_constraintStart_toEndOf="@id/iv_profile"
app:layout_constraintTop_toTopOf="parent">
<!-- 보낸이 이름 (필요 시 표시) -->
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/white"
android:textSize="12sp"
android:includeFontPadding="false"
android:layout_marginBottom="2dp"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="1"
android:ellipsize="end" />
android:textColor="@android:color/white"
android:textSize="12sp" />
<!-- 타이핑 표시 컨테이너 (AI 버블 배경 재사용 가능) -->
<androidx.constraintlayout.widget.ConstraintLayout
@@ -57,10 +52,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_chat_ai_message"
android:paddingStart="14dp"
android:paddingEnd="14dp"
android:paddingTop="10dp"
android:paddingBottom="10dp">
android:paddingHorizontal="14dp"
android:paddingVertical="10dp">
<!-- 3개 점 애니메이션 -->
<LinearLayout
@@ -68,42 +61,42 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/dot1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="\u2022"
android:textSize="18sp"
android:textColor="@android:color/white"
android:animation="@anim/typing_dots_animation"
android:includeFontPadding="false"
android:animation="@anim/typing_dots_animation" />
android:text="\u2022"
android:textColor="@android:color/white"
android:textSize="18sp" />
<TextView
android:id="@+id/dot2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="\u2022"
android:textSize="18sp"
android:textColor="@android:color/white"
android:animation="@anim/typing_dots_animation"
android:includeFontPadding="false"
android:animation="@anim/typing_dots_animation" />
android:text="\u2022"
android:textColor="@android:color/white"
android:textSize="18sp" />
<TextView
android:id="@+id/dot3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="\u2022"
android:textSize="18sp"
android:textColor="@android:color/white"
android:animation="@anim/typing_dots_animation"
android:includeFontPadding="false"
android:animation="@anim/typing_dots_animation" />
android:text="\u2022"
android:textColor="@android:color/white"
android:textSize="18sp" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

View File

@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
<?xml version="1.0" encoding="utf-8"?><!--
사용자 메시지 아이템 레이아웃
- 오른쪽 정렬된 메시지 버블
- 버블의 왼쪽에 시간 표시
@@ -11,8 +10,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="6dp"
android:paddingEnd="12dp"
android:paddingBottom="6dp">
<!-- 전송 시간: 버블의 왼쪽에 표시 -->
@@ -20,13 +19,13 @@
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:fontFamily="@font/pretendard_regular"
android:includeFontPadding="false"
android:textColor="@color/color_777777"
android:textSize="12sp"
android:includeFontPadding="false"
app:layout_constraintBottom_toBottomOf="@id/message_container"
app:layout_constraintEnd_toStartOf="@id/message_container"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginEnd="8dp"/>
app:layout_constraintEnd_toStartOf="@id/message_container" />
<!-- 메시지 버블 컨테이너: 오른쪽 정렬 -->
<androidx.constraintlayout.widget.ConstraintLayout
@@ -34,30 +33,28 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_chat_user_message"
android:paddingStart="14dp"
android:paddingEnd="14dp"
android:paddingTop="10dp"
android:paddingBottom="10dp"
app:layout_constraintTop_toTopOf="parent"
android:paddingHorizontal="14dp"
android:paddingVertical="10dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/tv_time">
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/tv_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/white"
android:textSize="15sp"
android:lineSpacingExtra="2dp"
android:includeFontPadding="false"
android:hyphenationFrequency="normal"
android:breakStrategy="balanced"
android:ellipsize="end"
android:fontFamily="@font/pretendard_regular"
android:hyphenationFrequency="normal"
android:includeFontPadding="false"
android:lineSpacingExtra="2dp"
android:maxLines="1000"
app:layout_constraintTop_toTopOf="parent"
android:textColor="@android:color/white"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
app:layout_constraintTop_toTopOf="parent" />
<!-- 재전송 버튼: FAILED 상태일 때만 보임 -->
<ImageView
@@ -66,11 +63,11 @@
android:layout_height="20dp"
android:layout_marginStart="6dp"
android:layout_marginTop="-8dp"
android:src="@android:drawable/ic_popup_sync"
android:background="@android:color/transparent"
android:contentDescription="@string/action_retry"
android:src="@android:drawable/ic_popup_sync"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>