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(
|
class CharacterGalleryAdapter(
|
||||||
private var items: List<CharacterImageListItemResponse> = emptyList(),
|
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>() {
|
) : RecyclerView.Adapter<CharacterGalleryAdapter.ViewHolder>() {
|
||||||
|
|
||||||
inner class ViewHolder(
|
inner class ViewHolder(
|
||||||
@@ -26,12 +27,17 @@ class CharacterGalleryAdapter(
|
|||||||
if (item.isOwned) {
|
if (item.isOwned) {
|
||||||
binding.llLock.visibility = View.GONE
|
binding.llLock.visibility = View.GONE
|
||||||
binding.btnBuy.setOnClickListener(null)
|
binding.btnBuy.setOnClickListener(null)
|
||||||
|
binding.root.setOnClickListener {
|
||||||
|
onClickOwned(item, bindingAdapterPosition)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
binding.llLock.visibility = View.VISIBLE
|
binding.llLock.visibility = View.VISIBLE
|
||||||
binding.tvPrice.text = item.imagePriceCan.toString()
|
binding.tvPrice.text = item.imagePriceCan.toString()
|
||||||
binding.btnBuy.setOnClickListener {
|
binding.btnBuy.setOnClickListener {
|
||||||
onClickBuy(item, bindingAdapterPosition)
|
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.annotation.SuppressLint
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.text.style.ForegroundColorSpan
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.core.graphics.toColorInt
|
import androidx.core.graphics.toColorInt
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
@@ -26,6 +27,8 @@ class CharacterGalleryFragment : BaseFragment<FragmentCharacterGalleryBinding>(
|
|||||||
private lateinit var adapter: CharacterGalleryAdapter
|
private lateinit var adapter: CharacterGalleryAdapter
|
||||||
private lateinit var loadingDialog: LoadingDialog
|
private lateinit var loadingDialog: LoadingDialog
|
||||||
|
|
||||||
|
private var latestItems: List<CharacterImageListItemResponse> = emptyList()
|
||||||
|
|
||||||
private val characterId: Long by lazy {
|
private val characterId: Long by lazy {
|
||||||
arguments?.getLong("arg_character_id")
|
arguments?.getLong("arg_character_id")
|
||||||
?: requireActivity().intent.getLongExtra(EXTRA_CHARACTER_ID, 0L)
|
?: requireActivity().intent.getLongExtra(EXTRA_CHARACTER_ID, 0L)
|
||||||
@@ -49,9 +52,24 @@ class CharacterGalleryFragment : BaseFragment<FragmentCharacterGalleryBinding>(
|
|||||||
GridSpacingItemDecoration(3, 2f.dpToPx().toInt(), true)
|
GridSpacingItemDecoration(3, 2f.dpToPx().toInt(), true)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
adapter = CharacterGalleryAdapter(onClickBuy = { item, position ->
|
adapter = CharacterGalleryAdapter(
|
||||||
showPurchaseDialog(item, position)
|
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.adapter = adapter
|
||||||
|
|
||||||
binding.rvGallery.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
binding.rvGallery.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
@@ -88,7 +106,6 @@ class CharacterGalleryFragment : BaseFragment<FragmentCharacterGalleryBinding>(
|
|||||||
binding.clRatio.visibility = View.VISIBLE
|
binding.clRatio.visibility = View.VISIBLE
|
||||||
|
|
||||||
val percent = (state.ratio * 100).toInt()
|
val percent = (state.ratio * 100).toInt()
|
||||||
// 좌측 라벨은 고정("보유중"), 우측은 보유/전체 개수(ownedCount만 강조 색상 적용)
|
|
||||||
binding.tvRatioLeft.text = "$percent% 보유중"
|
binding.tvRatioLeft.text = "$percent% 보유중"
|
||||||
|
|
||||||
val ownedStr = state.ownedCount.toString()
|
val ownedStr = state.ownedCount.toString()
|
||||||
@@ -97,7 +114,7 @@ class CharacterGalleryFragment : BaseFragment<FragmentCharacterGalleryBinding>(
|
|||||||
val spannable = android.text.SpannableString(fullText)
|
val spannable = android.text.SpannableString(fullText)
|
||||||
val ownedColor = "#FDD453".toColorInt()
|
val ownedColor = "#FDD453".toColorInt()
|
||||||
spannable.setSpan(
|
spannable.setSpan(
|
||||||
android.text.style.ForegroundColorSpan(ownedColor),
|
ForegroundColorSpan(ownedColor),
|
||||||
/* start */ 0,
|
/* start */ 0,
|
||||||
/* end */ ownedStr.length,
|
/* end */ ownedStr.length,
|
||||||
android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
@@ -108,6 +125,7 @@ class CharacterGalleryFragment : BaseFragment<FragmentCharacterGalleryBinding>(
|
|||||||
// 슬라이더(ProgressBar) 값 설정: 0~100
|
// 슬라이더(ProgressBar) 값 설정: 0~100
|
||||||
binding.progressRatio.progress = percent
|
binding.progressRatio.progress = percent
|
||||||
|
|
||||||
|
latestItems = state.items
|
||||||
adapter.submitItems(state.items)
|
adapter.submitItems(state.items)
|
||||||
} else {
|
} else {
|
||||||
binding.rvGallery.visibility = View.VISIBLE
|
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:id="@+id/tv_desc"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="6dp"
|
android:layout_marginTop="12dp"
|
||||||
android:fontFamily="@font/gmarket_sans_medium"
|
android:fontFamily="@font/gmarket_sans_medium"
|
||||||
android:textColor="@color/color_bbbbbb"
|
android:textColor="@color/color_bbbbbb"
|
||||||
android:textSize="15sp"
|
android:textSize="15sp"
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="45dp"
|
android:layout_marginTop="24dp"
|
||||||
android:layout_marginBottom="16.7dp"
|
android:layout_marginBottom="16.7dp"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
android:id="@+id/tv_confirm"
|
android:id="@+id/tv_confirm"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="2"
|
||||||
android:background="@drawable/bg_round_corner_10_3bb9f1"
|
android:background="@drawable/bg_round_corner_10_3bb9f1"
|
||||||
android:fontFamily="@font/gmarket_sans_bold"
|
android:fontFamily="@font/gmarket_sans_bold"
|
||||||
android:gravity="center"
|
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