fix(widget): 배너 가상 목록 갱신을 안정화한다

This commit is contained in:
2026-06-05 20:53:28 +09:00
parent 7f307346f3
commit 51226cf5cc
4 changed files with 107 additions and 17 deletions

View File

@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.widget.banner
import android.annotation.SuppressLint
import android.graphics.Outline
import android.view.LayoutInflater
import android.view.View
@@ -38,19 +39,17 @@ class BannerAdapter(
this.items.addAll(items)
val newItemCount = itemCount
when {
previousItemCount == 0 && newItemCount > 0 -> notifyItemRangeInserted(0, newItemCount)
previousItemCount > 0 && newItemCount == 0 -> notifyItemRangeRemoved(0, previousItemCount)
previousItemCount == newItemCount -> notifyItemRangeChanged(0, newItemCount)
else -> {
notifyItemRangeRemoved(0, previousItemCount)
notifyItemRangeInserted(0, newItemCount)
}
previousItemCount == 0 && newItemCount == 1 -> notifyItemInserted(0)
previousItemCount == 1 && newItemCount == 0 -> notifyItemRemoved(0)
previousItemCount == 1 && newItemCount == 1 -> notifyItemChanged(0)
else -> notifyVirtualItemsChanged()
}
}
fun setItemSizePx(itemSizePx: Int) {
val shouldNotify = this.itemSizePx != itemSizePx
this.itemSizePx = itemSizePx
notifyItemsChanged()
if (shouldNotify) notifyVisibleItemsChanged()
}
fun setOnClickItem(listener: ((BannerItem) -> Unit)?) {
@@ -62,8 +61,9 @@ class BannerAdapter(
}
fun setPreviewImageResource(resId: Int) {
val shouldNotify = previewImageResId != resId
previewImageResId = resId
notifyItemsChanged()
if (shouldNotify) notifyVisibleItemsChanged()
}
fun toRealIndex(position: Int): Int = if (items.isEmpty()) 0 else position % items.size
@@ -106,9 +106,17 @@ class BannerAdapter(
}
}
private fun notifyItemsChanged() {
val count = itemCount
if (count > 0) notifyItemRangeChanged(0, count)
private fun notifyVisibleItemsChanged() {
when (itemCount) {
0 -> Unit
1 -> notifyItemChanged(0)
else -> notifyVirtualItemsChanged()
}
}
@SuppressLint("NotifyDataSetChanged")
private fun notifyVirtualItemsChanged() {
notifyDataSetChanged()
}
private companion object {

View File

@@ -1,6 +1,11 @@
package kr.co.vividnext.sodalive.v2.widget.banner
import kr.co.vividnext.sodalive.settings.event.EventItem
data class BannerItem(
val bannerId: String?,
val imageUrl: String
val imageUrl: String,
val eventItem: EventItem?,
val creatorId: Long?,
val seriesId: Long?,
val link: String?
)

View File

@@ -146,7 +146,13 @@ class BannerView @JvmOverloads constructor(
if (previewImageResId != 0) adapter.setPreviewImageResource(previewImageResId)
if (previewItemCount > 0) {
items = List(previewItemCount) { index ->
BannerItem(bannerId = "preview-$index", imageUrl = "")
BannerItem(
imageUrl = "",
eventItem = null,
creatorId = null,
seriesId = null,
link = "preview-$index"
)
}
currentIndex = BannerState.from(previewItemCount, previewCurrentIndex).currentIndex
visibility = VISIBLE

View File

@@ -67,6 +67,74 @@ class BannerViewTest {
assertEquals(items[1], clickedItem)
}
@Test
fun `adapter는 carousel 설정 시 max range notify를 호출하지 않는다`() {
val adapter = BannerAdapter()
var insertedItemCount = 0
var changedItemCount = 0
var dataSetChangedCount = 0
adapter.registerAdapterDataObserver(
object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
insertedItemCount = itemCount
}
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
changedItemCount = itemCount
}
override fun onChanged() {
dataSetChangedCount += 1
}
}
)
adapter.submitItems(listOf(sampleItem("1"), sampleItem("2")))
adapter.setItemSizePx(100)
assertEquals(0, insertedItemCount)
assertEquals(0, changedItemCount)
assertEquals(2, dataSetChangedCount)
}
@Test
fun `adapter는 단일 배너 설정 시 specific notify를 호출한다`() {
val adapter = BannerAdapter()
var insertedItemCount = 0
var changedItemCount = 0
var removedItemCount = 0
var dataSetChangedCount = 0
adapter.registerAdapterDataObserver(
object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
insertedItemCount += itemCount
}
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
changedItemCount += itemCount
}
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
removedItemCount += itemCount
}
override fun onChanged() {
dataSetChangedCount += 1
}
}
)
adapter.submitItems(listOf(sampleItem("1")))
adapter.setItemSizePx(100)
adapter.submitItems(listOf(sampleItem("2")))
adapter.submitItems(emptyList())
assertEquals(1, insertedItemCount)
assertEquals(2, changedItemCount)
assertEquals(1, removedItemCount)
assertEquals(0, dataSetChangedCount)
}
@Test
fun `배너 view는 빈 목록이면 숨기고 단일 목록이면 counter를 숨긴다`() {
val view = inflateBannerView()
@@ -533,8 +601,11 @@ class BannerViewTest {
}
private fun sampleItem(id: String) = BannerItem(
bannerId = id,
imageUrl = "https://example.com/banner-$id.png"
imageUrl = "https://example.com/banner-$id.png",
eventItem = null,
creatorId = id.toLongOrNull(),
seriesId = null,
link = "https://example.com/banner-$id"
)
private fun exactly(size: Int): Int = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY)