feat(banner): 배너 자동 전환 동작을 추가한다
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -52,6 +52,12 @@ class BannerAdapter(
|
||||
|
||||
fun toRealIndex(position: Int): Int = if (items.isEmpty()) 0 else position % items.size
|
||||
|
||||
fun startPosition(realIndex: Int): Int {
|
||||
if (items.size <= 1) return realIndex
|
||||
val center = VIRTUAL_ITEM_COUNT / 2
|
||||
return center - (center % items.size) + realIndex
|
||||
}
|
||||
|
||||
inner class BannerViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
private val imageView: ImageView = itemView.findViewById(R.id.iv_banner)
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.banner
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@@ -8,6 +10,7 @@ import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.LinearSmoothScroller
|
||||
import androidx.recyclerview.widget.PagerSnapHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kr.co.vividnext.sodalive.R
|
||||
@@ -28,8 +31,23 @@ class BannerView @JvmOverloads constructor(
|
||||
private val snapHelper = PagerSnapHelper()
|
||||
private var items: List<BannerItem> = emptyList()
|
||||
private var currentIndex: Int = 0
|
||||
private var currentAdapterPosition: Int = 0
|
||||
private var itemSpacingPx: Int = 0
|
||||
private var spacingDecoration: RecyclerView.ItemDecoration? = null
|
||||
private val autoScrollHandler = Handler(Looper.getMainLooper())
|
||||
private val autoScrollRunnable = Runnable { moveToNextBanner() }
|
||||
private var hasWindowAttachmentForAutoScroll = false
|
||||
private val bannerScrollListener = object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||
when (newState) {
|
||||
RecyclerView.SCROLL_STATE_DRAGGING -> stopAutoScroll()
|
||||
RecyclerView.SCROLL_STATE_IDLE -> {
|
||||
updateCurrentIndexFromSnap()
|
||||
startAutoScrollIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
clipToPadding = false
|
||||
@@ -44,6 +62,18 @@ class BannerView @JvmOverloads constructor(
|
||||
updateCounter()
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
hasWindowAttachmentForAutoScroll = true
|
||||
startAutoScrollIfNeeded()
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
stopAutoScroll()
|
||||
hasWindowAttachmentForAutoScroll = false
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
if (w > 0) applyLayoutSize(w)
|
||||
@@ -53,9 +83,12 @@ class BannerView @JvmOverloads constructor(
|
||||
this.items = items
|
||||
currentIndex = BannerState.from(items.size, currentIndex).currentIndex
|
||||
visibility = if (items.isEmpty()) GONE else VISIBLE
|
||||
stopAutoScroll()
|
||||
adapter.submitItems(items)
|
||||
scrollToCurrentBanner()
|
||||
if (width > 0) applyLayoutSize(width)
|
||||
updateCounter()
|
||||
startAutoScrollIfNeeded()
|
||||
}
|
||||
|
||||
fun setOnBannerClickListener(listener: ((BannerItem) -> Unit)?) {
|
||||
@@ -73,11 +106,7 @@ class BannerView @JvmOverloads constructor(
|
||||
clipToPadding = false
|
||||
clipChildren = false
|
||||
if (onFlingListener == null) snapHelper.attachToRecyclerView(this)
|
||||
addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||
if (newState == RecyclerView.SCROLL_STATE_IDLE) updateCurrentIndexFromSnap()
|
||||
}
|
||||
})
|
||||
addOnScrollListener(bannerScrollListener)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,10 +124,44 @@ class BannerView @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun scrollToCurrentBanner() {
|
||||
val recyclerView = requireNotNull(recyclerView)
|
||||
currentAdapterPosition = if (items.size > 1) {
|
||||
adapter.startPosition(currentIndex)
|
||||
} else {
|
||||
currentIndex
|
||||
}
|
||||
recyclerView.scrollToPosition(currentAdapterPosition)
|
||||
}
|
||||
|
||||
private fun moveToNextBanner() {
|
||||
if (BannerState.from(items.size, currentIndex).displayMode != BannerDisplayMode.Carousel) return
|
||||
val recyclerView = requireNotNull(recyclerView)
|
||||
val layoutManager = recyclerView.layoutManager as? LinearLayoutManager ?: return
|
||||
val nextPosition = currentAdapterPosition + 1
|
||||
currentAdapterPosition = nextPosition
|
||||
currentIndex = adapter.toRealIndex(nextPosition)
|
||||
layoutManager.startSmoothScroll(BannerSmoothScroller(context).apply { targetPosition = nextPosition })
|
||||
updateCounter()
|
||||
startAutoScrollIfNeeded()
|
||||
}
|
||||
|
||||
private fun startAutoScrollIfNeeded() {
|
||||
stopAutoScroll()
|
||||
if (!hasWindowAttachmentForAutoScroll) return
|
||||
if (BannerState.from(items.size, currentIndex).displayMode != BannerDisplayMode.Carousel) return
|
||||
autoScrollHandler.postDelayed(autoScrollRunnable, AUTO_SCROLL_INTERVAL_MS)
|
||||
}
|
||||
|
||||
private fun stopAutoScroll() {
|
||||
autoScrollHandler.removeCallbacks(autoScrollRunnable)
|
||||
}
|
||||
|
||||
private fun updateCurrentIndexFromSnap() {
|
||||
val layoutManager = requireNotNull(recyclerView).layoutManager ?: return
|
||||
val snapView = snapHelper.findSnapView(layoutManager) ?: return
|
||||
val position = layoutManager.getPosition(snapView)
|
||||
currentAdapterPosition = position
|
||||
currentIndex = adapter.toRealIndex(position)
|
||||
updateCounter()
|
||||
}
|
||||
@@ -118,6 +181,17 @@ class BannerView @JvmOverloads constructor(
|
||||
|
||||
private fun Int.dpToPx(): Int = (this * resources.displayMetrics.density).roundToInt()
|
||||
|
||||
companion object {
|
||||
private const val AUTO_SCROLL_INTERVAL_MS = 5_000L
|
||||
private const val SCROLL_ANIMATION_DURATION_MS = 350
|
||||
|
||||
fun scrollAnimationDurationMsForTest(): Int = SCROLL_ANIMATION_DURATION_MS
|
||||
}
|
||||
|
||||
private class BannerSmoothScroller(context: Context) : LinearSmoothScroller(context) {
|
||||
override fun calculateTimeForScrolling(dx: Int): Int = SCROLL_ANIMATION_DURATION_MS
|
||||
}
|
||||
|
||||
private class BannerSpacingDecoration(
|
||||
private val spacingPx: Int
|
||||
) : RecyclerView.ItemDecoration() {
|
||||
|
||||
Reference in New Issue
Block a user