feat(character list): 캐릭터 탭

- 배너 리스트 추가
- 배너, 캐릭터 클릭시 캐릭터 상세 페이지로 이동
This commit is contained in:
2025-08-13 00:05:39 +09:00
parent d8b48fe362
commit ff1e134fe4
7 changed files with 96 additions and 52 deletions

View File

@@ -0,0 +1,54 @@
package kr.co.vividnext.sodalive.chat.character
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
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentBannerResponse
class CharacterBannerAdapter(
private val context: Context,
private val itemWidth: Int,
private val itemHeight: Int,
private val onClick: (CharacterBannerResponse) -> Unit
) : BaseBannerAdapter<CharacterBannerResponse>() {
override fun bindData(
holder: BaseViewHolder<CharacterBannerResponse>,
data: CharacterBannerResponse,
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.imageUrl)
.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

@@ -2,15 +2,20 @@ package kr.co.vividnext.sodalive.chat.character
import androidx.annotation.Keep import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentBannerResponse
import kr.co.vividnext.sodalive.chat.character.curation.CurationSection import kr.co.vividnext.sodalive.chat.character.curation.CurationSection
import kr.co.vividnext.sodalive.chat.character.recent.RecentCharacter import kr.co.vividnext.sodalive.chat.character.recent.RecentCharacter
@Keep @Keep
data class CharacterHomeResponse( data class CharacterHomeResponse(
@SerializedName("banners") val banners: List<GetAudioContentBannerResponse>, @SerializedName("banners") val banners: List<CharacterBannerResponse>,
@SerializedName("recentCharacters") val recentCharacters: List<RecentCharacter>, @SerializedName("recentCharacters") val recentCharacters: List<RecentCharacter>,
@SerializedName("popularCharacters") val popularCharacters: List<Character>, @SerializedName("popularCharacters") val popularCharacters: List<Character>,
@SerializedName("newCharacters") val newCharacters: List<Character>, @SerializedName("newCharacters") val newCharacters: List<Character>,
@SerializedName("curationSections") val curationSections: List<CurationSection> @SerializedName("curationSections") val curationSections: List<CurationSection>
) )
@Keep
data class CharacterBannerResponse(
@SerializedName("characterId") val characterId: Long,
@SerializedName("imageUrl") val imageUrl: String
)

View File

@@ -2,7 +2,6 @@ package kr.co.vividnext.sodalive.chat.character
import android.content.Intent import android.content.Intent
import android.graphics.Rect import android.graphics.Rect
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.LinearLayout import android.widget.LinearLayout
@@ -16,21 +15,17 @@ import com.zhpan.bannerview.BaseBannerAdapter
import com.zhpan.indicator.enums.IndicatorSlideMode 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.main.AudioContentBannerType
import kr.co.vividnext.sodalive.audio_content.main.banner.AudioContentMainBannerAdapter
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
import kr.co.vividnext.sodalive.base.BaseFragment import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.chat.character.curation.CurationSectionAdapter import kr.co.vividnext.sodalive.chat.character.curation.CurationSectionAdapter
import kr.co.vividnext.sodalive.chat.character.detail.CharacterDetailActivity
import kr.co.vividnext.sodalive.chat.character.detail.CharacterDetailActivity.Companion.EXTRA_CHARACTER_ID
import kr.co.vividnext.sodalive.chat.character.recent.RecentCharacter import kr.co.vividnext.sodalive.chat.character.recent.RecentCharacter
import kr.co.vividnext.sodalive.chat.character.recent.RecentCharacterAdapter import kr.co.vividnext.sodalive.chat.character.recent.RecentCharacterAdapter
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.FragmentCharacterTabBinding import kr.co.vividnext.sodalive.databinding.FragmentCharacterTabBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.extensions.dpToPx import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.main.MainActivity import kr.co.vividnext.sodalive.main.MainActivity
import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
// 캐릭터 탭 프래그먼트 // 캐릭터 탭 프래그먼트
@@ -40,7 +35,7 @@ class CharacterTabFragment : BaseFragment<FragmentCharacterTabBinding>(
) { ) {
private val viewModel: CharacterTabViewModel by inject() private val viewModel: CharacterTabViewModel by inject()
private lateinit var contentBannerAdapter: AudioContentMainBannerAdapter private lateinit var contentBannerAdapter: CharacterBannerAdapter
private lateinit var recentCharacterAdapter: RecentCharacterAdapter private lateinit var recentCharacterAdapter: RecentCharacterAdapter
private lateinit var popularCharacterAdapter: CharacterAdapter private lateinit var popularCharacterAdapter: CharacterAdapter
private lateinit var newCharacterAdapter: CharacterAdapter private lateinit var newCharacterAdapter: CharacterAdapter
@@ -75,41 +70,17 @@ class CharacterTabFragment : BaseFragment<FragmentCharacterTabBinding>(
layoutParams.width = pagerWidth layoutParams.width = pagerWidth
layoutParams.height = pagerHeight layoutParams.height = pagerHeight
contentBannerAdapter = AudioContentMainBannerAdapter( contentBannerAdapter = CharacterBannerAdapter(
requireContext(), requireContext(),
pagerWidth, pagerWidth,
pagerHeight pagerHeight
) { ) {
if (SharedPreferenceManager.token.isNotBlank()) { if (SharedPreferenceManager.token.isNotBlank()) {
when (it.type) {
AudioContentBannerType.EVENT -> {
startActivity( startActivity(
Intent(requireContext(), EventDetailActivity::class.java).apply { Intent(requireContext(), CharacterDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_EVENT, it.eventItem!!) putExtra(EXTRA_CHARACTER_ID, it.characterId)
} }
) )
}
AudioContentBannerType.CREATOR -> {
startActivity(
Intent(requireContext(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it.creatorId!!)
}
)
}
AudioContentBannerType.SERIES -> {
startActivity(
Intent(requireContext(), SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, it.seriesId!!)
}
)
}
AudioContentBannerType.LINK -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.link!!)))
}
}
} else { } else {
(requireActivity() as MainActivity).showLoginActivity() (requireActivity() as MainActivity).showLoginActivity()
} }
@@ -401,6 +372,14 @@ class CharacterTabFragment : BaseFragment<FragmentCharacterTabBinding>(
} }
private fun onCharacterClick(character: Character) { private fun onCharacterClick(character: Character) {
// TODO: 캐릭터 클릭 처리 if (SharedPreferenceManager.token.isNotBlank()) {
startActivity(
Intent(requireContext(), CharacterDetailActivity::class.java).apply {
putExtra(EXTRA_CHARACTER_ID, character.id)
}
)
} else {
(requireActivity() as MainActivity).showLoginActivity()
}
} }
} }

View File

@@ -22,8 +22,8 @@ class CharacterTabViewModel(
val toastLiveData: LiveData<String?> val toastLiveData: LiveData<String?>
get() = _toastLiveData get() = _toastLiveData
private var _bannerListLiveData = MutableLiveData<List<GetAudioContentBannerResponse>>() private var _bannerListLiveData = MutableLiveData<List<CharacterBannerResponse>>()
val bannerListLiveData: LiveData<List<GetAudioContentBannerResponse>> val bannerListLiveData: LiveData<List<CharacterBannerResponse>>
get() = _bannerListLiveData get() = _bannerListLiveData
// 최근 대화한 캐릭터 LiveData // 최근 대화한 캐릭터 LiveData

View File

@@ -40,10 +40,10 @@ class CharacterDetailActivity : BaseActivity<ActivityCharacterDetailBinding>(
// 더미 데이터 로드 (추후 Intent/Repository 연동) // 더미 데이터 로드 (추후 Intent/Repository 연동)
val characterId = intent.getLongExtra(EXTRA_CHARACTER_ID, 0) val characterId = intent.getLongExtra(EXTRA_CHARACTER_ID, 0)
// if (characterId <= 0) { if (characterId <= 0) {
// showToast("잘못된 접근 입니다.") showToast("잘못된 접근 입니다.")
// finish() finish()
// } }
viewModel.loadMock(characterId) viewModel.loadMock(characterId)
bindObservers() bindObservers()

View File

@@ -17,7 +17,7 @@
android:id="@+id/ll_banner" android:id="@+id/ll_banner"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="48dp" android:layout_marginBottom="24dp"
android:orientation="vertical"> android:orientation="vertical">
<com.zhpan.bannerview.BannerViewPager <com.zhpan.bannerview.BannerViewPager
@@ -40,7 +40,7 @@
android:id="@+id/ll_latest_characters" android:id="@+id/ll_latest_characters"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="48dp" android:layout_marginBottom="24dp"
android:orientation="vertical"> android:orientation="vertical">
<!-- 제목 --> <!-- 제목 -->
@@ -87,7 +87,7 @@
android:id="@+id/ll_popular_characters" android:id="@+id/ll_popular_characters"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="48dp" android:layout_marginBottom="24dp"
android:orientation="vertical"> android:orientation="vertical">
<!-- 제목과 전체보기 --> <!-- 제목과 전체보기 -->
@@ -114,7 +114,8 @@
android:fontFamily="@font/pretendard_regular" android:fontFamily="@font/pretendard_regular"
android:text="전체보기" android:text="전체보기"
android:textColor="#90A4AE" android:textColor="#90A4AE"
android:textSize="14sp" /> android:textSize="14sp"
android:visibility="gone" />
</LinearLayout> </LinearLayout>
<!-- 캐릭터 카드 리스트 --> <!-- 캐릭터 카드 리스트 -->
@@ -132,7 +133,7 @@
android:id="@+id/ll_new_characters" android:id="@+id/ll_new_characters"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="48dp" android:layout_marginBottom="24dp"
android:orientation="vertical"> android:orientation="vertical">
<!-- 제목과 전체보기 --> <!-- 제목과 전체보기 -->
@@ -159,7 +160,8 @@
android:fontFamily="@font/pretendard_regular" android:fontFamily="@font/pretendard_regular"
android:text="전체보기" android:text="전체보기"
android:textColor="#90A4AE" android:textColor="#90A4AE"
android:textSize="14sp" /> android:textSize="14sp"
android:visibility="gone" />
</LinearLayout> </LinearLayout>

View File

@@ -47,7 +47,9 @@
android:id="@+id/tv_character_name" android:id="@+id/tv_character_name"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ellipsize="end"
android:fontFamily="@font/pretendard_regular" android:fontFamily="@font/pretendard_regular"
android:maxLines="1"
android:textColor="@color/color_b0bec5" android:textColor="@color/color_b0bec5"
android:textSize="18sp" /> android:textSize="18sp" />
@@ -56,7 +58,9 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:ellipsize="end"
android:fontFamily="@font/pretendard_regular" android:fontFamily="@font/pretendard_regular"
android:maxLines="1"
android:textColor="#78909C" android:textColor="#78909C"
android:textSize="14sp" /> android:textSize="14sp" />