버튼 수집상

[안드로이드] 스크롤 상단에 margin 적용한 sticky view 만들기 본문

TIL - 안드로이드

[안드로이드] 스크롤 상단에 margin 적용한 sticky view 만들기

cocokaribou 2023. 5. 2. 17:58

배경

이전에 sticky view 를 구현할 땐

LinearLayout.firstVisibleItemPosition 을 가지고 sticky를 띄울지 말지 판단했었다.

// 예시
recyclerView.addOnScrollListener(object: RecyclerView.OnScrollListener() {
    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
        super.onScrolled(recyclerView, dx, dy)
        val lm = recyclerView.layoutManager as LinearLayoutManager
        val firstVisiblePosition = lm.findFirstVisibleItemPosition()

        stickyView.root.visibility = if (firstVisiblePosition >= stickyIndex) View.VISIBLE else View.GONE
    }
})

그런데 이번엔 sticky 위에 헤더가 있었다.

firstVisibleItemPosition은 index 값이기 때문에
헤더 높이만큼 margin을 주기 위해선 스크롤 offset 값을 알아야했다.

 

과정1

RecyclerView.computeVerticalScrollOffset() 함수는

화면에서 첫번째로 보이는 뷰홀더의 top에서부터

리사이클러뷰 최상단 사이의 offset 값을 리턴한다.

그러나 스크롤을 내리면서 뷰홀더들이 onBind되거나 disappeared 될 때마다 영향을 받는다.

 

스크롤을 내릴 때 offset 누적값을 구하는 레이아웃 매니저를 만들었다.

class CustomLayoutManager @JvmOverloads constructor(
    context: Context,
    orientation: Int,
    reverseLayout: Boolean
) : LinearLayoutManager(context, orientation, reverseLayout) {

    private var currentScrollOffset = 0

    override fun scrollVerticallyBy(dy: Int, recycler: RecyclerView.Recycler?, state: RecyclerView.State?): Int {
        val scrolled = super.scrollVerticallyBy(dy, recycler, state)
        currentScrollOffset += scrolled
        return scrolled
    }
    fun computeVerticalScrollOffset() = currentScrollOffset
}

(혹시 다른 함수를 아신다면 댓글로 공유 부탁드립니다)

 

만든 레이아웃 매니저를 리사이클러뷰에 등록한다.

//...
recyclerView.adapter = mAdapter
recyclerView.layoutManager = CustomLayoutManager(this@MainActivity, VERTICAL, false)

 

과정2

이제 sticky view에 도달할 때까지 보이는 뷰홀더들의 높이의 총합을 구한다.

목표는 scrollListener 안에서 for문을 돌리지 않는 것.

// sticky view 도달전까지 뷰홀더 높이값들을 담을 Int 리스트
val heightList : MutableList<Int> = List(stickyIndex){ 0 }.toMutableList()

val stickyScrollListener = {
    val lm = list.layoutManager as CustomLayoutManager
    val currentIndex = lm.findFirstVisibleItemPosition()
    val currentViewHolderHeight = lm.findViewByPosition(currentIndex)?.height ?: 0

    // 뷰홀더가 처음 그려졌을 때 한 번만 리스트에 높이값 저장
    if (currentIndex < stickyIndex && heightList[currentIndex] == 0) {
        heightList[currentIndex] = currentViewHolderHeight
    }

    // sticky가 뜨는 지점 = sticky 도달전까지 뷰홀더 높이 총합 - 헤더 높이
    val totalHeight = heightList.sum() - header.height

    if (heightList.all { it != 0 } && lm.computeVerticalScrollOffset() >= totalHeight) {
        sticky.root.visibility = View.VISIBLE
    } else {
        sticky.root.visibility = View.GONE
    }
}

//...

recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
        super.onScrolled(recyclerView, dx, dy)

        if (shouldShowSticky) stickyScrollListener()
    }
})

 

 

추가

sticky view를 붙일 때 계산된 마진값을 줄 게 아니라

header 뷰 하단에 앵커링되게 뷰를 붙일 수 있지 않을까..

728x90