feat(content): 전체보기 화면 UI를 추가한다
This commit is contained in:
@@ -0,0 +1,153 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.main.content.overview
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.core.view.doOnLayout
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
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.base.BaseActivity
|
||||||
|
import kr.co.vividnext.sodalive.common.Constants
|
||||||
|
import kr.co.vividnext.sodalive.databinding.ActivityContentOverviewBinding
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.overview.data.ContentOverviewType
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.overview.model.ContentOverviewUiState
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.overview.model.toTitleResId
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.overview.ui.ContentOverviewAdapter
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.ui.addContentGridItemSpacing
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.ui.calculateContentGridItemWidthPx
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
||||||
|
class ContentOverviewActivity : BaseActivity<ActivityContentOverviewBinding>(
|
||||||
|
ActivityContentOverviewBinding::inflate
|
||||||
|
) {
|
||||||
|
private val viewModel: ContentOverviewViewModel by viewModel()
|
||||||
|
private val overviewAdapter = ContentOverviewAdapter(::openAudioContentDetail)
|
||||||
|
private val overviewType: ContentOverviewType by lazy { intent.contentOverviewType() }
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
viewModel.loadFirstPage(overviewType)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setupView() {
|
||||||
|
setupTitleBar()
|
||||||
|
setupRecyclerView()
|
||||||
|
observeViewModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupTitleBar() {
|
||||||
|
binding.tvContentOverviewTitle.setText(overviewType.toTitleResId())
|
||||||
|
binding.ivContentOverviewBack.setOnClickListener { finish() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupRecyclerView() {
|
||||||
|
binding.rvContentOverviewItems.adapter = overviewAdapter
|
||||||
|
binding.rvContentOverviewItems.layoutManager = GridLayoutManager(this, CONTENT_OVERVIEW_GRID_SPAN_COUNT)
|
||||||
|
binding.rvContentOverviewItems.addContentGridItemSpacing(CONTENT_OVERVIEW_GRID_SPAN_COUNT)
|
||||||
|
binding.rvContentOverviewItems.doOnLayout { updateGridItemWidth() }
|
||||||
|
binding.rvContentOverviewItems.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
|
if (!recyclerView.canScrollVertically(1)) {
|
||||||
|
viewModel.loadMore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observeViewModel() {
|
||||||
|
viewModel.isLoading.observe(this) { isLoading ->
|
||||||
|
binding.pbContentOverviewInitialLoading.visibility = if (isLoading) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
viewModel.overviewStateLiveData.observe(this, ::renderState)
|
||||||
|
viewModel.toastLiveData.observe(this) { toastMessage ->
|
||||||
|
toastMessage?.let(::showToast)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderState(state: ContentOverviewUiState) {
|
||||||
|
when (state) {
|
||||||
|
is ContentOverviewUiState.Loading -> renderLoading()
|
||||||
|
is ContentOverviewUiState.Content -> renderContent(state)
|
||||||
|
is ContentOverviewUiState.Empty -> renderEmpty()
|
||||||
|
is ContentOverviewUiState.Error -> renderError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderLoading() {
|
||||||
|
binding.rvContentOverviewItems.visibility = View.GONE
|
||||||
|
binding.tvContentOverviewEmptyError.visibility = View.GONE
|
||||||
|
binding.pbContentOverviewInitialLoading.visibility = View.VISIBLE
|
||||||
|
binding.pbContentOverviewLoadMore.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderContent(state: ContentOverviewUiState.Content) {
|
||||||
|
binding.rvContentOverviewItems.visibility = View.VISIBLE
|
||||||
|
binding.tvContentOverviewEmptyError.visibility = View.GONE
|
||||||
|
binding.pbContentOverviewInitialLoading.visibility = View.GONE
|
||||||
|
binding.pbContentOverviewLoadMore.visibility = if (state.isLoadingMore) View.VISIBLE else View.GONE
|
||||||
|
overviewAdapter.submitItems(state.items)
|
||||||
|
updateGridItemWidth()
|
||||||
|
state.paginationErrorMessage?.let {
|
||||||
|
showToast(it)
|
||||||
|
viewModel.consumePaginationErrorMessage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderEmpty() {
|
||||||
|
overviewAdapter.submitItems(emptyList())
|
||||||
|
binding.rvContentOverviewItems.visibility = View.GONE
|
||||||
|
binding.tvContentOverviewEmptyError.visibility = View.VISIBLE
|
||||||
|
binding.pbContentOverviewInitialLoading.visibility = View.GONE
|
||||||
|
binding.pbContentOverviewLoadMore.visibility = View.GONE
|
||||||
|
binding.tvContentOverviewEmptyError.setText(R.string.screen_content_all_empty)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderError() {
|
||||||
|
overviewAdapter.submitItems(emptyList())
|
||||||
|
binding.rvContentOverviewItems.visibility = View.GONE
|
||||||
|
binding.tvContentOverviewEmptyError.visibility = View.VISIBLE
|
||||||
|
binding.pbContentOverviewInitialLoading.visibility = View.GONE
|
||||||
|
binding.pbContentOverviewLoadMore.visibility = View.GONE
|
||||||
|
binding.tvContentOverviewEmptyError.setText(R.string.common_error_unknown)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateGridItemWidth() {
|
||||||
|
val widthPx = binding.rvContentOverviewItems.calculateContentGridItemWidthPx(CONTENT_OVERVIEW_GRID_SPAN_COUNT)
|
||||||
|
overviewAdapter.setGridItemWidthPx(widthPx)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showToast(toastMessage: kr.co.vividnext.sodalive.common.ToastMessage) {
|
||||||
|
toastMessage.message?.let { message -> showToast(message) }
|
||||||
|
?: toastMessage.resId?.let { resId -> showToast(getString(resId)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openAudioContentDetail(contentId: Long) {
|
||||||
|
if (contentId <= 0) return
|
||||||
|
|
||||||
|
startActivity(
|
||||||
|
Intent(this, AudioContentDetailActivity::class.java).apply {
|
||||||
|
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Intent.contentOverviewType(): ContentOverviewType {
|
||||||
|
val typeName = getStringExtra(EXTRA_CONTENT_OVERVIEW_TYPE)
|
||||||
|
return ContentOverviewType.entries.firstOrNull { it.name == typeName }
|
||||||
|
?: ContentOverviewType.NEW_AND_HOT_AUDIO
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val CONTENT_OVERVIEW_GRID_SPAN_COUNT = 2
|
||||||
|
private const val EXTRA_CONTENT_OVERVIEW_TYPE = "content_overview_type"
|
||||||
|
|
||||||
|
fun newIntent(context: Context, type: ContentOverviewType): Intent {
|
||||||
|
return Intent(context, ContentOverviewActivity::class.java)
|
||||||
|
.putExtra(EXTRA_CONTENT_OVERVIEW_TYPE, type.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.main.content.overview.ui
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import kr.co.vividnext.sodalive.databinding.ItemContentAudioCardBinding
|
||||||
|
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.overview.model.ContentOverviewUiModel
|
||||||
|
|
||||||
|
class ContentOverviewAdapter(
|
||||||
|
private val onContentClick: (Long) -> Unit = {}
|
||||||
|
) : RecyclerView.Adapter<ContentOverviewAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
private var items: List<ContentOverviewUiModel> = emptyList()
|
||||||
|
private var gridItemWidthPx: Int = 0
|
||||||
|
|
||||||
|
fun setGridItemWidthPx(widthPx: Int) {
|
||||||
|
if (widthPx <= 0 || gridItemWidthPx == widthPx) return
|
||||||
|
gridItemWidthPx = widthPx
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun submitItems(items: List<ContentOverviewUiModel>) {
|
||||||
|
this.items = items
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
return ViewHolder(
|
||||||
|
ItemContentAudioCardBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||||
|
onContentClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
holder.bind(items[position], gridItemWidthPx)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = items.size
|
||||||
|
|
||||||
|
class ViewHolder(
|
||||||
|
private val binding: ItemContentAudioCardBinding,
|
||||||
|
private val onContentClick: (Long) -> Unit
|
||||||
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
fun bind(item: ContentOverviewUiModel, gridItemWidthPx: Int) = with(binding.audioContentCard.root) {
|
||||||
|
setGridItemWidthPx(gridItemWidthPx)
|
||||||
|
setContent(item.title, item.creatorNickname)
|
||||||
|
setTags(item.tags)
|
||||||
|
setAdultVisible(item.showAdultBadge)
|
||||||
|
thumbnailView().loadUrl(item.coverImage)
|
||||||
|
setOnClickListener {
|
||||||
|
if (item.contentId <= 0) return@setOnClickListener
|
||||||
|
onContentClick(item.contentId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
96
app/src/main/res/layout/activity_content_overview.xml
Normal file
96
app/src/main/res/layout/activity_content_overview.xml
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<?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:id="@+id/layout_content_overview_root"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/black">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/layout_content_overview_title_bar"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="60dp"
|
||||||
|
android:background="@color/black"
|
||||||
|
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_content_overview_back"
|
||||||
|
android:layout_width="@dimen/spacing_24"
|
||||||
|
android:layout_height="@dimen/spacing_24"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
android:src="@drawable/ic_new_bar_back"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_content_overview_title"
|
||||||
|
style="@style/Typography.Heading2"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/spacing_14"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/iv_content_overview_back"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="New&Hot" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/rv_content_overview_items"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:paddingHorizontal="@dimen/spacing_14"
|
||||||
|
android:paddingTop="@dimen/spacing_8"
|
||||||
|
android:paddingBottom="@dimen/spacing_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/layout_content_overview_title_bar"
|
||||||
|
tools:listitem="@layout/item_content_audio_card" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_content_overview_empty_error"
|
||||||
|
style="@style/Typography.Body2"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/gray_500"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/layout_content_overview_title_bar"
|
||||||
|
tools:text="콘텐츠가 없습니다."
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/pb_content_overview_initial_loading"
|
||||||
|
android:layout_width="50dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/layout_content_overview_title_bar"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/pb_content_overview_load_more"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_marginBottom="@dimen/spacing_16"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
Reference in New Issue
Block a user