diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerView.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerView.kt index d1aa4de4..6920f826 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerView.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerView.kt @@ -38,6 +38,7 @@ class BannerView @JvmOverloads constructor( private var currentAdapterPosition: Int = 0 private var itemSpacingPx: Int = 0 private var spacingDecoration: RecyclerView.ItemDecoration? = null + private var shouldAlignCurrentPositionAfterLayout = false private val autoScrollHandler = Handler(Looper.getMainLooper()) private val autoScrollRunnable = Runnable { moveToNextBanner() } private var hasWindowAttachmentForAutoScroll = false @@ -116,6 +117,7 @@ class BannerView @JvmOverloads constructor( fun setItems(items: List) { this.items = items currentIndex = 0 + shouldAlignCurrentPositionAfterLayout = true visibility = if (items.isEmpty()) GONE else VISIBLE stopAutoScroll() adapter.submitItems(items) @@ -179,6 +181,15 @@ class BannerView @JvmOverloads constructor( spacingDecoration?.let(::removeItemDecoration) spacingDecoration = BannerSpacingDecoration(itemSpacingPx).also(::addItemDecoration) } + counterContainer?.let { counter -> + val params = counter.layoutParams as? LayoutParams ?: return@let + params.marginEnd = (size.sideInsetDp + COUNTER_ITEM_MARGIN_DP).dpToPx() + counter.layoutParams = params + } + if (shouldAlignCurrentPositionAfterLayout) { + scrollToCurrentBanner() + shouldAlignCurrentPositionAfterLayout = false + } } private fun scrollToCurrentBanner() { @@ -188,7 +199,15 @@ class BannerView @JvmOverloads constructor( } else { currentIndex } - recyclerView.scrollToPosition(currentAdapterPosition) + val layoutManager = recyclerView.layoutManager as? LinearLayoutManager + if (items.size > 1 && layoutManager != null) { + layoutManager.scrollToPositionWithOffset( + currentAdapterPosition, + -(itemSpacingPx / 2) + ) + } else { + recyclerView.scrollToPosition(currentAdapterPosition) + } } private fun moveToNextBanner() { @@ -263,6 +282,7 @@ class BannerView @JvmOverloads constructor( companion object { private const val AUTO_SCROLL_INTERVAL_MS = 5_000L private const val SCROLL_ANIMATION_DURATION_MS = 350 + private const val COUNTER_ITEM_MARGIN_DP = 14 fun scrollAnimationDurationMsForTest(): Int = SCROLL_ANIMATION_DURATION_MS } @@ -280,7 +300,9 @@ class BannerView @JvmOverloads constructor( parent: RecyclerView, state: RecyclerView.State ) { - outRect.right = spacingPx + val leftSpacing = spacingPx / 2 + outRect.left = leftSpacing + outRect.right = spacingPx - leftSpacing } } } diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerViewTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerViewTest.kt index 11bf4171..e5b176c7 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerViewTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerViewTest.kt @@ -173,11 +173,63 @@ class BannerViewTest { assertEquals(20.dpToPx(), recyclerView.paddingLeft) assertEquals(20.dpToPx(), recyclerView.paddingRight) - assertEquals(8.dpToPx(), itemOffset.right) + assertEquals(4.dpToPx(), itemOffset.left) + assertEquals(4.dpToPx(), itemOffset.right) + assertEquals(8.dpToPx(), itemOffset.left + itemOffset.right) assertEquals(362.dpToPx(), holder.itemView.layoutParams.width) assertEquals(362.dpToPx(), holder.itemView.layoutParams.height) } + @Test + fun `배너 view는 carousel item 간격을 좌우 대칭으로 적용한다`() { + val view = inflateBannerView() + val recyclerView = view.findViewById(R.id.rv_banner) + + view.setItems(listOf(sampleItem("1"), sampleItem("2"))) + view.measure(exactly(402.dpToPx()), exactly(402.dpToPx())) + view.layout(0, 0, 402.dpToPx(), 402.dpToPx()) + val holder = recyclerView.adapter!!.onCreateViewHolder(recyclerView, 0) + recyclerView.adapter!!.onBindViewHolder(holder, 0) + val itemOffset = Rect() + recyclerView.getItemDecorationAt(0).getItemOffsets(itemOffset, holder.itemView, recyclerView, RecyclerView.State()) + + assertEquals(4.dpToPx(), itemOffset.left) + assertEquals(4.dpToPx(), itemOffset.right) + assertEquals(8.dpToPx(), itemOffset.left + itemOffset.right) + } + + @Test + fun `배너 view counter는 현재 item 내부 우상단 기준 margin을 적용한다`() { + val view = inflateBannerView() + + view.setItems(listOf(sampleItem("1"), sampleItem("2"))) + view.measure(exactly(402.dpToPx()), exactly(402.dpToPx())) + view.layout(0, 0, 402.dpToPx(), 402.dpToPx()) + + val counter = view.findViewById(R.id.layout_banner_counter) + val params = counter.layoutParams as FrameLayout.LayoutParams + assertEquals(14.dpToPx(), params.topMargin) + assertEquals(34.dpToPx(), params.marginEnd) + } + + @Test + fun `배너 view는 최초 layout 직후 현재 item 중심을 view 중심에 맞춘다`() { + val view = inflateBannerView() + val recyclerView = view.findViewById(R.id.rv_banner) + + view.setItems(listOf(sampleItem("1"), sampleItem("2"), sampleItem("3"))) + view.measure(exactly(402.dpToPx()), exactly(402.dpToPx())) + view.layout(0, 0, 402.dpToPx(), 402.dpToPx()) + + val layoutManager = requireNotNull(recyclerView.layoutManager) + val currentItem = requireNotNull(layoutManager.findViewByPosition(view.currentAdapterPositionForTest())) + val itemCenter = (currentItem.left + currentItem.right) / 2 + val viewCenter = view.width / 2 + assertEquals(viewCenter, itemCenter) + assertEquals(20.dpToPx(), currentItem.left) + assertEquals(20.dpToPx(), view.width - currentItem.right) + } + @Test fun `배너 view는 wrap content 높이면 402dp 폭 기준 362dp 높이로 측정된다`() { val view = inflateBannerView() @@ -221,6 +273,7 @@ class BannerViewTest { val itemOffset = Rect() recyclerView.getItemDecorationAt(0).getItemOffsets(itemOffset, holder.itemView, recyclerView, RecyclerView.State()) + assertEquals(0, itemOffset.left) assertEquals(0, itemOffset.right) }