[TIL] View Pager Auto Scroll (feat. Coroutine)

박봉팔·2024년 2월 8일
0

Auto Scroll

뷰페이저를 사용해 컨텐츠를 노출 시키고 있는 앱들을 살펴보면, 보다 효과적으로 콘텐츠를 보여주기 위해 일정 시간마다 자동으로 스크롤되는 Auto Scroll기능을 사용하는 앱들이 상당히 많다.

그래서 이번에 진행하는 팀 프로젝트에 Auto Scroll기능을 적용해 보기로했다.


Coroutine

기존의 동작과는 별개로 비동기적으로 스크롤이 실행되어야 하기때문에 코루틴을 사용하여 일정 시간마다 포지션을 증가시키도록 구현했다.

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상태가 된다. fakeDragSTATE_DRAG상태로 드래그 되는 것을 구현한다.

따라서 위의 코드는 setCurrentItem이 실행될때 fakeDrag를 통해 설정한 duration동안 드래그를 서서히 실행하는 코드라고 할 수 있다.


fakeDrag 주의사항

하지만 위의 코드를 사용하면서 문제가 생겼는데, 기존의 코드는 스크롤이 되면 새로운 아이템이 나오기 전에 setCurrentItem메서드를 실행해 포지션을 변경했다.

그런데 fakeDrag를 통해 화면이 스크롤 되는 중에 setCurrentItem메서드가 실행될 경우 에러가 발생해서 로그를 살펴보니 fakeDrag가 실행되는 중에는 setCurrentItem이 동시에 실행될 수 없다는 것을 알게됐다.

따라서 기존에 스크롤시 실행되던 setCurrentItem메서드를 스크롤이 모두 끝난후 position을 확인해 실행될 수 있도록 바꿔서 구현했다.


터치시 정지기능 구현하기

Auto Scroll기능이 실행되는 동안에는 사용자가 스크롤을 하더라도 계속해서 Auto Scroll이 실행됐다.

그래서 사용자가 직접 터치를 해서 드래그 하는 동안에는 Auto Scroll이 실행되지 않도록 OnPageChangeCallbackonPageScrollStateChanged메서드를 사용해 DRAG상태일 경우 Auto Scroll 코루틴 Jobcancel되도록 했다.

override fun onPageScrollStateChanged(state: Int) {
	if (state == 0) {
    	setTopContentsAutoScroll()
	} else {
    	autoScroll?.cancel()
	}
	super.onPageScrollStateChanged(state)
}

※ Job.start() & Job.cancel()

코루틴액션이 할당된 객체는 Job타입으로 지정되며 해당 객체의 메서드를 통해 코루틴을 정지/실행 할 수 있다. 하지만 코루틴 내부에 delay처럼 일정시간이 지나야 실행되는 코드가 있을경우 cancel메서드를 사용해도 해당 동작이 끝날 때까지 코루틴이 active상태가 유지되는데, 이 경우에는 start메서드를 사용해도 이미 코루틴이 실행중이기 때문에 다시 실행되지 않는 경우가 발생했다.

그렇기 때문에 다시 시작되어야 하는 경우 Job을 다시 초기화 해주는 방법으로 Auto Scroll을 다시 실행해 주었다.

또한 화면을 전환할 경우 Fragment에서 코루틴을 사용하고 있어, 계속해서 코루틴이 실행되었는데 불필요한 메모리 소모를 막기위해 onPause단계에서 코루틴을 cancel하고 onResume단계에서 다시 실행하도록 구현했다.


오늘은 어땠나요?

잠을 좀 자야겠다...

profile
개발 첫걸음! 가보자구!

0개의 댓글