feat(character-gallery): 구매 이미지 전체화면 Carousel 뷰어 추가

구매된 이미지를 탭하면 전체화면 DialogFragment로 열리고,
ViewPager2 기반 Carousel로 좌우 슬라이딩 탐색이 가능하도록 구현.
This commit is contained in:
2025-08-23 01:48:57 +09:00
parent 770c4179a3
commit 6c57c5a98a
6 changed files with 165 additions and 9 deletions

View File

@@ -11,7 +11,8 @@ import kr.co.vividnext.sodalive.databinding.ItemCharacterGalleryBinding
class CharacterGalleryAdapter(
private var items: List<CharacterImageListItemResponse> = emptyList(),
private val onClickBuy: (item: CharacterImageListItemResponse, position: Int) -> Unit = { _, _ -> }
private val onClickBuy: (item: CharacterImageListItemResponse, position: Int) -> Unit = { _, _ -> },
private val onClickOwned: (item: CharacterImageListItemResponse, position: Int) -> Unit = { _, _ -> }
) : RecyclerView.Adapter<CharacterGalleryAdapter.ViewHolder>() {
inner class ViewHolder(
@@ -26,12 +27,17 @@ class CharacterGalleryAdapter(
if (item.isOwned) {
binding.llLock.visibility = View.GONE
binding.btnBuy.setOnClickListener(null)
binding.root.setOnClickListener {
onClickOwned(item, bindingAdapterPosition)
}
} else {
binding.llLock.visibility = View.VISIBLE
binding.tvPrice.text = item.imagePriceCan.toString()
binding.btnBuy.setOnClickListener {
onClickBuy(item, bindingAdapterPosition)
}
// 잠금 상태에서는 아이템 클릭 시 아무 동작 없음 (구매 버튼만 활성)
binding.root.setOnClickListener(null)
}
}
}

View File

@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.chat.character.detail.gallery
import android.annotation.SuppressLint
import android.os.Bundle
import android.text.style.ForegroundColorSpan
import android.view.View
import androidx.core.graphics.toColorInt
import androidx.recyclerview.widget.GridLayoutManager
@@ -26,6 +27,8 @@ class CharacterGalleryFragment : BaseFragment<FragmentCharacterGalleryBinding>(
private lateinit var adapter: CharacterGalleryAdapter
private lateinit var loadingDialog: LoadingDialog
private var latestItems: List<CharacterImageListItemResponse> = emptyList()
private val characterId: Long by lazy {
arguments?.getLong("arg_character_id")
?: requireActivity().intent.getLongExtra(EXTRA_CHARACTER_ID, 0L)
@@ -49,9 +52,24 @@ class CharacterGalleryFragment : BaseFragment<FragmentCharacterGalleryBinding>(
GridSpacingItemDecoration(3, 2f.dpToPx().toInt(), true)
)
}
adapter = CharacterGalleryAdapter(onClickBuy = { item, position ->
adapter = CharacterGalleryAdapter(
onClickBuy = { item, position ->
showPurchaseDialog(item, position)
})
},
onClickOwned = { item, position ->
// 구매된 항목만 전체화면 뷰어로 진입
val ownedItems = latestItems.filter { it.isOwned }
if (ownedItems.isEmpty()) return@CharacterGalleryAdapter
val startIndex = ownedItems.indexOfFirst {
it.id == item.id
}.coerceAtLeast(0)
val urls = ownedItems.map { it.imageUrl }
val dialog = CharacterGalleryViewerDialogFragment.newInstance(urls, startIndex)
if (!dialog.isAdded) {
dialog.show(parentFragmentManager, "CharacterGalleryViewerDialog")
}
}
)
binding.rvGallery.adapter = adapter
binding.rvGallery.addOnScrollListener(object : RecyclerView.OnScrollListener() {
@@ -88,7 +106,6 @@ class CharacterGalleryFragment : BaseFragment<FragmentCharacterGalleryBinding>(
binding.clRatio.visibility = View.VISIBLE
val percent = (state.ratio * 100).toInt()
// 좌측 라벨은 고정("보유중"), 우측은 보유/전체 개수(ownedCount만 강조 색상 적용)
binding.tvRatioLeft.text = "$percent% 보유중"
val ownedStr = state.ownedCount.toString()
@@ -97,7 +114,7 @@ class CharacterGalleryFragment : BaseFragment<FragmentCharacterGalleryBinding>(
val spannable = android.text.SpannableString(fullText)
val ownedColor = "#FDD453".toColorInt()
spannable.setSpan(
android.text.style.ForegroundColorSpan(ownedColor),
ForegroundColorSpan(ownedColor),
/* start */ 0,
/* end */ ownedStr.length,
android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
@@ -108,6 +125,7 @@ class CharacterGalleryFragment : BaseFragment<FragmentCharacterGalleryBinding>(
// 슬라이더(ProgressBar) 값 설정: 0~100
binding.progressRatio.progress = percent
latestItems = state.items
adapter.submitItems(state.items)
} else {
binding.rvGallery.visibility = View.VISIBLE

View File

@@ -0,0 +1,89 @@
package kr.co.vividnext.sodalive.chat.character.detail.gallery
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import kr.co.vividnext.sodalive.databinding.FragmentCharacterGalleryViewerBinding
import kr.co.vividnext.sodalive.databinding.ItemFullscreenImageBinding
class CharacterGalleryViewerDialogFragment : DialogFragment() {
private var _binding: FragmentCharacterGalleryViewerBinding? = null
private val binding get() = _binding!!
private val imageUrls: ArrayList<String> by lazy {
arguments?.getStringArrayList(ARG_URLS) ?: arrayListOf()
}
private val startIndex: Int by lazy {
arguments?.getInt(ARG_START_INDEX) ?: 0
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NORMAL, android.R.style.Theme_Black_NoTitleBar_Fullscreen)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentCharacterGalleryViewerBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.viewPager.adapter = ImagePagerAdapter(imageUrls)
if (startIndex in imageUrls.indices) {
binding.viewPager.setCurrentItem(startIndex, false)
}
binding.btnClose.setOnClickListener { dismissAllowingStateLoss() }
}
override fun onDestroyView() {
_binding = null
super.onDestroyView()
}
class ImagePagerAdapter(private val urls: List<String>) : RecyclerView.Adapter<ImageViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder {
val binding = ItemFullscreenImageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ImageViewHolder(binding)
}
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
holder.bind(urls[position])
}
override fun getItemCount(): Int = urls.size
}
class ImageViewHolder(private val binding: ItemFullscreenImageBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(url: String) {
Glide.with(binding.ivFull)
.load(url)
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
.into(binding.ivFull)
}
}
companion object {
private const val ARG_URLS = "arg_urls"
private const val ARG_START_INDEX = "arg_start_index"
fun newInstance(urls: List<String>, startIndex: Int): CharacterGalleryViewerDialogFragment {
val fragment = CharacterGalleryViewerDialogFragment()
fragment.arguments = Bundle().apply {
putStringArrayList(ARG_URLS, ArrayList(urls))
putInt(ARG_START_INDEX, startIndex)
}
return fragment
}
}
}

View File

@@ -26,7 +26,7 @@
android:id="@+id/tv_desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:layout_marginTop="12dp"
android:fontFamily="@font/gmarket_sans_medium"
android:textColor="@color/color_bbbbbb"
android:textSize="15sp"
@@ -38,7 +38,7 @@
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="45dp"
android:layout_marginTop="24dp"
android:layout_marginBottom="16.7dp"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
@@ -64,7 +64,7 @@
android:id="@+id/tv_confirm"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_weight="2"
android:background="@drawable/bg_round_corner_10_3bb9f1"
android:fontFamily="@font/gmarket_sans_bold"
android:gravity="center"

View File

@@ -0,0 +1,29 @@
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000000">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<ImageButton
android:id="@+id/btnClose"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_margin="16dp"
android:background="@android:color/transparent"
android:contentDescription="close"
android:scaleType="center"
android:src="@android:drawable/ic_menu_close_clear_cancel"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000000">
<ImageView
android:id="@+id/ivFull"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:scaleType="fitCenter" />
</FrameLayout>