이전에는 View Pager
에 FragmentStateAdapter
를 사용해 단순하게 Fragment
만 교체하는 방식을 사용했었다.
하지만 Fragment
전체를 교체하는게 아니라 일부분에 아이템들을 스크롤하는 섹션을 만들고 싶었기에, RecyclerView
의 ListAdapter
를 사용해 구현했다.
(RecyclerView
를 사용해도 되지만 ViewPager
특유의 찰지게 스크롤 되는 느낌을 살리고싶어서 ViewPager
를 사용했다.)
ViewPager
는 기본적으로 RecyclerView
를 기반으로 하기때문에 전체 Item
의 갯수가 정해져있다.
따라서 현재 노출되고 있는 아이템이 기본적으로 ItemList
의 마지막 아이템일 경우 더이상의 스크롤이 불가능하다.
(마찬가지로 제일 처음일 경우에도 이전으로 스크롤이 불가능하다.)
하지만 많은 앱들이 View Pager
형식으로 끝없이 스크롤 되는 기능을 사용하고 있다. 그래서 기왕 ViewPager
를 사용하는 김에 무한 스크롤에 도전해 보기로했다.
View Pager
의 스크롤에 관해 찾아보다보니 크게 2가지의 방법이 있었다
무한(Infinity)으로 스크롤 되는 방식과 (사실 무한에 가까워보이는) 계속해서 순환하는(Circular) 형식 으로 구현되는 방식이었다.
구글에 검색해보면 보통 무한정 스크롤되는 것을 Infinity
스크롤이라고 지칭한다.
가장 많이 나오는 방법중 하나로 대부분의 Infinity
방식은 View Pager
에 연결된 어댑터의
Item Count
를 큰 값으로 만들어 아이템의 position
이 무한정 증가되는 것처럼 만드는 방법이다.
(많은 글들이 Item count
를 Int.MAX_VALUE
를 사용해 Int
타입의 최댓값을 사용하고 있었다.)
override fun getItemCount(): Int = Int.MAX_VALUE
override fun onBindViewHolder(holder: TopContentViewHolder, position: Int) {
val item = getItem(position % currentList.size)
...
}
getItemCount
에 어탭터에 전달된 리스트의 크기보다 큰 값을 지정해 어댑터의 position
값이 계속해서 증가 하도록 만들고, 사용되는 아이템의 포지션을 어댑터에 크기로 나눈나머지로 지정해 position
이 증가되도 계속해서 아이템들이 반복되어 나오도록 구현한다.
이 방법의 경우 View Pager
에 노출되는 아이템의 포지션이 처음 혹은 마지막일 경우 스크롤을 할 수 없다는 단점이 있다.
(물론 최초에 current Item
의 값을 중간으로 지정해주면 스크롤되는 것처럼 구현할 수 있다.)
Infinity
스크롤에 대해 검색하다가 마지막 포지션의 아이템에서 스크롤 할 경우 첫 포지션으로 이동하게만드는 방법을 찾을 수 있었고, 이를 많은 글들에서 순환스크롤 Circular
스크롤이라고 지칭하는것을 알게됐다.
Infinity
방법과의 가장 큰 차이점은 Infinity
방법이 View Pager
에 연결된 어댑터 내부에서 아이템 크기를 조정해 무한정 스크롤 되는 것처럼 구현했다면, Circular
방법은 View Pager
에 OnPageChangeCallback
을 연결해 스크롤과 포지션을 받아서 가장 마지막 포지션에서 스크롤 할 경우 실제로 제일 처음 포지션의 아이템으로 이동시켜 무한정 스크롤 할 수 있도록 구현했다는 점이다.
with(binding.viewPager) {
adapter = topContentsAdapter
registerOnPageChangeCallback(getPageChangeCallback())
}
private fun getPageChangeCallback(): OnPageChangeCallback {
return object : OnPageChangeCallback() {
var currentItems = listOf<VideoItem>()
var currentState = -1
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
super.onPageScrolled(position, positionOffset, positionOffsetPixels)
if (position == currentItems.size - 1 && positionOffset <= 0 && currentState == 1) {
binding.viewPager.setCurrentItem(0, false)
return
}
if (position == 0 && positionOffset <= 0 && currentState == 1) {
binding.viewPager.setCurrentItem(currentItems.size - 1, false)
return
}
}
override fun onPageScrollStateChanged(state: Int) {
super.onPageScrollStateChanged(state)
currentState = state
}
}
}
OnPageChangeCallback
메서드를 Override
해게되면 onPageScrolled
메서드를 사용할 수 있게 되는데, 전달받는 매개변수를 각각 살펴보면 다음과 같다.
- positionOffsetPixels : 스크롤 했을때 아이템이 이동하는 픽셀수를 나타낸다.
- positionOffset : 스크롤시 아이템의 위치를 나타낸다.
- 최초 아이템의 Offset은
0
이며 다음 아이템으로 스크롤시1
까지 소수점 단위로 값이 증가한다.
반대로 이전 아이템으로 스크롤 할 경우1
에서부터 소수점 단위로 값이 줄어든다.
- position : positionOffset이 0과 1사이인 아이템의 포지션을 나타낸다.
- 예를들어 다음 아이템으로 스크롤시 positionOffset이
1
이 되어 완전히 아이템이 전환되었을 경우 다음 아이템의 포지션이 된다.- 반대로 이전 아이템으로 스크롤 할 경우에는 이전아이템의 positionOffset이
1
부터 작아지기때문에 즉시 이전 아이템의 포지션이 된다.
또한 onPageScrollStateChanged
메서드를 사용하면 현재 스크롤을 하고있는지 알 수 있는데 (스크롤시 state
가 1이 된다.) 이를통해 처음/마지막 아이템에서 스크롤을 하면 원하는 코드가 작동되도록 구현이 가능하다.
따라서 onPageScrolled
메서드를 사용해 포지션 값이 처음/마지막 일때 이전/다음으로 스크롤 할 시
(currentState
로 드래그 중인가 확인, positionOffset
으로 다음/이전 으로 스크롤 중인지 확인 - positionOffset
이 0
보다 크면 다음 아이템으로 이동중)
setCurrentPositon
메서드로 마지막/처음으로 이동시켜 순환하는것을 구현하는게 가능하다.
하지만 이 방식의 경우 처음이나 마지막에서 스크롤을 시도할 경우 다른때와 다르게 아이템이 자연스럽게 스크롤 되는게 아니라, 갑자기 바뀌다보니 어색한 느낌을 준다.
Infinity
방식의 자연스러운 아이템 전환과 Circular
방식의 순환방식을 함께 사용하기 위해 두 로직을 섞어서 구성해보았다.
override fun getItemCount(): Int = currentList.size * 3
override fun onBindViewHolder(holder: TopContentViewHolder, position: Int) {
val item = getItem(position % currentList.size)
...
}
우선 어댑터에서 아이템 사이즈를 현재 리스트 * 3
으로 지정해 아이템을 3덩이로 나눠준다.
이렇게 되면 가운데에 있는 아이템은 처음/마지막에서 이동할 경우 각각 리스트의 마지막/처음 아이템으로 이동하는것과 같이 보이게 된다.
이제 ScrollCallback
을 사용해 가운데에 있는 아이템 덩이의 처음/마지막 값이 이동해 이전/다음 아이템으로 이동하게 되면 setCurrentPosition
으로 포지션을 이동하게 해주면 된다.
private fun getPageChangeCallback(): OnPageChangeCallback {
return object : OnPageChangeCallback() {
var currentItems = listOf<VideoItem>()
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
super.onPageScrolled(position, positionOffset, positionOffsetPixels)
// item size -1인 포지션은 첫번째 덩이의 마지막 아이템이다.
// 따라서 첫번째 덩이의 마지막 아이템으로 포지션이 옮겨지면 2번째 덩이의 마지막으로 이동한다.
if (position == currentItems.size - 1 && positionOffset <= 0.1f) {
binding.vpTopContentImage.setCurrentItem((currentItems.size * 2) - 1, false)
return
}
// item size * 2인 포지션은 세번째 덩이의 첫번째 아이템이다.
// 따라서 3번째 덩이의 첫번째 아이템으로 포지션이 옮겨가면 2번째 덩이의 첫번째로 이동한다.
if (position == currentItems.size * 2 && positionOffset <= 0.1f) {
binding.vpTopContentImage.setCurrentItem(currentItems.size, false)
return
}
}
}
}
positionOffset
이 0f
일경우 스크롤이 민감하기때문에 아직 진행되고 있는 상황에 한번더 이전으로 스크롤을 입력하면 원하는 대로 포지션이 바뀌지 않는 경우가 생겼다.
그래서 여러가지로 시도해본결과 0.1f
정도면 적당히 이전으로 스크롤 되지 않았다.
안녕하세요~^^