feat(chat): 채팅 탭 기본 layout을 추가한다

This commit is contained in:
2026-06-10 14:26:39 +09:00
parent 55fb032b22
commit ee703eb13a
7 changed files with 178 additions and 5 deletions

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/soda_400" />
</shape>

View File

@@ -1,5 +1,52 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/black" /> android:background="@color/black">
<include
android:id="@+id/view_chat_title_bar"
layout="@layout/view_title_bar_default"
android:layout_width="0dp"
android:layout_height="60dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<include
android:id="@+id/view_chat_filter_tabs"
layout="@layout/view_capsule_tab_bar"
android:layout_width="0dp"
android:layout_height="52dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/view_chat_title_bar" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_chat_rooms"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:paddingBottom="@dimen/spacing_48"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/view_chat_filter_tabs"
tools:listitem="@layout/item_v2_chat_room" />
<ImageView
android:id="@+id/btn_chat_floating"
android:layout_width="@dimen/spacing_48"
android:layout_height="@dimen/spacing_48"
android:layout_marginEnd="@dimen/spacing_20"
android:layout_marginBottom="@dimen/spacing_20"
android:background="@drawable/bg_chat_floating_button"
android:contentDescription="@string/screen_chat_floating_button"
android:scaleType="center"
android:src="@drawable/ic_plus_no_bg"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -24,9 +24,17 @@
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" /> android:layout_weight="1" />
<ImageView <LinearLayout
android:id="@+id/iv_title_bar_menu" android:id="@+id/ll_title_bar_actions"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:contentDescription="@null" /> android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/iv_title_bar_menu"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null" />
</LinearLayout>
</LinearLayout> </LinearLayout>

View File

@@ -157,6 +157,7 @@
<string name="screen_chat_filter_all">All</string> <string name="screen_chat_filter_all">All</string>
<string name="screen_chat_filter_ai">AI Chat</string> <string name="screen_chat_filter_ai">AI Chat</string>
<string name="screen_chat_filter_dm">DM</string> <string name="screen_chat_filter_dm">DM</string>
<string name="screen_chat_floating_button">New chat</string>
<string name="live_now">On Air</string> <string name="live_now">On Air</string>
<string name="screen_live_now_all_title">View all</string> <string name="screen_live_now_all_title">View all</string>
<string name="screen_live_now_all_empty_message">There are currently no live broadcasts available, or access may be restricted due to age limits.\nVerify your identity or follow the channel to get notified when a live broadcast starts.</string> <string name="screen_live_now_all_empty_message">There are currently no live broadcasts available, or access may be restricted due to age limits.\nVerify your identity or follow the channel to get notified when a live broadcast starts.</string>

View File

@@ -157,6 +157,7 @@
<string name="screen_chat_filter_all">すべて</string> <string name="screen_chat_filter_all">すべて</string>
<string name="screen_chat_filter_ai">AIチャット</string> <string name="screen_chat_filter_ai">AIチャット</string>
<string name="screen_chat_filter_dm">DM</string> <string name="screen_chat_filter_dm">DM</string>
<string name="screen_chat_floating_button">新しいチャット</string>
<string name="live_now">ただいま配信中</string> <string name="live_now">ただいま配信中</string>
<string name="screen_live_now_all_title">配信中のライブをすべて見る</string> <string name="screen_live_now_all_title">配信中のライブをすべて見る</string>
<string name="screen_live_now_all_empty_message">参加可能なライブがないか、年齢制限で入室できません。\n本人確認を行うか、チャンネルをフォローして\n配信通知を受け取ってみましょう。</string> <string name="screen_live_now_all_empty_message">参加可能なライブがないか、年齢制限で入室できません。\n本人確認を行うか、チャンネルをフォローして\n配信通知を受け取ってみましょう。</string>

View File

@@ -156,6 +156,7 @@
<string name="screen_chat_filter_all">전체</string> <string name="screen_chat_filter_all">전체</string>
<string name="screen_chat_filter_ai">AI 채팅</string> <string name="screen_chat_filter_ai">AI 채팅</string>
<string name="screen_chat_filter_dm">DM</string> <string name="screen_chat_filter_dm">DM</string>
<string name="screen_chat_floating_button">새 대화</string>
<string name="live_now">지금 라이브 중</string> <string name="live_now">지금 라이브 중</string>
<string name="screen_live_now_all_title">지금 라이브 중 전체보기</string> <string name="screen_live_now_all_title">지금 라이브 중 전체보기</string>
<string name="screen_live_now_all_empty_message">현재 참여 가능한 라이브 방송이 없거나\n연령제한으로 입장이 불가능합니다.\n본인인증을 해보거나 채널을 팔로잉하고\n라이브 방송 알림을 받아보세요.</string> <string name="screen_live_now_all_empty_message">현재 참여 가능한 라이브 방송이 없거나\n연령제한으로 입장이 불가능합니다.\n본인인증을 해보거나 채널을 팔로잉하고\n라이브 방송 알림을 받아보세요.</string>

View File

@@ -0,0 +1,110 @@
package kr.co.vividnext.sodalive.v2.main.chat
import android.app.Application
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ApplicationProvider
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.v2.widget.CapsuleTabBarView
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [28], application = Application::class)
class ChatMainFragmentLayoutTest {
@Test
fun `기본 title bar는 title과 가변 action container를 함께 제공한다`() {
val titleBar = inflateView(R.layout.view_title_bar_default) as LinearLayout
val title = requireNotNull(titleBar.findViewById<TextView>(R.id.tv_title_bar_title))
val actions = requireNotNull(titleBar.findViewById<LinearLayout>(R.id.ll_title_bar_actions))
val menu = requireNotNull(titleBar.findViewById<ImageView>(R.id.iv_title_bar_menu))
assertSame(actions, menu.parent)
assertSame(titleBar, title.parent)
assertSame(titleBar, actions.parent)
assertTrue(titleBar.indexOfChild(title) < titleBar.indexOfChild(actions))
assertEquals(LinearLayout.HORIZONTAL, actions.orientation)
assertEquals(ViewGroup.LayoutParams.WRAP_CONTENT, actions.layoutParams.width)
assertEquals(ViewGroup.LayoutParams.WRAP_CONTENT, actions.layoutParams.height)
}
@Test
fun `채팅 fragment layout은 title bar tab list floating button만 포함한다`() {
val root = inflateView(R.layout.fragment_v2_main_chat) as ConstraintLayout
val titleBar = requireNotNull(root.findViewById<View>(R.id.view_chat_title_bar))
val tabBar = requireNotNull(root.findViewById<CapsuleTabBarView>(R.id.view_chat_filter_tabs))
val recyclerView = requireNotNull(root.findViewById<RecyclerView>(R.id.rv_chat_rooms))
val floatingButton = requireNotNull(root.findViewById<ImageView>(R.id.btn_chat_floating))
assertEquals(Color.BLACK, (root.background as ColorDrawable).color)
assertSame(root, titleBar.parent)
assertSame(root, tabBar.parent)
assertSame(root, recyclerView.parent)
assertSame(root, floatingButton.parent)
assertFalse(root.containsClassName("com.google.android.material.bottomnavigation.BottomNavigationView"))
assertFalse(root.containsViewIdContaining("unread"))
}
@Test
fun `채팅 fragment list와 floating button은 계획된 constraint를 사용한다`() {
val root = inflateView(R.layout.fragment_v2_main_chat)
val titleBar = requireNotNull(root.findViewById<View>(R.id.view_chat_title_bar))
val tabBar = requireNotNull(root.findViewById<CapsuleTabBarView>(R.id.view_chat_filter_tabs))
val recyclerView = requireNotNull(root.findViewById<RecyclerView>(R.id.rv_chat_rooms))
val floatingButton = requireNotNull(root.findViewById<ImageView>(R.id.btn_chat_floating))
val tabParams = tabBar.layoutParams as ConstraintLayout.LayoutParams
val listParams = recyclerView.layoutParams as ConstraintLayout.LayoutParams
val buttonParams = floatingButton.layoutParams as ConstraintLayout.LayoutParams
assertEquals(60.dpToPx(), titleBar.layoutParams.height)
assertEquals(52.dpToPx(), tabBar.layoutParams.height)
assertEquals(R.id.view_chat_title_bar, tabParams.topToBottom)
assertEquals(R.id.view_chat_filter_tabs, listParams.topToBottom)
assertEquals(ConstraintLayout.LayoutParams.PARENT_ID, listParams.bottomToBottom)
assertEquals(false, recyclerView.clipToPadding)
assertTrue(recyclerView.paddingBottom >= 28.dpToPx())
assertEquals(ConstraintLayout.LayoutParams.PARENT_ID, buttonParams.endToEnd)
assertEquals(ConstraintLayout.LayoutParams.PARENT_ID, buttonParams.bottomToBottom)
}
private fun inflateView(layoutResId: Int): View {
val context = ApplicationProvider.getApplicationContext<Context>()
return LayoutInflater.from(context).inflate(layoutResId, null, false)
}
private fun View.containsViewIdContaining(idNamePart: String): Boolean {
if (id != View.NO_ID) {
val idName = runCatching { resources.getResourceEntryName(id) }.getOrNull()
if (idName != null && idName.contains(idNamePart, ignoreCase = true)) return true
}
if (this !is ViewGroup) return false
return (0 until childCount).any { getChildAt(it).containsViewIdContaining(idNamePart) }
}
private fun View.containsClassName(className: String): Boolean {
if (javaClass.name == className) return true
if (this !is ViewGroup) return false
return (0 until childCount).any { getChildAt(it).containsClassName(className) }
}
private fun Int.dpToPx(): Int {
val context = ApplicationProvider.getApplicationContext<Context>()
return (this * context.resources.displayMetrics.density).toInt()
}
}