feat(series-main): 시리즈 전체보기 장르별 탭 UI 및 데이터
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.series.main.by_genre
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.databinding.ItemHomeContentThemeBinding
|
||||
|
||||
class GenreAdapter(
|
||||
private val onClickItem: (GetSeriesGenreListResponse) -> Unit
|
||||
) : RecyclerView.Adapter<GenreAdapter.ViewHolder>() {
|
||||
|
||||
private val items = mutableListOf<GetSeriesGenreListResponse>()
|
||||
private var selectedGenreId: Long? = null
|
||||
|
||||
inner class ViewHolder(
|
||||
private val binding: ItemHomeContentThemeBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun bind(item: GetSeriesGenreListResponse) {
|
||||
binding.tvTheme.text = item.genre
|
||||
if (item.id == selectedGenreId) {
|
||||
binding.tvTheme.setBackgroundResource(R.drawable.bg_round_corner_999_3bb9f1)
|
||||
} else {
|
||||
binding.tvTheme.setBackgroundResource(R.drawable.bg_round_corner_999_263238)
|
||||
}
|
||||
binding.root.setOnClickListener {
|
||||
selectedGenreId = item.id
|
||||
onClickItem(item)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
|
||||
ItemHomeContentThemeBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(items[position])
|
||||
}
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
fun submitList(list: List<GetSeriesGenreListResponse>, preselectId: Long?) {
|
||||
items.clear()
|
||||
items.addAll(list)
|
||||
selectedGenreId = preselectId
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,151 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.series.main.by_genre
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kr.co.vividnext.sodalive.audio_content.series.SeriesListAdapter
|
||||
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
|
||||
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.databinding.FragmentSeriesMainByGenreBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import org.koin.android.ext.android.inject
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class SeriesMainByGenreFragment : BaseFragment<FragmentSeriesMainByGenreBinding>(
|
||||
FragmentSeriesMainByGenreBinding::inflate
|
||||
) {
|
||||
private val viewModel: SeriesMainByGenreViewModel by inject()
|
||||
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
private lateinit var seriesAdapter: SeriesListAdapter
|
||||
private lateinit var genreAdapter: GenreAdapter
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setupView()
|
||||
observeViewModel()
|
||||
viewModel.loadGenres()
|
||||
}
|
||||
|
||||
private fun setupView() {
|
||||
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
|
||||
setupGenreView()
|
||||
setupSeriesView()
|
||||
}
|
||||
|
||||
private fun setupGenreView() {
|
||||
genreAdapter = GenreAdapter { genre ->
|
||||
seriesAdapter.clear()
|
||||
viewModel.setGenre(genre.id)
|
||||
}
|
||||
|
||||
val rvGenre = binding.rvGenre
|
||||
val layoutManager = LinearLayoutManager(
|
||||
context,
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
rvGenre.layoutManager = layoutManager
|
||||
rvGenre.adapter = genreAdapter
|
||||
|
||||
rvGenre.addItemDecoration(object : RecyclerView.ItemDecoration() {
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
super.getItemOffsets(outRect, view, parent, state)
|
||||
|
||||
val position = parent.getChildAdapterPosition(view)
|
||||
val itemCount = parent.adapter?.itemCount ?: 0
|
||||
|
||||
when (position) {
|
||||
0 -> {
|
||||
outRect.left = 0
|
||||
outRect.right = 8f.dpToPx().toInt()
|
||||
}
|
||||
|
||||
itemCount - 1 -> {
|
||||
outRect.left = 8f.dpToPx().toInt()
|
||||
outRect.right = 0
|
||||
}
|
||||
|
||||
else -> {
|
||||
outRect.left = 8f.dpToPx().toInt()
|
||||
outRect.right = 8f.dpToPx().toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun setupSeriesView() {
|
||||
seriesAdapter = SeriesListAdapter(
|
||||
itemWidth = ((screenWidth - 24f.dpToPx() * 2 - 16f.dpToPx()) / 2f).roundToInt(),
|
||||
onClickItem = {
|
||||
startActivity(
|
||||
Intent(
|
||||
requireContext(),
|
||||
SeriesDetailActivity::class.java
|
||||
).apply {
|
||||
putExtra(Constants.EXTRA_SERIES_ID, it)
|
||||
}
|
||||
)
|
||||
},
|
||||
onClickCreator = {},
|
||||
isVisibleCreator = false
|
||||
)
|
||||
|
||||
val spanCount = 2
|
||||
val spacingPx = 16f.dpToPx().toInt()
|
||||
val recyclerView = binding.rvSeriesByGenre
|
||||
recyclerView.layoutManager = GridLayoutManager(requireContext(), spanCount)
|
||||
recyclerView.addItemDecoration(GridSpacingItemDecoration(spanCount, spacingPx, false))
|
||||
recyclerView.adapter = seriesAdapter
|
||||
|
||||
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
|
||||
val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!!
|
||||
.findLastCompletelyVisibleItemPosition()
|
||||
val itemTotalCount = recyclerView.adapter!!.itemCount - 1
|
||||
|
||||
if (!recyclerView.canScrollVertically(1) && lastVisibleItemPosition == itemTotalCount) {
|
||||
viewModel.getSeriesList()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
viewModel.seriesListLiveData.observe(viewLifecycleOwner) {
|
||||
seriesAdapter.addItems(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeViewModel() {
|
||||
viewModel.isLoading.observe(viewLifecycleOwner) {
|
||||
if (it) {
|
||||
loadingDialog.show(screenWidth)
|
||||
} else {
|
||||
loadingDialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.toastLiveData.observe(viewLifecycleOwner) {
|
||||
it?.let { Toast.makeText(requireActivity(), it, Toast.LENGTH_LONG).show() }
|
||||
}
|
||||
|
||||
viewModel.genreListLiveData.observe(viewLifecycleOwner) { genres ->
|
||||
genreAdapter.submitList(genres, viewModel.selectedGenreId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,12 @@ package kr.co.vividnext.sodalive.audio_content.series.main.by_genre
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
|
||||
import kr.co.vividnext.sodalive.audio_content.series.main.SeriesMainRepository
|
||||
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
|
||||
class SeriesMainByGenreViewModel(
|
||||
private val repository: SeriesMainRepository
|
||||
@@ -15,4 +19,103 @@ class SeriesMainByGenreViewModel(
|
||||
private val _toastLiveData = MutableLiveData<String?>()
|
||||
val toastLiveData: LiveData<String?>
|
||||
get() = _toastLiveData
|
||||
|
||||
private val _genreListLiveData = MutableLiveData<List<GetSeriesGenreListResponse>>()
|
||||
val genreListLiveData: LiveData<List<GetSeriesGenreListResponse>>
|
||||
get() = _genreListLiveData
|
||||
|
||||
private val _seriesListLiveData = MutableLiveData<List<GetSeriesListResponse.SeriesListItem>>()
|
||||
val seriesListLiveData: LiveData<List<GetSeriesListResponse.SeriesListItem>>
|
||||
get() = _seriesListLiveData
|
||||
|
||||
private var page = 1
|
||||
private var isLast = false
|
||||
private val pageSize = 20
|
||||
|
||||
private var _selectedGenreId: Long? = null
|
||||
val selectedGenreId: Long?
|
||||
get() = _selectedGenreId
|
||||
|
||||
fun loadGenres() {
|
||||
_isLoading.value = true
|
||||
compositeDisposable.add(
|
||||
repository.getGenreList(
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
_isLoading.value = false
|
||||
if (it.success && it.data != null) {
|
||||
val genres = it.data
|
||||
if (genres.isNotEmpty()) {
|
||||
_selectedGenreId = genres.first().id
|
||||
page = 1
|
||||
isLast = false
|
||||
_genreListLiveData.value = genres
|
||||
getSeriesList()
|
||||
} else {
|
||||
_genreListLiveData.value = emptyList()
|
||||
}
|
||||
} else {
|
||||
_toastLiveData.value = it.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
}
|
||||
},
|
||||
{
|
||||
_isLoading.value = false
|
||||
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun setGenre(genreId: Long) {
|
||||
if (_selectedGenreId != genreId) {
|
||||
_selectedGenreId = genreId
|
||||
page = 1
|
||||
isLast = false
|
||||
// 시리즈 목록 초기화는 Fragment에서 어댑터를 clear로 처리
|
||||
getSeriesList()
|
||||
}
|
||||
}
|
||||
|
||||
fun getSeriesList() {
|
||||
val genreId = _selectedGenreId ?: return
|
||||
if (isLast) return
|
||||
if (_isLoading.value == true) return
|
||||
|
||||
_isLoading.value = true
|
||||
compositeDisposable.add(
|
||||
repository.getSeriesListByGenre(
|
||||
genreId = genreId,
|
||||
page = page,
|
||||
size = pageSize,
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
_isLoading.value = false
|
||||
val data = it.data
|
||||
if (it.success && data != null) {
|
||||
page += 1
|
||||
val items = data.items
|
||||
if (items.isNotEmpty()) {
|
||||
_seriesListLiveData.value = items
|
||||
} else {
|
||||
isLast = true
|
||||
}
|
||||
} else {
|
||||
_toastLiveData.value = it.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
}
|
||||
},
|
||||
{
|
||||
_isLoading.value = false
|
||||
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
android:id="@+id/rv_genre"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:clipToPadding="false"
|
||||
android:nestedScrollingEnabled="false"
|
||||
android:paddingHorizontal="24dp" />
|
||||
@@ -17,7 +17,7 @@
|
||||
android:id="@+id/rv_series_by_genre"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="24dp"
|
||||
android:clipToPadding="false"
|
||||
android:paddingHorizontal="24dp" />
|
||||
android:paddingHorizontal="24dp"
|
||||
android:paddingVertical="16dp" />
|
||||
</LinearLayout>
|
||||
|
||||
Reference in New Issue
Block a user