feat(creator): 채널 홈 Activity 진입점을 추가한다

This commit is contained in:
2026-06-15 13:20:11 +09:00
parent 402ea5e9c0
commit a631aa1b65
4 changed files with 490 additions and 1 deletions

View File

@@ -112,6 +112,7 @@
</activity> </activity>
<activity android:name=".main.MainActivity" /> <activity android:name=".main.MainActivity" />
<activity android:name=".v2.main.MainV2Activity" /> <activity android:name=".v2.main.MainV2Activity" />
<activity android:name=".v2.creator.channel.CreatorChannelHomeActivity" />
<activity <activity
android:name=".v2.main.chat.dm.DmChatRoomActivity" android:name=".v2.main.chat.dm.DmChatRoomActivity"
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" /> android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />

View File

@@ -31,6 +31,8 @@ abstract class BaseActivity<T : ViewBinding>(
lateinit var binding: T lateinit var binding: T
private set private set
protected open val shouldApplySystemBarTopInset: Boolean = true
val screenWidth: Int by lazy { val screenWidth: Int by lazy {
resources.displayMetrics.widthPixels resources.displayMetrics.widthPixels
} }
@@ -81,7 +83,7 @@ abstract class BaseActivity<T : ViewBinding>(
// 루트는 좌/우/하만 처리(상단은 Toolbar에 위임). IME가 등장하면 하단 패딩을 IME 높이까지 확장 // 루트는 좌/우/하만 처리(상단은 Toolbar에 위임). IME가 등장하면 하단 패딩을 IME 높이까지 확장
val left = max(systemBars.left, ime.left) val left = max(systemBars.left, ime.left)
val top = systemBars.top val top = if (shouldApplySystemBarTopInset) systemBars.top else 0
val right = max(systemBars.right, ime.right) val right = max(systemBars.right, ime.right)
val bottom = max(systemBars.bottom, ime.bottom) val bottom = max(systemBars.bottom, ime.bottom)
v.setPadding(left, top, right, bottom) v.setPadding(left, top, right, bottom)

View File

@@ -0,0 +1,261 @@
package kr.co.vividnext.sodalive.v2.creator.channel
import android.content.Context
import android.content.Intent
import android.graphics.Typeface
import android.view.Gravity
import android.view.View
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowCompat
import androidx.recyclerview.widget.LinearLayoutManager
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelHomeBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.loadUrl
import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.live.room.detail.LiveRoomDetailFragment
import kr.co.vividnext.sodalive.v2.common.CreatorActivityType
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelScheduleResponse
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHeaderUiModel
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHomeUiState
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelTab
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelTitleBarState
import kr.co.vividnext.sodalive.v2.creator.channel.ui.CreatorChannelHomeSectionAdapter
import kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivity
import org.koin.androidx.viewmodel.ext.android.viewModel
class CreatorChannelHomeActivity : BaseActivity<ActivityCreatorChannelHomeBinding>(
ActivityCreatorChannelHomeBinding::inflate
) {
private val viewModel: CreatorChannelHomeViewModel by viewModel()
private val sectionAdapter = CreatorChannelHomeSectionAdapter(::onScheduleClicked)
private var creatorId: Long = 0L
private var currentHeader: CreatorChannelHeaderUiModel? = null
override val shouldApplySystemBarTopInset: Boolean = false
override fun setupView() {
creatorId = intent.getLongExtra(EXTRA_CREATOR_ID, 0L)
if (creatorId <= 0L) {
finish()
return
}
setupRecyclerView()
setStatusBarIconAppearance()
setTitleBarTopInset()
setupClickListeners()
observeViewModel()
viewModel.loadHome(creatorId)
}
private fun setupRecyclerView() {
binding.rvHomeSections.layoutManager = LinearLayoutManager(this)
binding.rvHomeSections.adapter = sectionAdapter
}
private fun setupClickListeners() {
binding.ivBack.setOnClickListener { finish() }
binding.ivMore.setOnClickListener { onMoreClicked() }
binding.tvChatButton.setOnClickListener {
currentHeader?.characterId?.let { characterId -> viewModel.createChatRoom(characterId) }
}
binding.tvDmButton.setOnClickListener {
startActivity(DmChatRoomActivity.newIntentByCreatorId(this, creatorId))
}
}
private fun observeViewModel() {
viewModel.homeStateLiveData.observe(this) { state ->
when (state) {
is CreatorChannelHomeUiState.Content -> bindContent(state)
is CreatorChannelHomeUiState.Error -> Unit
CreatorChannelHomeUiState.Empty -> Unit
CreatorChannelHomeUiState.Loading -> Unit
}
}
viewModel.chatRoomIdLiveData.observe(this) { event ->
event.consume()?.let { chatRoomId ->
startActivity(ChatRoomActivity.newIntent(this, chatRoomId))
}
}
viewModel.toastLiveData.observe(this) { event ->
event.consume()?.let {
val message = it.message ?: it.resId?.let(::getString)
message?.let { text -> Toast.makeText(applicationContext, text, Toast.LENGTH_LONG).show() }
}
}
}
private fun bindContent(content: CreatorChannelHomeUiState.Content) {
currentHeader = content.header
bindHeader(content.header)
bindTitleBar(content.header)
bindTabs(content.tabs)
sectionAdapter.submitItems(content.sections)
}
private fun bindHeader(header: CreatorChannelHeaderUiModel) {
binding.tvNickname.text = header.nickname
binding.tvFollowerCount.text = getString(
R.string.creator_channel_follower_count,
header.followerCount.moneyFormat()
)
binding.ivHeaderImage.loadUrl(header.profileImageUrl) {
placeholder(R.drawable.ic_placeholder_profile)
error(R.drawable.ic_placeholder_profile)
}
updateActionButtonLayout(
isChatVisible = header.isAiChatAvailable && header.characterId != null,
isDmVisible = header.isDmAvailable
)
}
private fun bindTitleBar(header: CreatorChannelHeaderUiModel) {
val titleBarState = CreatorChannelTitleBarState.from(
isFollow = header.isFollow,
isNotify = header.isNotify,
isInProgress = false
)
binding.ivFollow.setImageResource(titleBarState.followIconResId)
binding.layoutFollowCapsule.isEnabled = titleBarState.isActionEnabled
binding.layoutFollowCapsule.setBackgroundResource(
if (header.isFollow) {
R.drawable.bg_creator_channel_following_capsule
} else {
R.drawable.bg_creator_channel_follow_capsule
}
)
binding.layoutFollowCapsule.updateLayoutParams<LinearLayout.LayoutParams> {
width = if (header.isFollow) 36.dpToPx().toInt() else LinearLayout.LayoutParams.WRAP_CONTENT
}
binding.tvFollowLabel.isVisible = !header.isFollow
titleBarState.bellIconResId?.let {
binding.ivBell.setImageResource(it)
binding.ivBell.visibility = View.VISIBLE
} ?: run {
binding.ivBell.visibility = View.GONE
}
}
private fun bindTabs(tabs: List<CreatorChannelTab>) {
binding.tabContainer.removeAllViews()
tabs.forEachIndexed { index, tab ->
binding.tabContainer.addView(createTabView(tab, isSelected = index == 0))
}
}
private fun createTabView(tab: CreatorChannelTab, isSelected: Boolean): LinearLayout {
val tabText = TextView(this).apply {
text = getString(tab.labelResId)
gravity = Gravity.CENTER
setTextColor(getColor(if (isSelected) R.color.white else R.color.gray_500))
setTypeface(null, Typeface.NORMAL)
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
0,
1f
)
}
tabText.textSize = 16f
val indicator = View(this).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
3.dpToPx().toInt()
)
}
indicator.setBackgroundColor(getColor(R.color.soda_400))
indicator.isVisible = isSelected
return LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.MATCH_PARENT
).apply {
width = 110.dpToPx().toInt()
}
addView(tabText)
addView(indicator)
}
}
private fun setTitleBarTopInset() {
val baseTitleBarHeight = 60.dpToPx().toInt()
ViewCompat.setOnApplyWindowInsetsListener(binding.titleBarContainer) { view, insets ->
val topInset = insets.getInsets(WindowInsetsCompat.Type.systemBars()).top
view.updatePadding(top = topInset)
view.updateLayoutParams {
height = baseTitleBarHeight + topInset
}
insets
}
}
private fun setStatusBarIconAppearance() {
WindowCompat.getInsetsController(window, binding.root).isAppearanceLightStatusBars = false
}
private fun updateActionButtonLayout(isChatVisible: Boolean, isDmVisible: Boolean) {
binding.tvChatButton.isVisible = isChatVisible
binding.tvDmButton.isVisible = isDmVisible
(binding.tvDmButton.layoutParams as LinearLayout.LayoutParams).apply {
marginStart = if (isChatVisible && isDmVisible) 6.dpToPx().toInt() else 0
}.also(binding.tvDmButton::setLayoutParams)
}
private fun onMoreClicked() {
Toast.makeText(applicationContext, getString(R.string.creator_channel_more_ready), Toast.LENGTH_SHORT).show()
}
private fun onScheduleClicked(schedule: CreatorChannelScheduleResponse) {
when (schedule.type) {
CreatorActivityType.Audio,
CreatorActivityType.LiveReplay -> startActivity(
Intent(this, AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, schedule.targetId)
}
)
CreatorActivityType.Live -> showLiveRoomDetail(schedule.targetId)
CreatorActivityType.Community -> Unit
}
}
private fun showLiveRoomDetail(roomId: Long) {
val detailFragment = LiveRoomDetailFragment(
roomId,
onClickParticipant = {},
onClickReservation = {},
onClickModify = {},
onClickStart = {},
onClickCancel = {}
)
if (detailFragment.isAdded) return
detailFragment.show(supportFragmentManager, detailFragment.tag)
}
companion object {
const val EXTRA_CREATOR_ID: String = "extra_creator_id"
fun newIntent(context: Context, creatorId: Long): Intent {
return Intent(context, CreatorChannelHomeActivity::class.java).apply {
putExtra(EXTRA_CREATOR_ID, creatorId)
}
}
}
}

View File

@@ -0,0 +1,225 @@
<?xml version="1.0" encoding="utf-8"?>
<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_height="match_parent"
android:background="@color/black">
<androidx.core.widget.NestedScrollView
android:id="@+id/nested_scroll_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:fillViewport="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<FrameLayout
android:id="@+id/header_container"
android:layout_width="match_parent"
android:layout_height="402dp">
<ImageView
android:id="@+id/iv_header_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/color_222222"
android:contentDescription="@null"
android:scaleType="centerCrop"
tools:src="@drawable/ic_placeholder_profile" />
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_creator_channel_header_gradient_bottom" />
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_creator_channel_header_gradient_top" />
<LinearLayout
android:id="@+id/header_text_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingHorizontal="@dimen/spacing_20"
android:paddingBottom="@dimen/spacing_14">
<TextView
android:id="@+id/tv_follower_count"
style="@style/Typography.Body2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
tools:text="팔로워 82명" />
<TextView
android:id="@+id/tv_nickname"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:fontFamily="@font/bold"
android:gravity="center"
android:maxLines="2"
android:textColor="@color/white"
android:textSize="32sp"
tools:text="크리에이터 이름" />
<LinearLayout
android:id="@+id/action_button_container"
android:layout_width="match_parent"
android:layout_height="64dp"
android:gravity="center"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_chat_button"
style="@style/Typography.Body2"
android:layout_width="wrap_content"
android:layout_height="44dp"
android:background="@drawable/bg_creator_channel_outline_button"
android:drawableStart="@drawable/ic_new_talk"
android:drawablePadding="@dimen/spacing_6"
android:gravity="center"
android:minWidth="108dp"
android:paddingHorizontal="@dimen/spacing_12"
android:text="@string/creator_channel_chat_button"
android:textColor="@color/white" />
<TextView
android:id="@+id/tv_dm_button"
style="@style/Typography.Body2"
android:layout_width="wrap_content"
android:layout_height="44dp"
android:layout_marginStart="@dimen/spacing_6"
android:background="@drawable/bg_creator_channel_outline_button"
android:drawableStart="@drawable/ic_new_dm"
android:drawablePadding="@dimen/spacing_6"
android:gravity="center"
android:minWidth="108dp"
android:paddingHorizontal="@dimen/spacing_12"
android:text="@string/creator_channel_dm_button"
android:textColor="@color/white" />
</LinearLayout>
</LinearLayout>
</FrameLayout>
<HorizontalScrollView
android:id="@+id/horizontal_tab_scroll_view"
android:layout_width="match_parent"
android:layout_height="52dp"
android:background="@color/black"
android:fillViewport="false"
android:overScrollMode="never"
android:scrollbars="none">
<LinearLayout
android:id="@+id/tab_container"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="@dimen/spacing_20" />
</HorizontalScrollView>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_home_sections"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:nestedScrollingEnabled="false"
android:paddingBottom="@dimen/spacing_32"
tools:itemCount="4"
tools:listitem="@layout/item_creator_channel_home_audio" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/title_bar_container"
android:layout_width="0dp"
android:layout_height="60dp"
android:background="@android:color/transparent"
android:paddingHorizontal="@dimen/spacing_14"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/iv_back"
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@string/a11y_back"
android:src="@drawable/ic_new_bar_back"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/title_action_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:id="@+id/layout_follow_capsule"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:background="@drawable/bg_creator_channel_follow_capsule"
android:gravity="center"
android:orientation="horizontal"
android:paddingHorizontal="@dimen/spacing_8"
android:paddingVertical="@dimen/spacing_8"
tools:background="@drawable/bg_creator_channel_following_capsule">
<ImageView
android:id="@+id/iv_follow"
android:layout_width="20dp"
android:layout_height="20dp"
android:contentDescription="@null"
tools:src="@drawable/ic_new_follow" />
<TextView
android:id="@+id/tv_follow_label"
style="@style/Typography.Body2"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:layout_marginStart="@dimen/spacing_6"
android:gravity="center"
android:text="@string/creator_channel_follow_button"
android:textColor="@color/white" />
</LinearLayout>
<ImageView
android:id="@+id/iv_bell"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="@dimen/spacing_14"
android:contentDescription="@null"
android:visibility="gone"
tools:src="@drawable/ic_bar_bell"
tools:visibility="visible" />
<ImageView
android:id="@+id/iv_more"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="@dimen/spacing_14"
android:contentDescription="@null"
android:src="@drawable/ic_new_more" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>