feat: 메인 홈

- 최신 콘텐츠 UI 추가
This commit is contained in:
2025-07-15 06:27:33 +09:00
parent f24cd97afa
commit 83a30fa088
9 changed files with 339 additions and 2 deletions

View File

@@ -0,0 +1,68 @@
package kr.co.vividnext.sodalive.home
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemHomeContentBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class HomeContentAdapter(
private val onClickItem: (Long) -> Unit,
) : RecyclerView.Adapter<HomeContentAdapter.ViewHolder>() {
private val items = mutableListOf<AudioContentMainItem>()
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
) = ViewHolder(
ItemHomeContentBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(
holder: ViewHolder,
position: Int
) {
holder.bind(items[position])
}
override fun getItemCount() = items.size
inner class ViewHolder(
private val binding: ItemHomeContentBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: AudioContentMainItem) {
binding.ivPoint.visibility = if (item.isPointAvailable) {
View.VISIBLE
} else {
View.GONE
}
binding.ivContentCoverImage.load(item.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(2.7f.dpToPx()))
}
binding.tvContentTitle.text = item.title
binding.tvNickname.text = item.creatorNickname
binding.root.setOnClickListener { onClickItem(item.contentId) }
}
}
@SuppressLint("NotifyDataSetChanged")
fun addItems(items: List<AudioContentMainItem>) {
this.items.clear()
this.items.addAll(items)
notifyDataSetChanged()
}
}

View File

@@ -0,0 +1,59 @@
package kr.co.vividnext.sodalive.home
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 HomeContentThemeAdapter(
private val onClickItem: (String) -> Unit
) : RecyclerView.Adapter<HomeContentThemeAdapter.ViewHolder>() {
private val themeList = mutableListOf<String>()
private var selectedTheme = ""
inner class ViewHolder(
private val binding: ItemHomeContentThemeBinding
) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("NotifyDataSetChanged")
fun bind(theme: String) {
if (theme == selectedTheme ||
(selectedTheme == "" && theme == "전체")
) {
binding.tvTheme.setBackgroundResource(R.drawable.bg_round_corner_999_3bb9f1)
} else {
binding.tvTheme.setBackgroundResource(R.drawable.bg_round_corner_999_263238)
}
binding.tvTheme.text = theme
binding.root.setOnClickListener {
onClickItem(theme)
selectedTheme = theme
notifyDataSetChanged()
}
}
}
@SuppressLint("NotifyDataSetChanged")
fun addItems(themeList: List<String>) {
this.themeList.clear()
this.themeList.addAll(themeList)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemHomeContentThemeBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun getItemCount() = themeList.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(themeList[position])
}
}

View File

@@ -14,11 +14,14 @@ import android.widget.Toast
import androidx.annotation.OptIn
import androidx.core.content.ContextCompat
import androidx.media3.common.util.UnstableApi
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.AudioContentPlayService
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.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerService
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity
import kr.co.vividnext.sodalive.base.BaseFragment
@@ -50,6 +53,9 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::infl
private lateinit var liveAdapter: HomeLiveAdapter
private lateinit var creatorRankingAdapter: CreatorRankingAdapter
private lateinit var latestContentThemeAdapter: HomeContentThemeAdapter
private lateinit var homeContentAdapter: HomeContentAdapter
private val handler = Handler(Looper.getMainLooper())
@@ -301,7 +307,111 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::infl
}
private fun setupLatestContent() {
val spSectionTitle = SpannableString(binding.tvNewContent.text)
spSectionTitle.setSpan(
ForegroundColorSpan(
ContextCompat.getColor(
requireContext(),
R.color.color_3bb9f1
)
),
0,
2,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
binding.tvNewContent.text = spSectionTitle
latestContentThemeAdapter = HomeContentThemeAdapter {
viewModel.getLatestContentByTheme(theme = it)
}
val rvTheme = binding.rvNewContentTheme
rvTheme.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
rvTheme.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()
}
latestContentThemeAdapter.itemCount - 1 -> {
outRect.left = 8f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 8f.dpToPx().toInt()
outRect.right = 8f.dpToPx().toInt()
}
}
}
})
rvTheme.adapter = latestContentThemeAdapter
viewModel.latestContentThemeListLiveData.observe(viewLifecycleOwner) {
binding.llNewContent.visibility = View.VISIBLE
latestContentThemeAdapter.addItems(it)
}
binding.tvNewContentAll.setOnClickListener {
if (SharedPreferenceManager.token.isNotBlank()) {
startActivity(Intent(requireContext(), AudioContentNewAllActivity::class.java))
} else {
(requireActivity() as MainActivity).showLoginActivity()
}
}
homeContentAdapter = HomeContentAdapter {
startActivity(
Intent(requireContext(), AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
)
}
val rvContent = binding.rvNewContent
rvContent.layoutManager = GridLayoutManager(context, 2, RecyclerView.HORIZONTAL, false)
rvContent.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.top = 8f.dpToPx().toInt()
outRect.bottom = 8f.dpToPx().toInt()
val position = parent.getChildAdapterPosition(view)
if (position == 0 || position == 1) {
outRect.left = 0f.dpToPx().toInt()
} else {
outRect.left = 8f.dpToPx().toInt()
}
outRect.right = 8f.dpToPx().toInt()
}
})
rvContent.adapter = homeContentAdapter
viewModel.latestContentListLiveData.observe(viewLifecycleOwner) {
homeContentAdapter.addItems(it)
}
}
private fun bindData() {

View File

@@ -90,7 +90,10 @@ class HomeViewModel(private val repository: HomeRepository) : BaseViewModel() {
if (it.success && data != null) {
_liveListLiveData.value = data.liveList
_creatorRankingLiveData.value = data.creatorRanking
_latestContentThemeListLiveData.value = data.latestContentThemeList
val themeList = data.latestContentThemeList.toMutableList()
themeList.add(0, "전체")
_latestContentThemeListLiveData.value = themeList
_latestContentListLiveData.value = data.latestContentList
_eventBannerListLiveData.value = data.bannerList
_originalAudioDramaListLiveData.value = data.originalAudioDramaList
@@ -121,7 +124,11 @@ class HomeViewModel(private val repository: HomeRepository) : BaseViewModel() {
compositeDisposable.add(
repository.getLatestContentByTheme(
theme,
theme = if (theme == "전체") {
""
} else {
theme
},
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
@@ -129,6 +136,9 @@ class HomeViewModel(private val repository: HomeRepository) : BaseViewModel() {
.subscribe(
{
_isLoading.value = false
if (it.success && it.data != null) {
_latestContentListLiveData.value = it.data!!
}
},
{
_isLoading.value = false

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#263238" />
<corners android:radius="999dp" />
</shape>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/color_3bb9f1" />
<corners android:radius="999dp" />
</shape>

View File

@@ -130,6 +130,7 @@
android:paddingHorizontal="24dp">
<TextView
android:id="@+id/tv_new_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/pretendard_bold"

View File

@@ -0,0 +1,58 @@
<?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:layout_width="160dp"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/iv_content_cover_image"
android:layout_width="160dp"
android:layout_height="160dp"
android:contentDescription="@null"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/iv_point"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:layout_marginEnd="6dp"
android:contentDescription="@null"
android:src="@drawable/ic_point"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="@+id/iv_content_cover_image"
app:layout_constraintTop_toTopOf="@+id/iv_content_cover_image" />
<TextView
android:id="@+id/tv_content_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:ellipsize="end"
android:fontFamily="@font/pretendard_regular"
android:maxLines="1"
android:textColor="@color/white"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="@+id/iv_content_cover_image"
app:layout_constraintStart_toStartOf="@+id/iv_content_cover_image"
app:layout_constraintTop_toBottomOf="@+id/iv_content_cover_image"
tools:text="빛이 나는 사람" />
<TextView
android:id="@+id/tv_nickname"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:ellipsize="end"
android:fontFamily="@font/pretendard_regular"
android:maxLines="1"
android:textColor="#78909C"
android:textSize="13.3sp"
app:layout_constraintEnd_toEndOf="@+id/tv_content_title"
app:layout_constraintStart_toStartOf="@+id/tv_content_title"
app:layout_constraintTop_toBottomOf="@+id/tv_content_title"
tools:text="빛이 나는 사람" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_theme"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@drawable/bg_round_corner_999_263238"
android:fontFamily="@font/pretendard_regular"
android:gravity="center"
android:paddingHorizontal="24dp"
android:paddingVertical="12dp"
android:textColor="@color/white"
android:textSize="16sp"
tools:text="자작곡" />
</FrameLayout>