feat(creator): 채널 홈 오디오 섹션을 재구성한다

This commit is contained in:
2026-06-15 15:01:41 +09:00
parent edb7d98de7
commit 5fde0bc469
4 changed files with 182 additions and 37 deletions

View File

@@ -26,6 +26,7 @@ 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.CreatorChannelAudioContentResponse
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
@@ -40,7 +41,7 @@ class CreatorChannelHomeActivity : BaseActivity<ActivityCreatorChannelHomeBindin
) {
private val viewModel: CreatorChannelHomeViewModel by viewModel()
private val sectionAdapter = CreatorChannelHomeSectionAdapter(::onScheduleClicked)
private val sectionAdapter = CreatorChannelHomeSectionAdapter(::onScheduleClicked, ::onAudioContentClicked)
private var creatorId: Long = 0L
private var currentHeader: CreatorChannelHeaderUiModel? = null
@@ -235,6 +236,14 @@ class CreatorChannelHomeActivity : BaseActivity<ActivityCreatorChannelHomeBindin
}
}
private fun onAudioContentClicked(audioContent: CreatorChannelAudioContentResponse) {
startActivity(
Intent(this, AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, audioContent.audioContentId)
}
)
}
private fun showLiveRoomDetail(roomId: Long) {
val detailFragment = LiveRoomDetailFragment(
roomId,

View File

@@ -14,6 +14,7 @@ import androidx.annotation.LayoutRes
import androidx.annotation.StringRes
import androidx.core.net.toUri
import androidx.core.view.isVisible
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R
import coil.transform.CircleCropTransformation
@@ -28,7 +29,8 @@ import java.util.TimeZone
import kotlin.math.roundToInt
class CreatorChannelHomeSectionAdapter(
private val onScheduleClick: (CreatorChannelScheduleResponse) -> Unit = {}
private val onScheduleClick: (CreatorChannelScheduleResponse) -> Unit = {},
private val onAudioContentClick: (CreatorChannelAudioContentResponse) -> Unit = {}
) : RecyclerView.Adapter<CreatorChannelHomeSectionAdapter.SectionViewHolder>() {
private var items: List<CreatorChannelHomeSection> = emptyList()
@@ -42,7 +44,7 @@ class CreatorChannelHomeSectionAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SectionViewHolder {
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
return SectionViewHolder(view, onScheduleClick)
return SectionViewHolder(view, onScheduleClick, onAudioContentClick)
}
override fun onBindViewHolder(holder: SectionViewHolder, position: Int) {
@@ -53,7 +55,8 @@ class CreatorChannelHomeSectionAdapter(
class SectionViewHolder(
view: View,
private val onScheduleClick: (CreatorChannelScheduleResponse) -> Unit
private val onScheduleClick: (CreatorChannelScheduleResponse) -> Unit,
private val onAudioContentClick: (CreatorChannelAudioContentResponse) -> Unit
) : RecyclerView.ViewHolder(view) {
private val title: TextView? = view.findViewById(R.id.tv_section_title)
private val sectionItems: LinearLayout? = view.findViewById(R.id.ll_section_items)
@@ -70,6 +73,15 @@ class CreatorChannelHomeSectionAdapter(
private val noticeItems: LinearLayout? = view.findViewById(R.id.ll_notice_items)
private val scheduleTimeline: LinearLayout? = view.findViewById(R.id.ll_schedule_timeline)
private val scheduleItems: LinearLayout? = view.findViewById(R.id.ll_schedule_items)
private val audioContentsRecyclerView: RecyclerView? = view.findViewById(R.id.rv_audio_contents)
private val audioContentGridAdapter = AudioContentGridAdapter(
itemWidth = calculateCreatorChannelAudioItemWidthDp(itemView.resources.configuration.screenWidthDp).dp(),
onAudioContentClick = onAudioContentClick
)
init {
setupAudioContentsRecyclerView()
}
fun bind(item: CreatorChannelHomeSection) {
title?.setText(item.titleResId)
@@ -225,11 +237,22 @@ class CreatorChannelHomeSectionAdapter(
}
private fun bindAudioContents(item: CreatorChannelHomeSection.AudioContents) {
val row = createHorizontalRow()
item.audioContents.forEach { audioContent ->
row.addView(createAudioTile(audioContent))
val visibleAudioContents = item.audioContents.take(MAX_AUDIO_ITEM_COUNT)
audioContentGridAdapter.submitItems(visibleAudioContents)
}
private fun setupAudioContentsRecyclerView() {
audioContentsRecyclerView?.apply {
if (layoutManager == null) {
layoutManager = GridLayoutManager(itemView.context, AUDIO_GRID_SPAN_COUNT, RecyclerView.HORIZONTAL, false)
}
if (adapter == null) {
adapter = audioContentGridAdapter
}
if (itemDecorationCount == 0) {
addItemDecoration(AudioContentGridSpacingDecoration(horizontalSpacing = 8.dp(), verticalSpacing = 8.dp()))
}
}
sectionItems?.addView(createHorizontalScrollRow(row))
}
private fun bindSeries(item: CreatorChannelHomeSection.Series) {
@@ -335,15 +358,6 @@ class CreatorChannelHomeSectionAdapter(
sectionItems?.addView(row)
}
private fun createAudioTile(audioContent: CreatorChannelAudioContentResponse): LinearLayout =
createContentTile(
title = audioContent.title,
body = listOfNotNull(audioContent.seriesName, audioContent.duration).joinToText(),
imageUrl = audioContent.imageUrl,
imageWidth = 122.dp(),
imageHeight = 122.dp()
)
private fun addTextCard(title: String, body: String, imageUrl: String?) {
val card = LinearLayout(itemView.context).apply {
orientation = LinearLayout.HORIZONTAL
@@ -504,6 +518,67 @@ class CreatorChannelHomeSectionAdapter(
private fun Int.dp(): Int = (this * itemView.resources.displayMetrics.density).toInt()
}
private class AudioContentGridAdapter(
private val itemWidth: Int,
private val onAudioContentClick: (CreatorChannelAudioContentResponse) -> Unit
) : RecyclerView.Adapter<AudioContentGridAdapter.AudioContentViewHolder>() {
private var items: List<CreatorChannelAudioContentResponse> = emptyList()
fun submitItems(items: List<CreatorChannelAudioContentResponse>) {
this.items = items
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AudioContentViewHolder {
val view = LayoutInflater.from(parent.context).inflate(
R.layout.item_creator_channel_home_audio_content,
parent,
false
)
view.layoutParams = RecyclerView.LayoutParams(itemWidth, RecyclerView.LayoutParams.WRAP_CONTENT)
return AudioContentViewHolder(view, onAudioContentClick)
}
override fun onBindViewHolder(holder: AudioContentViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount(): Int = items.size
class AudioContentViewHolder(
view: View,
private val onAudioContentClick: (CreatorChannelAudioContentResponse) -> Unit
) : RecyclerView.ViewHolder(view) {
private val card: CreatorChannelHomeAudioContentCardView = view as CreatorChannelHomeAudioContentCardView
fun bind(audioContent: CreatorChannelAudioContentResponse) {
card.bind(audioContent)
card.setOnClickListener { onAudioContentClick(audioContent) }
}
}
}
private class AudioContentGridSpacingDecoration(
private val horizontalSpacing: Int,
private val verticalSpacing: Int
) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: android.graphics.Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val position = parent.getChildAdapterPosition(view)
if (position == RecyclerView.NO_POSITION) return
val itemCount = parent.adapter?.itemCount ?: return
val lastColumnStartPosition = ((itemCount - 1) / AUDIO_GRID_SPAN_COUNT) * AUDIO_GRID_SPAN_COUNT
outRect.right = if (position >= lastColumnStartPosition) 0 else horizontalSpacing
outRect.bottom = if (position % AUDIO_GRID_SPAN_COUNT == AUDIO_GRID_SPAN_COUNT - 1) 0 else verticalSpacing
}
}
private companion object {
@get:LayoutRes
val CreatorChannelHomeSection.layoutResId: Int
@@ -542,6 +617,8 @@ class CreatorChannelHomeSectionAdapter(
private const val MAX_DONATION_ITEM_COUNT = 8
private const val MAX_NOTICE_ITEM_COUNT = 3
private const val MAX_SCHEDULE_ITEM_COUNT = 3
private const val MAX_AUDIO_ITEM_COUNT = 9
private const val AUDIO_GRID_SPAN_COUNT = 3
fun List<String>.joinToText(): String = filter(String::isNotBlank).joinToString(separator = " · ")
}
@@ -613,4 +690,13 @@ internal fun calculateCreatorChannelNoticeCardWidthDp(screenWidthDp: Int): Int {
}
}
internal fun calculateCreatorChannelAudioItemWidthDp(screenWidthDp: Int): Int {
val width = screenWidthDp.takeIf { it > 0 } ?: 402
return if (width >= 402) {
346
} else {
(346f * width / 402f).roundToInt()
}
}
internal fun calculateCreatorChannelScheduleTimelineLineCount(scheduleCount: Int): Int = (scheduleCount - 1).coerceAtLeast(0)

View File

@@ -1,25 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="@dimen/spacing_20"
android:paddingTop="@dimen/spacing_20">
<TextView
android:id="@+id/tv_section_title"
style="@style/Typography.Heading3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/white"
tools:text="섹션" />
<include layout="@layout/view_section_title" />
<LinearLayout
android:id="@+id/ll_section_items"
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_audio_contents"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_8"
android:orientation="vertical" />
android:layout_marginTop="@dimen/spacing_14"
android:clipToPadding="false"
android:overScrollMode="never"
android:paddingHorizontal="@dimen/spacing_14"
android:scrollbars="none" />
</LinearLayout>