feat(content): 전체 탭 화면 연결을 추가한다
This commit is contained in:
@@ -2,9 +2,15 @@ package kr.co.vividnext.sodalive.v2.main.content
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
|
||||
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
|
||||
@@ -14,7 +20,13 @@ import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.common.ToastMessage
|
||||
import kr.co.vividnext.sodalive.databinding.FragmentV2MainContentBinding
|
||||
import kr.co.vividnext.sodalive.databinding.ViewSectionTitleBinding
|
||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||
import kr.co.vividnext.sodalive.home.SeriesPublishedDaysOfWeek
|
||||
import kr.co.vividnext.sodalive.v2.common.data.ContentSort
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.model.toLabelResId
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.ui.CreatorChannelSortPopup
|
||||
import kr.co.vividnext.sodalive.v2.main.content.data.AudioRankingType
|
||||
import kr.co.vividnext.sodalive.v2.main.content.data.MainContentAllType
|
||||
import kr.co.vividnext.sodalive.v2.main.content.model.AudioRankingsUiState
|
||||
import kr.co.vividnext.sodalive.v2.main.content.model.AudioRecommendationsUiState
|
||||
import kr.co.vividnext.sodalive.v2.main.content.model.ContentAudioCardSection
|
||||
@@ -25,9 +37,17 @@ import kr.co.vividnext.sodalive.v2.main.content.model.ContentCommentedAudioSecti
|
||||
import kr.co.vividnext.sodalive.v2.main.content.model.ContentCommentedAudioUiModel
|
||||
import kr.co.vividnext.sodalive.v2.main.content.model.ContentOriginalSeriesSection
|
||||
import kr.co.vividnext.sodalive.v2.main.content.model.ContentOriginalSeriesUiModel
|
||||
import kr.co.vividnext.sodalive.v2.main.content.model.MainContentAllTabUiState
|
||||
import kr.co.vividnext.sodalive.v2.main.content.model.contentAllDayOfWeekOptions
|
||||
import kr.co.vividnext.sodalive.v2.main.content.model.toContentAllDayLabelResId
|
||||
import kr.co.vividnext.sodalive.v2.main.content.model.toContentAllTypeLabelResId
|
||||
import kr.co.vividnext.sodalive.v2.main.content.model.toContentBannerIntent
|
||||
import kr.co.vividnext.sodalive.v2.main.content.model.toContentBannerRoute
|
||||
import kr.co.vividnext.sodalive.v2.main.content.model.usesDayOfWeekQuery
|
||||
import kr.co.vividnext.sodalive.v2.main.content.model.usesSeriesItems
|
||||
import kr.co.vividnext.sodalive.v2.main.content.ui.CONTENT_RECOMMENDED_GRID_SPAN_COUNT
|
||||
import kr.co.vividnext.sodalive.v2.main.content.ui.ContentAllAudioCardAdapter
|
||||
import kr.co.vividnext.sodalive.v2.main.content.ui.ContentAllSeriesCardAdapter
|
||||
import kr.co.vividnext.sodalive.v2.main.content.ui.ContentAudioCardAdapter
|
||||
import kr.co.vividnext.sodalive.v2.main.content.ui.ContentBannerBinder
|
||||
import kr.co.vividnext.sodalive.v2.main.content.ui.ContentCommentedAudioAdapter
|
||||
@@ -45,7 +65,15 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
||||
) {
|
||||
private val contentMainViewModel: ContentMainViewModel by viewModel()
|
||||
private val contentRankingViewModel: ContentRankingViewModel by viewModel()
|
||||
private val contentAllTabViewModel: ContentAllTabViewModel by viewModel()
|
||||
private val loadingDialog: LoadingDialog by lazy { LoadingDialog(requireActivity(), layoutInflater) }
|
||||
private val contentAllTypes = listOf(
|
||||
MainContentAllType.AUDIO,
|
||||
MainContentAllType.SERIES,
|
||||
MainContentAllType.ORIGINAL,
|
||||
MainContentAllType.FREE,
|
||||
MainContentAllType.POINT
|
||||
)
|
||||
private val originalSeriesAdapter = ContentOriginalSeriesAdapter { openSeriesDetail(it) }
|
||||
private val latestAudioAdapter = ContentAudioCardAdapter(AudioContentCardSize.Medium) { openAudioContentDetail(it) }
|
||||
private val newAndHotAdapter = ContentNewAndHotAdapter { openAudioContentDetail(it) }
|
||||
@@ -54,15 +82,27 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
||||
private val commentedAudioAdapter = ContentCommentedAudioAdapter { openAudioContentDetail(it) }
|
||||
private val recommendedAudioAdapter = ContentAudioCardAdapter(AudioContentCardSize.Large) { openAudioContentDetail(it) }
|
||||
private val contentRankingAdapter = ContentRankingAdapter { openRankingAudioContentDetail(it) }
|
||||
private val contentAllAudioCardAdapter = ContentAllAudioCardAdapter { audioContentId ->
|
||||
openAudioContentDetail(audioContentId)
|
||||
}
|
||||
private val contentAllSeriesCardAdapter = ContentAllSeriesCardAdapter { seriesId -> openSeriesDetail(seriesId) }
|
||||
private var bannerBinder: ContentBannerBinder? = null
|
||||
private var sortPopup: CreatorChannelSortPopup? = null
|
||||
private var isRecommendationLoading = false
|
||||
private var isRankingLoading = false
|
||||
private var isAllTabLoading = false
|
||||
private var hasSelectedRankingTab = false
|
||||
private var hasSelectedAllTab = false
|
||||
private var currentContentTab = CONTENT_TAB_RECOMMENDATION
|
||||
private var currentAllTabState: MainContentAllTabUiState? = null
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setUpTextTabs()
|
||||
setUpRankingTypeTabs()
|
||||
setUpAllTypeTabs()
|
||||
setUpAllDayFilter()
|
||||
setUpAllSortButton()
|
||||
showContentTab(CONTENT_TAB_RECOMMENDATION)
|
||||
setUpSectionTitles()
|
||||
setUpAdapters()
|
||||
@@ -70,6 +110,14 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
||||
contentMainViewModel.loadRecommendations()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
sortPopup?.dismiss()
|
||||
sortPopup = null
|
||||
bannerBinder = null
|
||||
binding.rvContentAllItems.adapter = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun setUpTextTabs() {
|
||||
binding.textTabBarContent.root.setMenus(
|
||||
listOf(
|
||||
@@ -94,11 +142,56 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
||||
}
|
||||
}
|
||||
|
||||
private fun setUpAllTypeTabs() {
|
||||
binding.viewContentAllTypeTabs.root.setMenus(
|
||||
contentAllTypes.map { type -> getString(type.toContentAllTypeLabelResId()) },
|
||||
selectedIndex = MainContentAllType.AUDIO.ordinal
|
||||
)
|
||||
binding.viewContentAllTypeTabs.root.setOnTabSelectedListener { index ->
|
||||
contentAllTabViewModel.changeType(contentAllTypes[index])
|
||||
}
|
||||
}
|
||||
|
||||
private fun setUpAllDayFilter() {
|
||||
binding.layoutContentAllDayFilter.removeAllViews()
|
||||
contentAllDayOfWeekOptions.forEach { dayOfWeek ->
|
||||
binding.layoutContentAllDayFilter.addView(createAllDayFilterView(dayOfWeek))
|
||||
}
|
||||
}
|
||||
|
||||
private fun createAllDayFilterView(dayOfWeek: SeriesPublishedDaysOfWeek): TextView {
|
||||
return TextView(requireContext()).apply {
|
||||
tag = dayOfWeek
|
||||
gravity = Gravity.CENTER
|
||||
setText(dayOfWeek.toContentAllDayLabelResId())
|
||||
setTextAppearance(R.style.Typography_Body5)
|
||||
setPadding(
|
||||
resources.getDimensionPixelSize(R.dimen.spacing_8),
|
||||
0,
|
||||
resources.getDimensionPixelSize(R.dimen.spacing_8),
|
||||
0
|
||||
)
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
setOnClickListener { contentAllTabViewModel.changeDayOfWeek(dayOfWeek) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun setUpAllSortButton() {
|
||||
binding.layoutContentAllSortButton.setOnClickListener {
|
||||
val state = currentAllTabState ?: return@setOnClickListener
|
||||
showAllSortPopup(state.selectedSort)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showContentTab(index: Int) {
|
||||
currentContentTab = index
|
||||
when (index) {
|
||||
CONTENT_TAB_RECOMMENDATION -> showRecommendationContent()
|
||||
CONTENT_TAB_RANKING -> showRankingContent()
|
||||
CONTENT_TAB_ALL -> hideContentSurfaces()
|
||||
CONTENT_TAB_ALL -> showAllContent()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,22 +199,37 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
||||
binding.nsvContentRecommendationContent.visibility = View.VISIBLE
|
||||
binding.viewContentRankingTypeTabs.root.visibility = View.GONE
|
||||
binding.rvContentRankings.visibility = View.GONE
|
||||
binding.layoutContentAllSurface.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun showRankingContent() {
|
||||
binding.nsvContentRecommendationContent.visibility = View.GONE
|
||||
binding.viewContentRankingTypeTabs.root.visibility = View.VISIBLE
|
||||
binding.rvContentRankings.visibility = View.VISIBLE
|
||||
binding.layoutContentAllSurface.visibility = View.GONE
|
||||
if (!hasSelectedRankingTab) {
|
||||
hasSelectedRankingTab = true
|
||||
contentRankingViewModel.loadRankings(AudioRankingType.WEEKLY_POPULAR)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showAllContent() {
|
||||
binding.nsvContentRecommendationContent.visibility = View.GONE
|
||||
binding.viewContentRankingTypeTabs.root.visibility = View.GONE
|
||||
binding.rvContentRankings.visibility = View.GONE
|
||||
binding.layoutContentAllSurface.visibility = View.VISIBLE
|
||||
currentAllTabState?.let(::renderAllTabState)
|
||||
if (currentAllTabState == null && !hasSelectedAllTab) {
|
||||
hasSelectedAllTab = true
|
||||
contentAllTabViewModel.loadInitial()
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideContentSurfaces() {
|
||||
binding.nsvContentRecommendationContent.visibility = View.GONE
|
||||
binding.viewContentRankingTypeTabs.root.visibility = View.GONE
|
||||
binding.rvContentRankings.visibility = View.GONE
|
||||
binding.layoutContentAllSurface.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun setUpAdapters() {
|
||||
@@ -167,6 +275,24 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
||||
layoutManager = ContentRankingAdapter.createGridLayoutManager(requireContext())
|
||||
adapter = contentRankingAdapter
|
||||
}
|
||||
val contentAllGridLayoutManager = GridLayoutManager(requireContext(), CONTENT_ALL_GRID_SPAN_COUNT)
|
||||
binding.rvContentAllItems.apply {
|
||||
layoutManager = contentAllGridLayoutManager
|
||||
adapter = contentAllAudioCardAdapter
|
||||
addContentGridItemSpacing(CONTENT_ALL_GRID_SPAN_COUNT)
|
||||
addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
if (dy <= 0) return
|
||||
|
||||
val itemCount = recyclerView.adapter?.itemCount ?: return
|
||||
val lastVisiblePosition = contentAllGridLayoutManager.findLastVisibleItemPosition()
|
||||
if (lastVisiblePosition >= itemCount - CONTENT_ALL_GRID_SPAN_COUNT) {
|
||||
contentAllTabViewModel.loadMore()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindObservers() {
|
||||
@@ -202,6 +328,116 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
||||
contentRankingViewModel.toastLiveData.observe(viewLifecycleOwner) { toastMessage ->
|
||||
toastMessage?.let(::showToast)
|
||||
}
|
||||
contentAllTabViewModel.allTabStateLiveData.observe(viewLifecycleOwner) { state ->
|
||||
currentAllTabState = state
|
||||
renderAllTabState(state)
|
||||
}
|
||||
contentAllTabViewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
|
||||
isAllTabLoading = isLoading
|
||||
updateLoadingDialog()
|
||||
}
|
||||
contentAllTabViewModel.toastLiveData.observe(viewLifecycleOwner) { toastMessage ->
|
||||
toastMessage?.let(::showToast)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderAllTabState(state: MainContentAllTabUiState) {
|
||||
if (currentContentTab == CONTENT_TAB_ALL) {
|
||||
when (state) {
|
||||
is MainContentAllTabUiState.Content -> bindAllTabContent(state)
|
||||
is MainContentAllTabUiState.Empty -> bindAllTabEmpty(state)
|
||||
is MainContentAllTabUiState.Error -> bindAllTabError(state)
|
||||
is MainContentAllTabUiState.Loading -> bindAllTabLoading(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindAllTabContent(state: MainContentAllTabUiState.Content) {
|
||||
bindAllTabControls(state)
|
||||
binding.layoutContentAllSurface.visibility = View.VISIBLE
|
||||
hideAllTabEmptyError()
|
||||
if (state.selectedType.usesSeriesItems()) {
|
||||
binding.rvContentAllItems.adapter = contentAllSeriesCardAdapter
|
||||
contentAllAudioCardAdapter.submitItems(emptyList())
|
||||
contentAllSeriesCardAdapter.submitItems(state.seriesItems)
|
||||
} else {
|
||||
binding.rvContentAllItems.adapter = contentAllAudioCardAdapter
|
||||
contentAllSeriesCardAdapter.submitItems(emptyList())
|
||||
contentAllAudioCardAdapter.submitItems(state.audioItems)
|
||||
}
|
||||
state.paginationErrorMessage?.let { message ->
|
||||
showToast(message)
|
||||
contentAllTabViewModel.consumePaginationErrorMessage()
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindAllTabEmpty(state: MainContentAllTabUiState.Empty) {
|
||||
bindAllTabControls(state)
|
||||
binding.layoutContentAllSurface.visibility = View.VISIBLE
|
||||
binding.layoutContentAllEmptyError.visibility = View.VISIBLE
|
||||
binding.tvContentAllEmptyError.setText(R.string.screen_content_all_empty)
|
||||
clearAllTabItems()
|
||||
}
|
||||
|
||||
private fun bindAllTabError(state: MainContentAllTabUiState.Error) {
|
||||
bindAllTabControls(state)
|
||||
binding.layoutContentAllSurface.visibility = View.VISIBLE
|
||||
binding.layoutContentAllEmptyError.visibility = View.VISIBLE
|
||||
binding.tvContentAllEmptyError.text = state.message ?: getString(R.string.common_error_unknown)
|
||||
clearAllTabItems()
|
||||
}
|
||||
|
||||
private fun bindAllTabLoading(state: MainContentAllTabUiState.Loading) {
|
||||
bindAllTabControls(state)
|
||||
binding.layoutContentAllSurface.visibility = View.VISIBLE
|
||||
hideAllTabEmptyError()
|
||||
clearAllTabItems()
|
||||
}
|
||||
|
||||
private fun bindAllTabControls(state: MainContentAllTabUiState) {
|
||||
binding.tvContentAllTotalCount.text = state.totalCount.moneyFormat()
|
||||
binding.tvContentAllSortLabel.setText(state.selectedSort.toLabelResId())
|
||||
binding.viewContentAllTypeTabs.root.setMenus(
|
||||
contentAllTypes.map { type -> getString(type.toContentAllTypeLabelResId()) },
|
||||
selectedIndex = contentAllTypes.indexOf(state.selectedType).coerceAtLeast(0)
|
||||
)
|
||||
binding.layoutContentAllDayFilter.visibility = if (state.selectedType.usesDayOfWeekQuery()) View.VISIBLE else View.GONE
|
||||
bindAllDayFilterSelection(state.selectedDayOfWeek)
|
||||
}
|
||||
|
||||
private fun bindAllDayFilterSelection(selectedDayOfWeek: SeriesPublishedDaysOfWeek?) {
|
||||
for (index in 0 until binding.layoutContentAllDayFilter.childCount) {
|
||||
val child = binding.layoutContentAllDayFilter.getChildAt(index) as? TextView ?: continue
|
||||
val isSelected = child.tag == selectedDayOfWeek
|
||||
child.isSelected = isSelected
|
||||
child.setBackgroundResource(if (isSelected) R.drawable.bg_content_all_day_selected else 0)
|
||||
child.setTextColor(
|
||||
ContextCompat.getColor(requireContext(), if (isSelected) R.color.black else R.color.white)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearAllTabItems() {
|
||||
binding.rvContentAllItems.adapter = null
|
||||
contentAllAudioCardAdapter.submitItems(emptyList())
|
||||
contentAllSeriesCardAdapter.submitItems(emptyList())
|
||||
}
|
||||
|
||||
private fun hideAllTabEmptyError() {
|
||||
binding.layoutContentAllEmptyError.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun showAllSortPopup(selectedSort: ContentSort) {
|
||||
sortPopup?.dismiss()
|
||||
sortPopup = CreatorChannelSortPopup(
|
||||
anchor = binding.layoutContentAllSortButton,
|
||||
selectedSort = selectedSort,
|
||||
onSortSelected = { sort ->
|
||||
if (sort in ContentSort.entries) {
|
||||
contentAllTabViewModel.changeSort(sort)
|
||||
}
|
||||
}
|
||||
).also { it.show() }
|
||||
}
|
||||
|
||||
private fun bindContent(content: AudioRecommendationsUiState.Content) {
|
||||
@@ -299,6 +535,11 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
||||
|
||||
private fun openSeriesDetail(item: ContentOriginalSeriesUiModel) {
|
||||
val seriesId = item.seriesId.takeIf { it > 0L } ?: return
|
||||
openSeriesDetail(seriesId)
|
||||
}
|
||||
|
||||
private fun openSeriesDetail(seriesId: Long) {
|
||||
if (seriesId <= 0L) return
|
||||
startActivity(
|
||||
Intent(requireContext(), SeriesDetailActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_SERIES_ID, seriesId)
|
||||
@@ -312,7 +553,7 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
||||
}
|
||||
|
||||
private fun updateLoadingDialog() {
|
||||
if (isRecommendationLoading || isRankingLoading) {
|
||||
if (isRecommendationLoading || isRankingLoading || isAllTabLoading) {
|
||||
loadingDialog.show(screenWidth)
|
||||
} else {
|
||||
loadingDialog.dismiss()
|
||||
@@ -347,5 +588,6 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
||||
private const val CONTENT_TAB_RECOMMENDATION = 0
|
||||
private const val CONTENT_TAB_RANKING = 1
|
||||
private const val CONTENT_TAB_ALL = 2
|
||||
private const val CONTENT_ALL_GRID_SPAN_COUNT = 3
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user