feat(character-gallery): 구매 이미지 전체화면 Carousel 뷰어 추가
구매된 이미지를 탭하면 전체화면 DialogFragment로 열리고, ViewPager2 기반 Carousel로 좌우 슬라이딩 탐색이 가능하도록 구현.
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ->
|
||||
showPurchaseDialog(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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
14
app/src/main/res/layout/item_fullscreen_image.xml
Normal file
14
app/src/main/res/layout/item_fullscreen_image.xml
Normal 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>
|
||||
Reference in New Issue
Block a user