fix(content): 랭킹 카드 간격과 이미지를 보정한다
This commit is contained in:
@@ -274,6 +274,7 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
||||
binding.rvContentRankings.apply {
|
||||
layoutManager = ContentRankingAdapter.createGridLayoutManager(requireContext())
|
||||
adapter = contentRankingAdapter
|
||||
addItemDecoration(ContentRankingAdapter.createItemDecoration(requireContext()))
|
||||
}
|
||||
val contentAllGridLayoutManager = GridLayoutManager(requireContext(), CONTENT_ALL_GRID_SPAN_COUNT)
|
||||
binding.rvContentAllItems.apply {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.contentranking
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
@@ -27,19 +29,35 @@ class ContentRankingAdapter(
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
return when (viewType) {
|
||||
VIEW_TYPE_LARGE -> LargeViewHolder(
|
||||
inflater.inflate(R.layout.view_content_ranking_large_card, parent, false) as ContentRankingLargeCardView,
|
||||
inflater.inflate(
|
||||
R.layout.view_content_ranking_large_card,
|
||||
parent,
|
||||
false
|
||||
) as ContentRankingLargeCardView,
|
||||
parent
|
||||
)
|
||||
VIEW_TYPE_MEDIUM_GRID -> MediumGridViewHolder(
|
||||
inflater.inflate(R.layout.view_content_ranking_medium_grid_card, parent, false) as ContentRankingMediumGridCardView,
|
||||
inflater.inflate(
|
||||
R.layout.view_content_ranking_medium_grid_card,
|
||||
parent,
|
||||
false
|
||||
) as ContentRankingMediumGridCardView,
|
||||
parent
|
||||
)
|
||||
VIEW_TYPE_SMALL_GRID -> SmallGridViewHolder(
|
||||
inflater.inflate(R.layout.view_content_ranking_small_grid_card, parent, false) as ContentRankingSmallGridCardView,
|
||||
inflater.inflate(
|
||||
R.layout.view_content_ranking_small_grid_card,
|
||||
parent,
|
||||
false
|
||||
) as ContentRankingSmallGridCardView,
|
||||
parent
|
||||
)
|
||||
VIEW_TYPE_HORIZONTAL -> HorizontalViewHolder(
|
||||
inflater.inflate(R.layout.view_content_ranking_horizontal_card, parent, false) as ContentRankingHorizontalCardView,
|
||||
inflater.inflate(
|
||||
R.layout.view_content_ranking_horizontal_card,
|
||||
parent,
|
||||
false
|
||||
) as ContentRankingHorizontalCardView,
|
||||
parent
|
||||
)
|
||||
else -> error("Unknown viewType: $viewType")
|
||||
@@ -71,8 +89,8 @@ class ContentRankingAdapter(
|
||||
fun bind(item: ContentRankingItem) {
|
||||
val size = calculateSize(item, parent)
|
||||
view.setCardSize(size)
|
||||
view.imageView().loadContentImage(item)
|
||||
view.backgroundImageView().loadContentImage(item)
|
||||
view.backgroundImageView().loadContentImage(item, blurEnabled = true)
|
||||
view.imageView().loadContentImage(item, blurEnabled = false)
|
||||
view.bind(item)
|
||||
view.setOnContentClick(onClickItem)
|
||||
}
|
||||
@@ -121,9 +139,12 @@ class ContentRankingAdapter(
|
||||
view.setOnContentClick(onClickItem)
|
||||
}
|
||||
|
||||
private fun ImageView.loadContentImage(item: ContentRankingItem) {
|
||||
private fun ImageView.loadContentImage(
|
||||
item: ContentRankingItem,
|
||||
blurEnabled: Boolean = item.isInaccessible
|
||||
) {
|
||||
loadUrl(item.imageUrl) {
|
||||
val blurTransformations = ContentRankingBlur.transformations(context, item.isInaccessible)
|
||||
val blurTransformations = ContentRankingBlur.transformations(context, blurEnabled)
|
||||
if (blurTransformations.isNotEmpty()) {
|
||||
transformations(blurTransformations)
|
||||
}
|
||||
@@ -148,23 +169,47 @@ class ContentRankingAdapter(
|
||||
companion object {
|
||||
const val GRID_SPAN_COUNT = 6
|
||||
|
||||
fun createGridLayoutManager(context: Context): GridLayoutManager = GridLayoutManager(context, GRID_SPAN_COUNT).apply {
|
||||
spanSizeLookup = createSpanSizeLookup()
|
||||
}
|
||||
fun createGridLayoutManager(context: Context): GridLayoutManager =
|
||||
GridLayoutManager(context, GRID_SPAN_COUNT).apply {
|
||||
spanSizeLookup = createSpanSizeLookup()
|
||||
}
|
||||
|
||||
fun createSpanSizeLookup(): GridLayoutManager.SpanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||
override fun getSpanSize(position: Int): Int = when (ContentRankingPlacement.fromRank(position + 1).itemsPerRow) {
|
||||
1 -> GRID_SPAN_COUNT
|
||||
2 -> GRID_SPAN_COUNT / 2
|
||||
3 -> GRID_SPAN_COUNT / 3
|
||||
else -> GRID_SPAN_COUNT
|
||||
fun createItemDecoration(context: Context): RecyclerView.ItemDecoration {
|
||||
val spacingPx = HORIZONTAL_GAP_DP.dpToPx(context)
|
||||
return object : RecyclerView.ItemDecoration() {
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
val position = parent.getChildAdapterPosition(view)
|
||||
val itemCount = parent.adapter?.itemCount ?: return
|
||||
if (position != RecyclerView.NO_POSITION && position < itemCount - 1) {
|
||||
outRect.bottom = spacingPx
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createSpanSizeLookup(): GridLayoutManager.SpanSizeLookup =
|
||||
object : GridLayoutManager.SpanSizeLookup() {
|
||||
override fun getSpanSize(position: Int): Int =
|
||||
when (ContentRankingPlacement.fromRank(position + 1).itemsPerRow) {
|
||||
1 -> GRID_SPAN_COUNT
|
||||
2 -> GRID_SPAN_COUNT / 2
|
||||
3 -> GRID_SPAN_COUNT / 3
|
||||
else -> GRID_SPAN_COUNT
|
||||
}
|
||||
}
|
||||
|
||||
private const val VIEW_TYPE_LARGE = 1
|
||||
private const val VIEW_TYPE_MEDIUM_GRID = 2
|
||||
private const val VIEW_TYPE_SMALL_GRID = 3
|
||||
private const val VIEW_TYPE_HORIZONTAL = 4
|
||||
private const val HORIZONTAL_GAP_DP = 4
|
||||
private fun Int.dpToPx(context: Context): Int =
|
||||
(this * context.resources.displayMetrics.density).roundToInt()
|
||||
|
||||
private const val HORIZONTAL_GAP_DP = 8
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,9 +144,10 @@ class ContentRankingLargeCardView @JvmOverloads constructor(
|
||||
|
||||
private fun applyBlur(enabled: Boolean) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val effect = if (enabled) RenderEffect.createBlurEffect(16f, 16f, Shader.TileMode.CLAMP) else null
|
||||
contentImageView().setRenderEffect(effect)
|
||||
backgroundImageView().setRenderEffect(effect)
|
||||
val contentEffect = if (enabled) RenderEffect.createBlurEffect(16f, 16f, Shader.TileMode.CLAMP) else null
|
||||
val backgroundEffect = RenderEffect.createBlurEffect(16f, 16f, Shader.TileMode.CLAMP)
|
||||
contentImageView().setRenderEffect(contentEffect)
|
||||
backgroundImageView().setRenderEffect(backgroundEffect)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ class ContentMainFragmentSourceTest {
|
||||
assertTrue(source.contains("ContentCommentedAudioAdapter"))
|
||||
assertTrue(source.contains("ContentRankingAdapter"))
|
||||
assertTrue(source.contains("ContentRankingAdapter.createGridLayoutManager(requireContext())"))
|
||||
assertTrue(source.contains("ContentRankingAdapter.createItemDecoration(requireContext())"))
|
||||
assertTrue(source.contains("recommendationsStateLiveData.observe(viewLifecycleOwner)"))
|
||||
assertTrue(source.contains("rankingStateLiveData.observe(viewLifecycleOwner)"))
|
||||
assertTrue(source.contains("contentMainViewModel.loadRecommendations()"))
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.contentranking
|
||||
|
||||
import android.os.Build
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class ContentRankingBlurTest {
|
||||
|
||||
@Test
|
||||
fun `pre S blur는 enabled true이면 Coil transformation을 사용한다`() {
|
||||
assertTrue(ContentRankingBlur.shouldUseCoilBlur(enabled = true, sdkInt = Build.VERSION_CODES.R))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `S 이상 blur는 RenderEffect를 사용하므로 Coil transformation을 사용하지 않는다`() {
|
||||
assertFalse(ContentRankingBlur.shouldUseCoilBlur(enabled = true, sdkInt = Build.VERSION_CODES.S))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `pre S blur는 enabled false이면 Coil transformation을 사용하지 않는다`() {
|
||||
assertFalse(ContentRankingBlur.shouldUseCoilBlur(enabled = false, sdkInt = Build.VERSION_CODES.R))
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,12 @@ import androidx.test.core.app.ApplicationProvider
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.v2.widget.ranking.RankingChangeType.Increase
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import java.io.File
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [28], application = Application::class)
|
||||
@@ -136,6 +138,33 @@ class ContentRankingCardViewTest {
|
||||
assertEquals(View.VISIBLE, view.findViewById<View>(R.id.ll_content_ranking_delta).visibility)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `large card는 전경 1대1 이미지와 배경 이미지를 함께 유지한다`() {
|
||||
val view = inflateView<ContentRankingLargeCardView>(R.layout.view_content_ranking_large_card)
|
||||
|
||||
view.bind(sampleItem())
|
||||
|
||||
assertEquals(View.VISIBLE, view.findViewById<View>(R.id.iv_content_ranking_image).visibility)
|
||||
assertEquals(View.VISIBLE, view.findViewById<View>(R.id.iv_content_ranking_background).visibility)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `large view holder는 배경 blur와 전경 non blur coverImage를 모두 로드한다`() {
|
||||
val source = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingAdapter.kt"
|
||||
).readText()
|
||||
val largeViewHolderSource = source
|
||||
.substringAfter("private inner class LargeViewHolder")
|
||||
.substringBefore("private inner class MediumGridViewHolder")
|
||||
|
||||
assertTrue(
|
||||
largeViewHolderSource.contains("view.backgroundImageView().loadContentImage(item, blurEnabled = true)")
|
||||
)
|
||||
assertTrue(
|
||||
largeViewHolderSource.contains("view.imageView().loadContentImage(item, blurEnabled = false)")
|
||||
)
|
||||
}
|
||||
|
||||
private inline fun <reified T : View> inflateView(layoutResId: Int): T {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
return LayoutInflater.from(context).inflate(layoutResId, null, false) as T
|
||||
@@ -175,4 +204,10 @@ class ContentRankingCardViewTest {
|
||||
isBlocked = false,
|
||||
showRankChange = showRankChange
|
||||
)
|
||||
|
||||
private fun projectFile(relativePath: String): File {
|
||||
val candidates = listOf(File(relativePath), File("../$relativePath"))
|
||||
return candidates.firstOrNull { it.exists() }
|
||||
?: error("Project file not found: $relativePath")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ class ContentRankingLayoutCalculatorTest {
|
||||
fun `large item keeps figma large ratio`() {
|
||||
val size = ContentRankingLayoutCalculator.calculate(
|
||||
parentWidthPx = 374,
|
||||
horizontalGapPx = 4,
|
||||
horizontalGapPx = 8,
|
||||
placement = ContentRankingPlacement(ContentRankingCardVariant.Large, itemsPerRow = 1)
|
||||
)
|
||||
|
||||
@@ -21,31 +21,31 @@ class ContentRankingLayoutCalculatorTest {
|
||||
fun `medium grid item width divides available width by items per row`() {
|
||||
val size = ContentRankingLayoutCalculator.calculate(
|
||||
parentWidthPx = 374,
|
||||
horizontalGapPx = 4,
|
||||
horizontalGapPx = 8,
|
||||
placement = ContentRankingPlacement(ContentRankingCardVariant.MediumGrid, itemsPerRow = 2)
|
||||
)
|
||||
|
||||
assertEquals(185, size.widthPx)
|
||||
assertEquals(185, size.heightPx)
|
||||
assertEquals(183, size.widthPx)
|
||||
assertEquals(183, size.heightPx)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `small grid item subtracts two gaps`() {
|
||||
val size = ContentRankingLayoutCalculator.calculate(
|
||||
parentWidthPx = 374,
|
||||
horizontalGapPx = 4,
|
||||
horizontalGapPx = 8,
|
||||
placement = ContentRankingPlacement(ContentRankingCardVariant.SmallGrid, itemsPerRow = 3)
|
||||
)
|
||||
|
||||
assertEquals(122, size.widthPx)
|
||||
assertEquals(122, size.heightPx)
|
||||
assertEquals(119, size.widthPx)
|
||||
assertEquals(119, size.heightPx)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `horizontal item keeps figma ratio`() {
|
||||
val size = ContentRankingLayoutCalculator.calculate(
|
||||
parentWidthPx = 374,
|
||||
horizontalGapPx = 4,
|
||||
horizontalGapPx = 8,
|
||||
placement = ContentRankingPlacement(ContentRankingCardVariant.Horizontal, itemsPerRow = 1)
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user