From dd7251f18bc42991f3ba511e854de5d79eb68b64 Mon Sep 17 00:00:00 2001 From: klaus Date: Fri, 15 Aug 2025 00:29:56 +0900 Subject: [PATCH] =?UTF-8?q?fix(chat-room):=20=EC=B1=84=ED=8C=85=20?= =?UTF-8?q?=EC=95=84=EC=9D=B4=ED=85=9C=20UI,=20=EB=A9=94=EC=8B=9C=EC=A7=80?= =?UTF-8?q?=20=EC=9E=85=EB=A0=A5=20=EC=B0=BD=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 채팅 아이템이 화면을 벗어나는 버그 수정 - 메시지 입력창 글자크기 14sp, rounded corner 32dp --- .../chat/talk/room/ChatMessageAdapter.kt | 32 ++++++++-- .../chat/talk/room/ChatRoomActivity.kt | 42 +++++++++++++ .../main/res/drawable/bg_chat_ai_message.xml | 2 +- app/src/main/res/drawable/bg_chat_input.xml | 8 +-- .../main/res/layout/activity_chat_room.xml | 10 ++- .../main/res/layout/item_chat_ai_message.xml | 57 ++++++++--------- .../res/layout/item_chat_typing_indicator.xml | 61 ++++++++----------- .../res/layout/item_chat_user_message.xml | 43 ++++++------- 8 files changed, 154 insertions(+), 101 deletions(-) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatMessageAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatMessageAdapter.kt index 54ac8c52..408f3987 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatMessageAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatMessageAdapter.kt @@ -35,6 +35,20 @@ sealed class ChatListItem { class ChatMessageAdapter : RecyclerView.Adapter() { + // 타이핑 인디케이터 표시용 정보(캐릭터 이름/프로필) + 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() { } is TypingIndicatorViewHolder -> { - holder.bind() + holder.bind(typingName, typingProfileUrl) } } } @@ -300,8 +314,10 @@ class ChatMessageAdapter : RecyclerView.Adapter() { ) : 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(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() { 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() { override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) { super.onViewAttachedToWindow(holder) if (holder is TypingIndicatorViewHolder) { - holder.bind() + holder.bind(typingName, typingProfileUrl) } } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomActivity.kt index 818eed33..215b19cf 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/room/ChatRoomActivity.kt @@ -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( @@ -85,6 +87,9 @@ class ChatRoomActivity : BaseActivity( 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( } 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( } 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) { diff --git a/app/src/main/res/drawable/bg_chat_ai_message.xml b/app/src/main/res/drawable/bg_chat_ai_message.xml index 2f70235e..3e8aa1c9 100644 --- a/app/src/main/res/drawable/bg_chat_ai_message.xml +++ b/app/src/main/res/drawable/bg_chat_ai_message.xml @@ -2,7 +2,7 @@ - + - - - - + + + + diff --git a/app/src/main/res/layout/activity_chat_room.xml b/app/src/main/res/layout/activity_chat_room.xml index 4ac224dd..e1bb24c9 100644 --- a/app/src/main/res/layout/activity_chat_room.xml +++ b/app/src/main/res/layout/activity_chat_room.xml @@ -191,18 +191,22 @@ - + app:layout_constraintTop_toTopOf="@id/message_group" /> + app:layout_constraintStart_toEndOf="@id/iv_profile" + app:layout_constraintTop_toTopOf="parent"> + android:textColor="@android:color/white" + android:textSize="12sp" /> + android:paddingHorizontal="10dp" + android:paddingVertical="8dp"> + app:layout_constraintTop_toTopOf="parent" /> @@ -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" /> diff --git a/app/src/main/res/layout/item_chat_typing_indicator.xml b/app/src/main/res/layout/item_chat_typing_indicator.xml index 5c3c3815..0047ed44 100644 --- a/app/src/main/res/layout/item_chat_typing_indicator.xml +++ b/app/src/main/res/layout/item_chat_typing_indicator.xml @@ -1,5 +1,4 @@ - - @@ -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" /> + app:layout_constraintStart_toEndOf="@id/iv_profile" + app:layout_constraintTop_toTopOf="parent"> + android:textColor="@android:color/white" + android:textSize="12sp" /> + android:paddingHorizontal="14dp" + android:paddingVertical="10dp"> + app:layout_constraintTop_toTopOf="parent"> + android:text="\u2022" + android:textColor="@android:color/white" + android:textSize="18sp" /> + android:text="\u2022" + android:textColor="@android:color/white" + android:textSize="18sp" /> + android:text="\u2022" + android:textColor="@android:color/white" + android:textSize="18sp" /> diff --git a/app/src/main/res/layout/item_chat_user_message.xml b/app/src/main/res/layout/item_chat_user_message.xml index 53899133..3e83ee8d 100644 --- a/app/src/main/res/layout/item_chat_user_message.xml +++ b/app/src/main/res/layout/item_chat_user_message.xml @@ -1,5 +1,4 @@ - - @@ -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" /> + app:layout_constraintTop_toTopOf="parent"> + app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" />