뷰페이저를 사용해 컨텐츠를 노출 시키고 있는 앱들을 살펴보면, 보다 효과적으로 콘텐츠를 보여주기 위해 일정 시간마다 자동으로 스크롤되는 Auto Scroll
기능을 사용하는 앱들이 상당히 많다.
그래서 이번에 진행하는 팀 프로젝트에 Auto Scroll
기능을 적용해 보기로했다.
기존의 동작과는 별개로 비동기적으로 스크롤이 실행되어야 하기때문에 코루틴을 사용하여 일정 시간마다 포지션을 증가시키도록 구현했다.
private var autoScroll: Job? = null
private fun setTopContentsAutoScroll() {
val viewPager = binding.vpTopContentImage
autoScroll = viewLifecycleOwner.lifecycleScope.launch {
while (true) {
delay(6000)
viewPager.setCurrentItemWithDuration(viewPager.currentItem + 1, 3000)
}
}
}
코루틴을 원하는 경우에 실행하고 멈추기위해 변수에 할당 해준뒤 사용했으며, 오토 스크롤의 속도를 조절하기위해 확장함수를 사용해 스크롤 속도를 조정했다.
fun ViewPager2.setCurrentItemWithDuration(
item: Int,
duration: Long,
interpolator: TimeInterpolator = AccelerateDecelerateInterpolator(),
pagePxWidth: Int = width // Default value taken from getWidth() from ViewPager2 view
) {
val pxToDrag: Int = pagePxWidth * (item - currentItem)
val animator = ValueAnimator.ofInt(0, pxToDrag)
var previousValue = 0
animator.addUpdateListener { valueAnimator ->
val currentValue = valueAnimator.animatedValue as Int
val currentPxToDrag = (currentValue - previousValue).toFloat()
fakeDragBy(-currentPxToDrag)
previousValue = currentValue
}
animator.addListener(object : Animator.AnimatorListener {
override fun onAnimationStart(animation: Animator) { beginFakeDrag() }
override fun onAnimationEnd(animation: Animator) { endFakeDrag() }
override fun onAnimationCancel(animation: Animator) { /* Ignored */ }
override fun onAnimationRepeat(animation: Animator) { /* Ignored */ }
})
animator.interpolator = interpolator
animator.duration = duration
animator.start()
}
스택 오버플로우부터 많은 블로그에 위와 같이 뷰페이저의 setCurrentItem
메서드의 속도를 조절해주는 코드가 올라와 사용했는데, 코드를 분석해보면 뷰페이저 컨텐츠의 width
값을 받아 아이템의 끝을 구한뒤 animator
에서 전환되는 시작과 끝을 구한다.
그 후 대충 이전에 Lottie
를 사용할때 처럼 애니메이션의 duration
을 조절한다는 건데, 상세하게 알아보기에는 시간이 없어 추후에 다시 한번 알아봐야할듯하다.
다만 위의 코드에서 중요한점은 fakeDrag
를 사용한다는 것이다.
사용자가 스크롤을 할 경우 사용자의 손이 직접 화면에 터치될경우 DRAG
상태가 되고, 손이 떼졌지만 계속해서 드래그 되는 경우에는 STATE_DRAG
상태가 된다. fakeDrag
는 STATE_DRAG
상태로 드래그 되는 것을 구현한다.
따라서 위의 코드는 setCurrentItem
이 실행될때 fakeDrag
를 통해 설정한 duration
동안 드래그를 서서히 실행하는 코드라고 할 수 있다.
하지만 위의 코드를 사용하면서 문제가 생겼는데, 기존의 코드는 스크롤이 되면 새로운 아이템이 나오기 전에 setCurrentItem
메서드를 실행해 포지션을 변경했다.
그런데 fakeDrag
를 통해 화면이 스크롤 되는 중에 setCurrentItem
메서드가 실행될 경우 에러가 발생해서 로그를 살펴보니 fakeDrag
가 실행되는 중에는 setCurrentItem
이 동시에 실행될 수 없다는 것을 알게됐다.
따라서 기존에 스크롤시 실행되던 setCurrentItem
메서드를 스크롤이 모두 끝난후 position
을 확인해 실행될 수 있도록 바꿔서 구현했다.
Auto Scroll
기능이 실행되는 동안에는 사용자가 스크롤을 하더라도 계속해서 Auto Scroll
이 실행됐다.
그래서 사용자가 직접 터치를 해서 드래그 하는 동안에는 Auto Scroll
이 실행되지 않도록 OnPageChangeCallback
의 onPageScrollStateChanged
메서드를 사용해 DRAG
상태일 경우 Auto Scroll
코루틴 Job
이 cancel
되도록 했다.
override fun onPageScrollStateChanged(state: Int) {
if (state == 0) {
setTopContentsAutoScroll()
} else {
autoScroll?.cancel()
}
super.onPageScrollStateChanged(state)
}
코루틴액션이 할당된 객체는 Job
타입으로 지정되며 해당 객체의 메서드를 통해 코루틴을 정지/실행 할 수 있다. 하지만 코루틴 내부에 delay
처럼 일정시간이 지나야 실행되는 코드가 있을경우 cancel
메서드를 사용해도 해당 동작이 끝날 때까지 코루틴이 active
상태가 유지되는데, 이 경우에는 start
메서드를 사용해도 이미 코루틴이 실행중이기 때문에 다시 실행되지 않는 경우가 발생했다.
그렇기 때문에 다시 시작되어야 하는 경우 Job
을 다시 초기화 해주는 방법으로 Auto Scroll
을 다시 실행해 주었다.
또한 화면을 전환할 경우 Fragment
에서 코루틴을 사용하고 있어, 계속해서 코루틴이 실행되었는데 불필요한 메모리 소모를 막기위해 onPause
단계에서 코루틴을 cancel
하고 onResume
단계에서 다시 실행하도록 구현했다.
잠을 좀 자야겠다...