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

View File

@@ -1,6 +1,11 @@
package kr.co.vividnext.sodalive.v2.widget.banner package kr.co.vividnext.sodalive.v2.widget.banner
import kr.co.vividnext.sodalive.settings.event.EventItem
data class BannerItem( 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 (previewImageResId != 0) adapter.setPreviewImageResource(previewImageResId)
if (previewItemCount > 0) { if (previewItemCount > 0) {
items = List(previewItemCount) { index -> 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 currentIndex = BannerState.from(previewItemCount, previewCurrentIndex).currentIndex
visibility = VISIBLE visibility = VISIBLE

View File

@@ -67,6 +67,74 @@ class BannerViewTest {
assertEquals(items[1], clickedItem) 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 @Test
fun `배너 view는 빈 목록이면 숨기고 단일 목록이면 counter를 숨긴다`() { fun `배너 view는 빈 목록이면 숨기고 단일 목록이면 counter를 숨긴다`() {
val view = inflateBannerView() val view = inflateBannerView()
@@ -533,8 +601,11 @@ class BannerViewTest {
} }
private fun sampleItem(id: String) = BannerItem( 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) private fun exactly(size: Int): Int = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY)