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