feat(creator): 채널 홈 오디오 섹션을 재구성한다
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user