fix(creator): 커뮤니티 썸네일 그리드를 보정한다

This commit is contained in:
2026-06-22 14:28:02 +09:00
parent 4288e7284b
commit 8f5c55e0d1
4 changed files with 115 additions and 57 deletions

View File

@@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.v2.creator.channel.community
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.core.view.doOnLayout
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.GridLayoutManager
@@ -17,6 +18,7 @@ import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityPostUiModel
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityViewMode
import kr.co.vividnext.sodalive.v2.creator.channel.community.ui.CreatorChannelCommunityGridAdapter
import kr.co.vividnext.sodalive.v2.creator.channel.community.ui.calculateCreatorChannelCommunityGridItemSize
import kr.co.vividnext.sodalive.v2.creator.channel.community.ui.CreatorChannelCommunityListAdapter
import org.koin.androidx.viewmodel.ext.android.viewModel
@@ -162,6 +164,7 @@ class CreatorChannelCommunityFragment : BaseFragment<FragmentCreatorChannelCommu
layoutManager = LinearLayoutManager(requireContext())
adapter = listAdapter
}
applyCommunityListPadding()
listAdapter.submitItems(state.communityPosts)
}
CreatorChannelCommunityViewMode.Grid -> {
@@ -169,11 +172,33 @@ class CreatorChannelCommunityFragment : BaseFragment<FragmentCreatorChannelCommu
layoutManager = GridLayoutManager(requireContext(), 3)
adapter = gridAdapter
}
applyCommunityGridPadding()
updateGridItemSize()
doOnLayout { updateGridItemSize() }
gridAdapter.submitItems(state.communityPosts)
}
}
}
private fun applyCommunityListPadding() = with(binding.rvCreatorChannelCommunity) {
updatePadding(
left = DEFAULT_LIST_HORIZONTAL_PADDING_DP.dpToPx().toInt(),
right = DEFAULT_LIST_HORIZONTAL_PADDING_DP.dpToPx().toInt()
)
}
private fun applyCommunityGridPadding() = with(binding.rvCreatorChannelCommunity) {
updatePadding(left = 0, right = 0)
}
private fun calculateGridItemSize(): Int = with(binding.rvCreatorChannelCommunity) {
return calculateCreatorChannelCommunityGridItemSize(width - paddingStart - paddingEnd)
}
private fun updateGridItemSize() {
gridAdapter.setItemSizePx(calculateGridItemSize())
}
private fun notifyContentChangedIfLayoutChanged(state: CreatorChannelCommunityUiState.Content) {
val contentLayoutKey = state.toContentLayoutKey()
if (contentLayoutKey == lastContentLayoutKey) return
@@ -205,6 +230,7 @@ class CreatorChannelCommunityFragment : BaseFragment<FragmentCreatorChannelCommu
companion object {
private const val ARG_CREATOR_ID: String = "arg_creator_id"
private const val DEFAULT_LIST_HORIZONTAL_PADDING_DP = 14
private const val DEFAULT_LIST_BOTTOM_PADDING_DP = 32
private const val OWNER_CTA_LIST_BOTTOM_PADDING_DP = 132

View File

@@ -4,8 +4,11 @@ import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.request.RequestOptions
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelCommunityGridBinding
import kr.co.vividnext.sodalive.extensions.loadUrl
import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityImageMode
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityPostUiModel
@@ -13,18 +16,26 @@ import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChanne
class CreatorChannelCommunityGridAdapter : RecyclerView.Adapter<CreatorChannelCommunityGridAdapter.ViewHolder>() {
private var items: List<CreatorChannelCommunityPostUiModel> = emptyList()
private var itemSizePx: Int = 0
fun submitItems(items: List<CreatorChannelCommunityPostUiModel>) {
this.items = items
notifyDataSetChanged()
}
fun setItemSizePx(itemSizePx: Int) {
if (this.itemSizePx == itemSizePx) return
this.itemSizePx = itemSizePx
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(ItemCreatorChannelCommunityGridBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
holder.bind(items[position], itemSizePx)
}
override fun getItemCount(): Int = items.size
@@ -33,49 +44,47 @@ class CreatorChannelCommunityGridAdapter : RecyclerView.Adapter<CreatorChannelCo
private val binding: ItemCreatorChannelCommunityGridBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: CreatorChannelCommunityPostUiModel) = with(binding) {
val squareSize = calculateSquareSize()
if (squareSize > 0) {
fun bind(item: CreatorChannelCommunityPostUiModel, itemSizePx: Int) = with(binding) {
if (itemSizePx > 0) {
root.layoutParams = root.layoutParams.apply {
width = squareSize
height = squareSize
width = itemSizePx
height = itemSizePx
}
}
val visibleImageUrl = item.imageUrl.takeIf { item.imageMode == CreatorChannelCommunityImageMode.Image }
ivCreatorChannelCommunityGridImage.isVisible = visibleImageUrl != null
if (visibleImageUrl != null) {
ivCreatorChannelCommunityGridImage.loadUrl(visibleImageUrl)
Glide.with(ivCreatorChannelCommunityGridImage)
.asBitmap()
.load(visibleImageUrl)
.placeholder(R.drawable.ic_place_holder)
.apply(communityImageRequestOptions())
.into(ivCreatorChannelCommunityGridImage)
} else {
Glide.with(ivCreatorChannelCommunityGridImage).clear(ivCreatorChannelCommunityGridImage)
ivCreatorChannelCommunityGridImage.setImageDrawable(null)
}
tvCreatorChannelCommunityGridTextPreview.isVisible = item.imageMode != CreatorChannelCommunityImageMode.Image
tvCreatorChannelCommunityGridTextPreview.isVisible =
!item.isLocked && item.imageMode != CreatorChannelCommunityImageMode.Image
tvCreatorChannelCommunityGridTextPreview.text = item.gridPreviewText
layoutCreatorChannelCommunityGridLockedOverlay.isVisible = item.isLocked
ivCreatorChannelCommunityGridLock.isVisible = item.isLocked
tvCreatorChannelCommunityGridLockPrice.isVisible = item.isLocked
tvCreatorChannelCommunityGridLockPrice.text = item.price.moneyFormat()
tvCreatorChannelCommunityGridNotice.isVisible = item.showNotice
ivCreatorChannelCommunityGridNotice.isVisible = item.showNotice
}
private fun calculateSquareSize(): Int {
val parent = binding.root.parent as? RecyclerView ?: return 0
val availableWidth = parent.width - parent.paddingStart - parent.paddingEnd
if (availableWidth <= 0) return 0
val itemHorizontalMargins = (binding.root.layoutParams as? ViewGroup.MarginLayoutParams)
?.let {
val leftMargin = it.leftMargin
val rightMargin = it.rightMargin
leftMargin + rightMargin
}
?: 0
val totalHorizontalMargins = itemHorizontalMargins * GRID_SPAN_COUNT
return (availableWidth - totalHorizontalMargins).coerceAtLeast(0) / GRID_SPAN_COUNT
private fun communityImageRequestOptions(): RequestOptions {
return RequestOptions().transform(CenterCrop())
}
}
companion object {
private const val GRID_SPAN_COUNT = 3
internal companion object {
const val GRID_SPAN_COUNT = 3
}
}
internal fun calculateCreatorChannelCommunityGridItemSize(availableWidth: Int): Int {
return availableWidth.coerceAtLeast(0) / CreatorChannelCommunityGridAdapter.GRID_SPAN_COUNT
}

View File

@@ -3,17 +3,15 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/layout_creator_channel_community_grid_root"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_margin="@dimen/spacing_4"
android:background="@drawable/bg_feed_card"
app:layout_constraintDimensionRatio="1:1">
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/gray_900">
<ImageView
android:id="@+id/iv_creator_channel_community_grid_image"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/bg_feed_community_image"
android:background="@color/gray_900"
android:contentDescription="@string/a11y_feed_content_image"
android:scaleType="centerCrop"
android:visibility="gone"
@@ -70,35 +68,33 @@
<TextView
android:id="@+id/tv_creator_channel_community_grid_lock_price"
style="@style/Typography.Caption2"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:layout_marginBottom="@dimen/spacing_12"
style="@style/Typography.Body3"
android:layout_width="70dp"
android:layout_height="36dp"
android:layout_marginTop="@dimen/spacing_4"
android:background="@drawable/bg_creator_channel_community_price"
android:drawableStart="@drawable/ic_bar_cash"
android:drawablePadding="@dimen/spacing_4"
android:drawablePadding="@dimen/spacing_6"
android:gravity="center"
android:includeFontPadding="false"
android:paddingHorizontal="@dimen/spacing_8"
android:textColor="@color/black"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/iv_creator_channel_community_grid_lock"
tools:text="30"
tools:visibility="visible" />
<TextView
android:id="@+id/tv_creator_channel_community_grid_notice"
style="@style/Typography.Caption2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/spacing_8"
android:includeFontPadding="false"
android:text="@string/creator_channel_community_notice"
android:textColor="@color/soda_400"
<ImageView
android:id="@+id/iv_creator_channel_community_grid_notice"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_gravity="top|end"
android:layout_margin="6dp"
android:contentDescription="@null"
android:src="@drawable/ic_pin"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -9,6 +9,7 @@ import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ApplicationProvider
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.v2.creator.channel.community.ui.calculateCreatorChannelCommunityGridItemSize
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertSame
@@ -80,13 +81,14 @@ class CreatorChannelCommunityFragmentLayoutTest {
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_list_comment_count))
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_list_like_count))
assertNotNull(item.findViewById<View>(R.id.layout_creator_channel_community_list_top_actions))
assertNotNull(item.findViewById<View>(R.id.layout_creator_channel_community_list_top_price))
assertNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_community_list_owner_more))
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_list_top_price))
assertSame(imageContainer, (lockIcon.parent as View).parent)
assertSame(imageContainer, (lockedPrice.parent as View).parent)
assertSame(
item.findViewById<View>(R.id.layout_creator_channel_community_list_top_actions),
item.findViewById<TextView>(R.id.tv_creator_channel_community_list_top_price).parent
item.findViewById<View>(R.id.layout_creator_channel_community_list_top_price).parent
)
assertTrue(itemLayout.contains("android:id=\"@+id/layout_creator_channel_community_list_locked_overlay\""))
assertTrue(itemLayout.contains("android:id=\"@+id/tv_creator_channel_community_list_locked_price\""))
@@ -103,9 +105,16 @@ class CreatorChannelCommunityFragmentLayoutTest {
assertNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_community_grid_image))
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_grid_text_preview))
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_grid_lock_price))
assertNotNull(item.findViewById<TextView>(R.id.tv_creator_channel_community_grid_notice))
assertNotNull(item.findViewById<ImageView>(R.id.iv_creator_channel_community_grid_notice))
assertTrue(itemLayout.contains("android:id=\"@+id/layout_creator_channel_community_grid_root\""))
assertTrue(itemLayout.contains("app:layout_constraintDimensionRatio=\"1:1\"") || itemLayout.contains("Square"))
assertTrue(itemLayout.contains("android:layout_width=\"wrap_content\""))
assertTrue(itemLayout.contains("android:layout_height=\"wrap_content\""))
assertTrue(!itemLayout.contains("android:layout_margin=\"@dimen/spacing_4\""))
assertTrue(!itemLayout.contains("android:background=\"@drawable/bg_feed_card\""))
assertTrue(!itemLayout.contains("android:background=\"@drawable/bg_feed_community_image\""))
assertTrue(itemLayout.contains("android:id=\"@+id/iv_creator_channel_community_grid_notice\""))
assertTrue(itemLayout.contains("android:src=\"@drawable/ic_pin\""))
assertTrue(itemLayout.contains("android:layout_gravity=\"top|end\""))
}
@Test
@@ -142,6 +151,10 @@ class CreatorChannelCommunityFragmentLayoutTest {
fragment.contains("GridLayoutManager(requireContext(), 3") ||
fragment.contains("GridLayoutManager(context, 3")
)
assertTrue(fragment.contains("applyCommunityGridPadding"))
assertTrue(fragment.contains("updateGridItemSize()"))
assertTrue(fragment.contains("doOnLayout"))
assertTrue(fragment.contains("calculateCreatorChannelCommunityGridItemSize("))
assertTrue(fragment.contains("viewModel.consumePaginationErrorMessage()"))
assertTrue(fragment.contains("applyOwnerCtaPadding"))
assertTrue(fragment.contains("pauseContent"))
@@ -171,8 +184,14 @@ class CreatorChannelCommunityFragmentLayoutTest {
assertTrue(listAdapter.contains("R.drawable.ic_new_player_play"))
assertTrue(listAdapter.contains("onOwnerMoreClick(item)"))
assertTrue(listAdapter.contains("tvCreatorChannelCommunityListLockedPrice.isVisible = item.isLocked"))
assertTrue(listAdapter.contains("tvCreatorChannelCommunityListTopPrice.isVisible = item.showOwnerTopPrice"))
assertTrue(listAdapter.contains("layoutCreatorChannelCommunityListTopPrice.isVisible = item.showOwnerTopPrice"))
assertTrue(!listAdapter.contains("item.isLocked || item.showOwnerTopPrice"))
assertTrue(gridAdapter.contains(".asBitmap()"))
assertTrue(gridAdapter.contains("CenterCrop()"))
assertTrue(!gridAdapter.contains("RoundedCorners"))
assertTrue(!gridAdapter.contains("14f.dpToPx()"))
assertTrue(!gridAdapter.contains("root.clipToOutline = true"))
assertTrue(!listAdapter.contains(".asBitmap()"))
assertTrue(!listAdapter.contains("root.setOnClickListener"))
assertTrue(!gridAdapter.contains("root.setOnClickListener"))
}
@@ -205,18 +224,26 @@ class CreatorChannelCommunityFragmentLayoutTest {
assertTrue(listAdapter.contains("ivCreatorChannelCommunityListImage.setImageDrawable(null)"))
assertTrue(gridAdapter.contains("val visibleImageUrl = item.imageUrl.takeIf"))
assertTrue(gridAdapter.contains("item.imageMode == CreatorChannelCommunityImageMode.Image"))
assertTrue(gridAdapter.contains("!item.isLocked && item.imageMode != CreatorChannelCommunityImageMode.Image"))
assertTrue(gridAdapter.contains("ivCreatorChannelCommunityGridImage.setImageDrawable(null)"))
}
@Test
fun `커뮤니티 grid adapter source는 margin을 제외한 3열 정사각 크기를 계산한다`() {
fun `커뮤니티 grid adapter source는 레거시처럼 itemSize로 root 정사각 크기를 고정한다`() {
val gridAdapter = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/ui/CreatorChannelCommunityGridAdapter.kt"
).readText()
assertTrue(gridAdapter.contains("leftMargin + rightMargin"))
assertTrue(gridAdapter.contains("availableWidth - totalHorizontalMargins"))
assertTrue(gridAdapter.contains("coerceAtLeast(0)"))
assertEquals(120, calculateCreatorChannelCommunityGridItemSize(360))
assertEquals(134, calculateCreatorChannelCommunityGridItemSize(402))
assertEquals(0, calculateCreatorChannelCommunityGridItemSize(-1))
assertTrue(gridAdapter.contains("setItemSizePx"))
assertTrue(gridAdapter.contains("private var itemSizePx: Int = 0"))
assertTrue(gridAdapter.contains("calculateCreatorChannelCommunityGridItemSize("))
assertTrue(gridAdapter.contains("width = itemSizePx"))
assertTrue(gridAdapter.contains("height = itemSizePx"))
assertTrue(!gridAdapter.contains("itemHorizontalMargins"))
assertTrue(!gridAdapter.contains("totalHorizontalMargins"))
}
private fun inflateView(layoutResId: Int): View {