feat(series-main): 시리즈 전체보기 페이지 추가

- 홈, 요일별, 장르별 탭 추가
- 홈 리스트 UI 및 데이터
- 요일별 UI 및 데이터
This commit is contained in:
2025-11-13 18:27:04 +09:00
parent fba6d86018
commit 907b718a3a
20 changed files with 1141 additions and 17 deletions

View File

@@ -86,13 +86,15 @@
<data android:scheme="${URISCHEME}" /> <data android:scheme="${URISCHEME}" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW"/> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- PayVerse 리다이렉트에 등록할 커스텀 스킴/호스트 --> <!-- PayVerse 리다이렉트에 등록할 커스텀 스킴/호스트 -->
<data android:scheme="${URISCHEME}" <data
android:host="payverse" android:host="payverse"
android:path="/result"/> android:path="/result"
android:scheme="${URISCHEME}" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
@@ -196,6 +198,7 @@
</activity> </activity>
<activity android:name=".chat.character.newcharacters.NewCharactersAllActivity" /> <activity android:name=".chat.character.newcharacters.NewCharactersAllActivity" />
<activity android:name=".chat.original.detail.OriginalWorkDetailActivity" /> <activity android:name=".chat.original.detail.OriginalWorkDetailActivity" />
<activity android:name=".audio_content.series.main.SeriesMainActivity" />
<activity <activity
android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity" android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"

View File

@@ -0,0 +1,66 @@
package kr.co.vividnext.sodalive.audio_content.series.main
import com.google.android.material.tabs.TabLayout
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.series.main.by_genre.SeriesMainByGenreFragment
import kr.co.vividnext.sodalive.audio_content.series.main.day_of_week.SeriesMainDayOfWeekFragment
import kr.co.vividnext.sodalive.audio_content.series.main.home.SeriesMainHomeFragment
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.databinding.ActivitySeriesMainBinding
class SeriesMainActivity : BaseActivity<ActivitySeriesMainBinding>(
ActivitySeriesMainBinding::inflate
) {
private var currentTab = 0
override fun setupView() {
binding.toolbar.tvBack.text = "시리즈 전체보기"
binding.toolbar.tvBack.setOnClickListener { finish() }
setupTabs()
}
private fun setupTabs() {
binding.tabLayout.addTab(binding.tabLayout.newTab().setText(""))
binding.tabLayout.addTab(binding.tabLayout.newTab().setText("요일별"))
binding.tabLayout.addTab(binding.tabLayout.newTab().setText("장르별"))
// 탭 선택 리스너 설정
binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
currentTab = tab.position
showTabContent(currentTab)
}
override fun onTabUnselected(tab: TabLayout.Tab) {
// 필요한 경우 구현
}
override fun onTabReselected(tab: TabLayout.Tab) {
// 필요한 경우 구현
}
})
// 초기 탭 선택
showTabContent(currentTab)
}
private fun showTabContent(position: Int) {
val fragmentTransaction = supportFragmentManager.beginTransaction()
// 기존 프래그먼트 제거
supportFragmentManager.fragments.forEach {
fragmentTransaction.remove(it)
}
// 선택된 탭에 따라 프래그먼트 표시
val fragment = when (position) {
1 -> SeriesMainDayOfWeekFragment()
2 -> SeriesMainByGenreFragment()
else -> SeriesMainHomeFragment()
}
fragmentTransaction.add(R.id.fl_container, fragment)
fragmentTransaction.commit()
}
}

View File

@@ -0,0 +1,55 @@
package kr.co.vividnext.sodalive.audio_content.series.main
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
import kr.co.vividnext.sodalive.audio_content.series.main.by_genre.GetSeriesGenreListResponse
import kr.co.vividnext.sodalive.audio_content.series.main.home.SeriesHomeResponse
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.home.SeriesPublishedDaysOfWeek
import kr.co.vividnext.sodalive.settings.ContentType
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Query
interface SeriesMainApi {
@GET("/audio-content/series/main")
fun fetchHome(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<SeriesHomeResponse>>
@GET("/audio-content/series/main/recommend")
fun getRecommendSeriesList(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetSeriesListResponse.SeriesListItem>>>
@GET("/audio-content/series/main/day-of-week")
fun getDayOfWeekSeriesList(
@Query("dayOfWeek") dayOfWeek: SeriesPublishedDaysOfWeek,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetSeriesListResponse.SeriesListItem>>>
@GET("/audio-content/series/main/genre-list")
fun getGenreList(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetSeriesGenreListResponse>>>
@GET("/audio-content/series/main/list-by-genre")
fun getSeriesListByGenre(
@Query("genreId") genreId: Long,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetSeriesListResponse>>
}

View File

@@ -0,0 +1,55 @@
package kr.co.vividnext.sodalive.audio_content.series.main
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.home.SeriesPublishedDaysOfWeek
import kr.co.vividnext.sodalive.settings.ContentType
class SeriesMainRepository(
private val api: SeriesMainApi
) {
fun fetchData(token: String) = api.fetchHome(
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
authHeader = token
)
fun getRecommendSeriesList(token: String) = api.getRecommendSeriesList(
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
authHeader = token
)
fun getDayOfWeekSeriesList(
dayOfWeek: SeriesPublishedDaysOfWeek,
page: Int,
size: Int,
token: String
) = api.getDayOfWeekSeriesList(
dayOfWeek = dayOfWeek,
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
page = page - 1,
size = size,
authHeader = token
)
fun getGenreList(token: String) = api.getGenreList(
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
authHeader = token
)
fun getSeriesListByGenre(
genreId: Long,
page: Int,
size: Int,
token: String
) = api.getSeriesListByGenre(
genreId = genreId,
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
page = page - 1,
size = size,
authHeader = token
)
}

View File

@@ -0,0 +1,10 @@
package kr.co.vividnext.sodalive.audio_content.series.main.by_genre
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
data class GetSeriesGenreListResponse(
@SerializedName("id") val id: Long,
@SerializedName("genre") val genre: String
)

View File

@@ -0,0 +1,9 @@
package kr.co.vividnext.sodalive.audio_content.series.main.by_genre
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.databinding.FragmentSeriesMainByGenreBinding
class SeriesMainByGenreFragment : BaseFragment<FragmentSeriesMainByGenreBinding>(
FragmentSeriesMainByGenreBinding::inflate
) {
}

View File

@@ -0,0 +1,18 @@
package kr.co.vividnext.sodalive.audio_content.series.main.by_genre
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kr.co.vividnext.sodalive.audio_content.series.main.SeriesMainRepository
import kr.co.vividnext.sodalive.base.BaseViewModel
class SeriesMainByGenreViewModel(
private val repository: SeriesMainRepository
) : BaseViewModel() {
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
}

View File

@@ -0,0 +1,172 @@
package kr.co.vividnext.sodalive.audio_content.series.main.day_of_week
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.FragmentSeriesMainDayOfWeekBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.home.DayOfWeekAdapter
import kr.co.vividnext.sodalive.home.SeriesPublishedDaysOfWeek
import org.koin.android.ext.android.inject
import java.util.Calendar
import kotlin.math.roundToInt
class SeriesMainDayOfWeekFragment : BaseFragment<FragmentSeriesMainDayOfWeekBinding>(
FragmentSeriesMainDayOfWeekBinding::inflate
) {
private val viewModel: SeriesMainDayOfWeekViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: SeriesListAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupView()
observeViewModel()
}
private fun setupView() {
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
setupDayOfWeekDay()
setupSeriesView()
val dayOfWeeks = listOf(
SeriesPublishedDaysOfWeek.RANDOM,
SeriesPublishedDaysOfWeek.SUN,
SeriesPublishedDaysOfWeek.MON,
SeriesPublishedDaysOfWeek.TUE,
SeriesPublishedDaysOfWeek.WED,
SeriesPublishedDaysOfWeek.THU,
SeriesPublishedDaysOfWeek.FRI,
SeriesPublishedDaysOfWeek.SAT
)
val calendar = Calendar.getInstance()
val dayIndex = calendar.get(Calendar.DAY_OF_WEEK)
viewModel.dayOfWeek = dayOfWeeks[dayIndex]
}
private fun setupDayOfWeekDay() {
val dayOfWeekAdapter = DayOfWeekAdapter(screenWidth = screenWidth) {
adapter.clear()
viewModel.dayOfWeek = it
}
val rvDayOfWeek = binding.rvSeriesDayOfWeekDay
val layoutManager = object : LinearLayoutManager(
context,
HORIZONTAL,
false
) {
override fun canScrollVertically() = false
override fun canScrollHorizontally() = false
}
rvDayOfWeek.layoutManager = layoutManager
rvDayOfWeek.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 2.5f.dpToPx().toInt()
}
dayOfWeekAdapter.itemCount - 1 -> {
outRect.left = 2.5f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 2.5f.dpToPx().toInt()
outRect.right = 2.5f.dpToPx().toInt()
}
}
}
})
rvDayOfWeek.adapter = dayOfWeekAdapter
}
private fun setupSeriesView() {
adapter = 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.rvSeriesDayOfWeek
recyclerView.layoutManager = GridLayoutManager(requireContext(), spanCount)
recyclerView.addItemDecoration(
GridSpacingItemDecoration(spanCount, spacingPx, false)
)
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()
}
}
})
recyclerView.adapter = adapter
viewModel.seriesListLiveData.observe(viewLifecycleOwner) {
adapter.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() }
}
}
}

View File

@@ -0,0 +1,85 @@
package kr.co.vividnext.sodalive.audio_content.series.main.day_of_week
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.orhanobut.logger.Logger
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
import kr.co.vividnext.sodalive.home.SeriesPublishedDaysOfWeek
class SeriesMainDayOfWeekViewModel(
private val repository: SeriesMainRepository
) : BaseViewModel() {
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
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
var dayOfWeek = SeriesPublishedDaysOfWeek.RANDOM
set(newValue) {
if (field != newValue) {
page = 1
isLast = false
field = newValue
getSeriesList()
}
}
fun getSeriesList() {
if (isLast) return
_isLoading.value = true
compositeDisposable.add(
repository.getDayOfWeekSeriesList(
dayOfWeek = dayOfWeek,
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
if (data.isNotEmpty()) {
_seriesListLiveData.value = data
} else {
isLast = true
}
} else {
if (it.message != null) {
_toastLiveData.value = it.message
} else {
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
}
)
)
}
}

View File

@@ -0,0 +1,53 @@
package kr.co.vividnext.sodalive.audio_content.series.main.home
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.widget.FrameLayout
import android.widget.ImageView
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.zhpan.bannerview.BaseBannerAdapter
import com.zhpan.bannerview.BaseViewHolder
import kr.co.vividnext.sodalive.R
class SeriesBannerAdapter(
private val context: Context,
private val itemWidth: Int,
private val itemHeight: Int,
private val onClick: (SeriesBannerResponse) -> Unit
) : BaseBannerAdapter<SeriesBannerResponse>() {
override fun bindData(
holder: BaseViewHolder<SeriesBannerResponse?>,
data: SeriesBannerResponse,
position: Int,
pageSize: Int
) {
val ivBanner = holder.findViewById<ImageView>(R.id.iv_recommend_live)
val layoutParams = ivBanner.layoutParams as FrameLayout.LayoutParams
layoutParams.width = itemWidth
layoutParams.height = itemHeight
Glide
.with(context)
.asBitmap()
.load(data.imagePath)
.into(object : CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
ivBanner.setImageBitmap(resource)
ivBanner.layoutParams = layoutParams
}
override fun onLoadCleared(placeholder: Drawable?) {
}
})
ivBanner.setOnClickListener { onClick(data) }
}
override fun getLayoutId(viewType: Int): Int {
return R.layout.item_recommend_live
}
}

View File

@@ -0,0 +1,23 @@
package kr.co.vividnext.sodalive.audio_content.series.main.home
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
@Keep
data class SeriesHomeResponse(
@SerializedName("banners")
val banners: List<SeriesBannerResponse>,
@SerializedName("completedSeriesList")
val completedSeriesList: List<GetSeriesListResponse.SeriesListItem>,
@SerializedName("recommendSeriesList")
val recommendSeriesList: List<GetSeriesListResponse.SeriesListItem>
)
@Keep
data class SeriesBannerResponse(
@SerializedName("seriesId") val seriesId: Long,
@SerializedName("imagePath") val imagePath: String
)

View File

@@ -0,0 +1,233 @@
package kr.co.vividnext.sodalive.audio_content.series.main.home
import android.content.Intent
import android.graphics.Rect
import android.os.Bundle
import android.view.View
import android.widget.LinearLayout
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.zhpan.bannerview.BaseBannerAdapter
import com.zhpan.indicator.enums.IndicatorSlideMode
import com.zhpan.indicator.enums.IndicatorStyle
import kr.co.vividnext.sodalive.R
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.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.FragmentSeriesMainHomeBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.home.HomeSeriesAdapter
import kr.co.vividnext.sodalive.main.MainActivity
import org.koin.android.ext.android.inject
import kotlin.math.roundToInt
class SeriesMainHomeFragment : BaseFragment<FragmentSeriesMainHomeBinding>(
FragmentSeriesMainHomeBinding::inflate
) {
private val viewModel: SeriesMainHomeViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var bannerAdapter: SeriesBannerAdapter
private lateinit var completedAdapter: HomeSeriesAdapter
private lateinit var recommendAdapter: SeriesListAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupView()
observeViewModel()
viewModel.fetchData()
}
private fun setupView() {
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
setupBanner()
setupCompletedSeriesView()
setupRecommendSeriesView()
}
private fun setupBanner() {
val layoutParams = binding
.bannerSlider
.layoutParams as LinearLayout.LayoutParams
val pagerWidth = screenWidth
val pagerHeight = pagerWidth * 198 / 352
layoutParams.width = pagerWidth
layoutParams.height = pagerHeight
bannerAdapter = SeriesBannerAdapter(
requireContext(),
pagerWidth,
pagerHeight
) {
startActivity(
Intent(
requireContext(),
SeriesDetailActivity::class.java
).apply {
putExtra(Constants.EXTRA_SERIES_ID, it.seriesId)
}
)
}
binding
.bannerSlider
.layoutParams = layoutParams
binding.bannerSlider.apply {
adapter = bannerAdapter as BaseBannerAdapter<Any>
setLifecycleRegistry(lifecycle)
setScrollDuration(1000)
setInterval(4 * 1000)
}.create()
binding
.bannerSlider
.setIndicatorView(binding.indicatorBanner)
.setIndicatorStyle(IndicatorStyle.ROUND_RECT)
.setIndicatorSlideMode(IndicatorSlideMode.SMOOTH)
.setIndicatorVisibility(View.GONE)
.setIndicatorSliderColor(
ContextCompat.getColor(requireContext(), R.color.color_909090),
ContextCompat.getColor(requireContext(), R.color.color_3bb9f1)
)
.setIndicatorSliderWidth(10f.dpToPx().toInt(), 10f.dpToPx().toInt())
.setIndicatorHeight(10f.dpToPx().toInt())
viewModel.bannerListLiveData.observe(viewLifecycleOwner) {
if (it.isNotEmpty()) {
binding.llBanner.visibility = View.VISIBLE
binding.bannerSlider.refreshData(it)
} else {
binding.llBanner.visibility = View.GONE
}
}
}
private fun setupCompletedSeriesView() {
completedAdapter = HomeSeriesAdapter {
if (SharedPreferenceManager.token.isNotBlank()) {
startActivity(
Intent(requireContext(), SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, it)
}
)
} else {
(requireActivity() as MainActivity).showLoginActivity()
}
}
val recyclerView = binding.rvCompletedSeries
recyclerView.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 8f.dpToPx().toInt()
}
completedAdapter.itemCount - 1 -> {
outRect.left = 8f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 8f.dpToPx().toInt()
outRect.right = 8f.dpToPx().toInt()
}
}
}
})
recyclerView.adapter = completedAdapter
viewModel.completedSeriesLiveData.observe(viewLifecycleOwner) {
if (it.isNotEmpty()) {
binding.llCompletedSeries.visibility = View.VISIBLE
completedAdapter.addItems(it)
} else {
binding.llCompletedSeries.visibility = View.GONE
}
}
}
private fun setupRecommendSeriesView() {
recommendAdapter = 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.rvRecommendSeries
recyclerView.layoutManager = GridLayoutManager(requireContext(), spanCount)
recyclerView.addItemDecoration(
GridSpacingItemDecoration(spanCount, spacingPx, false)
)
recyclerView.adapter = recommendAdapter
binding.ivRecommendRefresh.setOnClickListener {
viewModel.getRecommendSeriesList()
}
viewModel.recommendSeriesLiveData.observe(viewLifecycleOwner) {
if (it.isNotEmpty()) {
binding.llRecommendSeries.visibility = View.VISIBLE
recommendAdapter.clear()
recommendAdapter.addItems(it)
} else {
binding.llRecommendSeries.visibility = View.GONE
}
}
}
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() }
}
}
}

View File

@@ -0,0 +1,101 @@
package kr.co.vividnext.sodalive.audio_content.series.main.home
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.orhanobut.logger.Logger
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 SeriesMainHomeViewModel(
private val repository: SeriesMainRepository
) : BaseViewModel() {
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private var _bannerListLiveData = MutableLiveData<List<SeriesBannerResponse>>()
val bannerListLiveData: LiveData<List<SeriesBannerResponse>>
get() = _bannerListLiveData
private var _completedSeriesLiveData =
MutableLiveData<List<GetSeriesListResponse.SeriesListItem>>()
val completedSeriesLiveData: LiveData<List<GetSeriesListResponse.SeriesListItem>>
get() = _completedSeriesLiveData
private var _recommendSeriesLiveData =
MutableLiveData<List<GetSeriesListResponse.SeriesListItem>>()
val recommendSeriesLiveData: LiveData<List<GetSeriesListResponse.SeriesListItem>>
get() = _recommendSeriesLiveData
fun fetchData() {
_isLoading.value = true
compositeDisposable.add(
repository.fetchData(token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
val data = it.data
if (it.success && data != null) {
_bannerListLiveData.value = data.banners
_completedSeriesLiveData.value = data.completedSeriesList
_recommendSeriesLiveData.value = data.recommendSeriesList
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun getRecommendSeriesList() {
_isLoading.value = true
compositeDisposable.add(
repository.getRecommendSeriesList(token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success && it.data != null) {
_recommendSeriesLiveData.value = it.data
} else {
if (it.message != null) {
_toastLiveData.value = it.message
} else {
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
}
)
)
}
}

View File

@@ -32,6 +32,11 @@ import kr.co.vividnext.sodalive.audio_content.series.SeriesListAllViewModel
import kr.co.vividnext.sodalive.audio_content.series.SeriesRepository import kr.co.vividnext.sodalive.audio_content.series.SeriesRepository
import kr.co.vividnext.sodalive.audio_content.series.content.SeriesContentAllViewModel import kr.co.vividnext.sodalive.audio_content.series.content.SeriesContentAllViewModel
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailViewModel import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailViewModel
import kr.co.vividnext.sodalive.audio_content.series.main.SeriesMainApi
import kr.co.vividnext.sodalive.audio_content.series.main.SeriesMainRepository
import kr.co.vividnext.sodalive.audio_content.series.main.by_genre.SeriesMainByGenreViewModel
import kr.co.vividnext.sodalive.audio_content.series.main.day_of_week.SeriesMainDayOfWeekViewModel
import kr.co.vividnext.sodalive.audio_content.series.main.home.SeriesMainHomeViewModel
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadViewModel import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadViewModel
import kr.co.vividnext.sodalive.audio_content.upload.theme.AudioContentThemeViewModel import kr.co.vividnext.sodalive.audio_content.upload.theme.AudioContentThemeViewModel
import kr.co.vividnext.sodalive.audition.AuditionApi import kr.co.vividnext.sodalive.audition.AuditionApi
@@ -224,6 +229,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
single { ApiBuilder().build(get(), TermsApi::class.java) } single { ApiBuilder().build(get(), TermsApi::class.java) }
single { ApiBuilder().build(get(), EventApi::class.java) } single { ApiBuilder().build(get(), EventApi::class.java) }
single { ApiBuilder().build(get(), SeriesApi::class.java) } single { ApiBuilder().build(get(), SeriesApi::class.java) }
single { ApiBuilder().build(get(), SeriesMainApi::class.java) }
single { ApiBuilder().build(get(), ReportApi::class.java) } single { ApiBuilder().build(get(), ReportApi::class.java) }
single { ApiBuilder().build(get(), LiveRecommendApi::class.java) } single { ApiBuilder().build(get(), LiveRecommendApi::class.java) }
single { ApiBuilder().build(get(), ExplorerApi::class.java) } single { ApiBuilder().build(get(), ExplorerApi::class.java) }
@@ -333,6 +339,9 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { NewCharactersAllViewModel(get()) } viewModel { NewCharactersAllViewModel(get()) }
viewModel { OriginalWorkViewModel(get()) } viewModel { OriginalWorkViewModel(get()) }
viewModel { OriginalWorkDetailViewModel(get()) } viewModel { OriginalWorkDetailViewModel(get()) }
viewModel { SeriesMainHomeViewModel(get()) }
viewModel { SeriesMainByGenreViewModel(get()) }
viewModel { SeriesMainDayOfWeekViewModel(get()) }
} }
private val repositoryModule = module { private val repositoryModule = module {
@@ -376,6 +385,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
factory { CharacterCommentRepository(get()) } factory { CharacterCommentRepository(get()) }
factory { NewCharactersRepository(get()) } factory { NewCharactersRepository(get()) }
factory { OriginalWorkRepository(get()) } factory { OriginalWorkRepository(get()) }
factory { SeriesMainRepository(get()) }
} }

View File

@@ -26,13 +26,16 @@ import com.zhpan.indicator.enums.IndicatorSlideMode
import com.zhpan.indicator.enums.IndicatorStyle import com.zhpan.indicator.enums.IndicatorStyle
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService
import kr.co.vividnext.sodalive.audio_content.all.AudioContentAllActivity
import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllActivity import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllActivity
import kr.co.vividnext.sodalive.audio_content.box.AudioContentBoxActivity import kr.co.vividnext.sodalive.audio_content.box.AudioContentBoxActivity
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.main.AudioContentBannerType import kr.co.vividnext.sodalive.audio_content.main.AudioContentBannerType
import kr.co.vividnext.sodalive.audio_content.main.banner.AudioContentMainBannerAdapter import kr.co.vividnext.sodalive.audio_content.main.banner.AudioContentMainBannerAdapter
import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerService import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerService
import kr.co.vividnext.sodalive.audio_content.series.SeriesListAllActivity
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
import kr.co.vividnext.sodalive.audio_content.series.main.SeriesMainActivity
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity
import kr.co.vividnext.sodalive.audition.AuditionActivity import kr.co.vividnext.sodalive.audition.AuditionActivity
import kr.co.vividnext.sodalive.base.BaseFragment import kr.co.vividnext.sodalive.base.BaseFragment
@@ -646,7 +649,10 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::infl
// ‘오직 보이스온에서만’ 전체보기: isOriginal=true로 시리즈 전체보기 화면 진입 // ‘오직 보이스온에서만’ 전체보기: isOriginal=true로 시리즈 전체보기 화면 진입
if (SharedPreferenceManager.token.isNotBlank()) { if (SharedPreferenceManager.token.isNotBlank()) {
startActivity( startActivity(
Intent(requireContext(), kr.co.vividnext.sodalive.audio_content.series.SeriesListAllActivity::class.java).apply { Intent(
requireContext(),
SeriesListAllActivity::class.java
).apply {
putExtra(kr.co.vividnext.sodalive.common.Constants.EXTRA_IS_ORIGINAL, true) putExtra(kr.co.vividnext.sodalive.common.Constants.EXTRA_IS_ORIGINAL, true)
} }
) )
@@ -791,7 +797,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::infl
outRect.right = 2.5f.dpToPx().toInt() outRect.right = 2.5f.dpToPx().toInt()
} }
seriesDayOfWeekAdapter.itemCount - 1 -> { dayOfWeekAdapter.itemCount - 1 -> {
outRect.left = 2.5f.dpToPx().toInt() outRect.left = 2.5f.dpToPx().toInt()
outRect.right = 0 outRect.right = 0
} }
@@ -804,6 +810,21 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::infl
} }
}) })
rvDayOfWeek.adapter = dayOfWeekAdapter rvDayOfWeek.adapter = dayOfWeekAdapter
binding.tvSeriesDayOfWeekAll.setOnClickListener {
if (SharedPreferenceManager.token.isNotBlank()) {
startActivity(
Intent(
requireContext(),
SeriesMainActivity::class.java
).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_FREE, true)
}
)
} else {
(requireActivity() as MainActivity).showLoginActivity()
}
}
} }
private fun setupPopularCharacters() { private fun setupPopularCharacters() {
@@ -1127,7 +1148,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::infl
binding.tvFreeContentAll.setOnClickListener { binding.tvFreeContentAll.setOnClickListener {
if (SharedPreferenceManager.token.isNotBlank()) { if (SharedPreferenceManager.token.isNotBlank()) {
startActivity( startActivity(
Intent(requireContext(), kr.co.vividnext.sodalive.audio_content.all.AudioContentAllActivity::class.java).apply { Intent(requireContext(), AudioContentAllActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_FREE, true) putExtra(Constants.EXTRA_AUDIO_CONTENT_FREE, true)
} }
) )
@@ -1211,7 +1232,10 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::infl
binding.tvPointContentAll.setOnClickListener { binding.tvPointContentAll.setOnClickListener {
if (SharedPreferenceManager.token.isNotBlank()) { if (SharedPreferenceManager.token.isNotBlank()) {
startActivity( startActivity(
Intent(requireContext(), kr.co.vividnext.sodalive.audio_content.all.AudioContentAllActivity::class.java).apply { Intent(
requireContext(),
AudioContentAllActivity::class.java
).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_POINT_ONLY, true) putExtra(Constants.EXTRA_AUDIO_CONTENT_POINT_ONLY, true)
} }
) )
@@ -1229,7 +1253,10 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::infl
recommendContentAdapter = HomeContentAdapter(onClickItem = { recommendContentAdapter = HomeContentAdapter(onClickItem = {
if (SharedPreferenceManager.token.isNotBlank()) { if (SharedPreferenceManager.token.isNotBlank()) {
startActivity( startActivity(
Intent(requireContext(), AudioContentDetailActivity::class.java).apply { Intent(
requireContext(),
AudioContentDetailActivity::class.java
).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it) putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
} }
) )

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/color_131313"
android:orientation="vertical">
<include
android:id="@+id/toolbar"
layout="@layout/detail_toolbar" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/color_131313"
app:tabIndicatorColor="@color/color_3bb9f1"
app:tabIndicatorFullWidth="true"
app:tabIndicatorHeight="4dp"
app:tabSelectedTextColor="@color/color_3bb9f1"
app:tabTextAppearance="@style/tabText"
app:tabTextColor="@color/color_b0bec5" />
<FrameLayout
android:id="@+id/fl_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@@ -253,16 +253,33 @@
android:layout_marginBottom="48dp" android:layout_marginBottom="48dp"
android:orientation="vertical"> android:orientation="vertical">
<TextView <LinearLayout
android:id="@+id/tv_series_day_of_week"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp" android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="24dp">
<TextView
android:id="@+id/tv_series_day_of_week"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:fontFamily="@font/pretendard_bold" android:fontFamily="@font/pretendard_bold"
android:text="요일별 시리즈" android:text="요일별 시리즈"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="24sp" /> android:textSize="24sp" />
<TextView
android:id="@+id/tv_series_day_of_week_all"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/pretendard_regular"
android:text="전체보기"
android:textColor="#90A4AE"
android:textSize="14sp" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_series_day_of_week_day" android:id="@+id/rv_series_day_of_week_day"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_genre"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:clipToPadding="false"
android:nestedScrollingEnabled="false"
android:paddingHorizontal="24dp" />
<androidx.recyclerview.widget.RecyclerView
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" />
</LinearLayout>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_series_day_of_week_day"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:clipToPadding="false"
android:nestedScrollingEnabled="false"
android:paddingHorizontal="24dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_series_day_of_week"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="24dp"
android:clipToPadding="false"
android:paddingHorizontal="24dp" />
</LinearLayout>

View File

@@ -0,0 +1,111 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/color_131313"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingVertical="24dp">
<!-- 배너 섹션 -->
<LinearLayout
android:id="@+id/ll_banner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.zhpan.bannerview.BannerViewPager
android:id="@+id/banner_slider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false" />
<com.zhpan.indicator.IndicatorView
android:id="@+id/indicator_banner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="16dp" />
</LinearLayout>
<!-- 완결 시리즈 섹션 -->
<LinearLayout
android:id="@+id/ll_completed_series"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:id="@+id/tv_completed_series"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:fontFamily="@font/pretendard_bold"
android:text="완결 시리즈"
android:textColor="@color/white"
android:textSize="24sp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_completed_series"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:clipToPadding="false"
android:paddingHorizontal="24dp" />
</LinearLayout>
<!-- 추천 시리즈 섹션 -->
<LinearLayout
android:id="@+id/ll_recommend_series"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:layout_marginBottom="24dp"
android:orientation="vertical"
android:visibility="gone">
<!-- 제목과 새로고침 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="24dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:fontFamily="@font/pretendard_bold"
android:text="추천 시리즈"
android:textColor="@color/white"
android:textSize="24sp" />
<ImageView
android:id="@+id/iv_recommend_refresh"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_refresh" />
</LinearLayout>
<!-- 2단 Grid 리스트 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_recommend_series"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:clipToPadding="false"
android:paddingHorizontal="24dp" />
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>