Search code examples
androidrunnableautoscrollandroid-viewpager2postdelayed

ViewPager2 autoscroll until end of adapter itemCount


I have tried this a few different ways and I havent been able to get this viewpager to perform correctly. I am setting a viewpager2 with an adapter but part of the requirements are that the viewpager be manually swipe-able as well the base on a button to increment the pager view. I have swiping and button click moving the view pager as intended however the autoscroll is something that is problematic.

I am setting a postDelayed runnable on the viewPager. The logs just done make sense when this is running you can see below for what the output looks like.

companion object {
    private const val TAG = "LauncherActivity"
    private const val timerDelay: Long = 10 * 1000 // 1 minute in milliseconds
    var position: Int = 0
}
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    ...
    pager.autoScroll(timerDelay)
    ...
    pager.registerOnPageChangeCallback(object : OnPageChangeCallback() {
        override fun onPageSelected(position: Int) {
            Log.d(TAG, "-> OnPageChangeCallback() -> position: ${position}")
            super.onPageSelected(position)
            if (position != LauncherActivity.position) LauncherActivity.position = position
        }
    })
}

fun ViewPager2.autoScroll(interval: Long) {

    val count = adapter?.itemCount ?: 0

    val handler = Handler()
    val runnable = object: Runnable {
        override fun run() {
            if (position < count) {
                Log.d(TAG, "Autoscroll: current position = ${position} ending position = ${position + 1}")
                setCurrentItem((position++ % count), true)
                Log.d(TAG, "New position: ${position}")
                handler.postDelayed(this, interval)
            }
        }
    }
    handler.post(runnable)
}

The log to better explain whats happening

2020-05-22 11:42:51.485 D/LauncherActivity: Autoscroll: current position = 0 ending position = 1
2020-05-22 11:42:51.485 D/LauncherActivity: New position: 1
2020-05-22 11:42:51.621 D/LauncherActivity: -> OnPageChangeCallback() -> position: 0
This is initial load. The view is still showing position 0 in viewpager. Seems like the postDelayed ran but didnt run?

2020-05-22 11:43:01.492 D/LauncherActivity: Autoscroll: current position = 0 ending position = 1
2020-05-22 11:43:01.492 D/LauncherActivity: New position: 1
10 seconds later the post delayed runs again but doesnt actually change the viewpager. BUT it starts
everything off as it should. lack of OnPageChangeCallback() indicated it didnt change the view.

2020-05-22 11:43:11.497 D/LauncherActivity: Autoscroll: current position = 1 ending position = 2
2020-05-22 11:43:11.503 D/LauncherActivity: -> OnPageChangeCallback() -> position: 1
2020-05-22 11:43:11.506 D/LauncherActivity: New position: 1
The view finally changed to the next position

2020-05-22 11:43:21.518 D/LauncherActivity: Autoscroll: current position = 1 ending position = 2
2020-05-22 11:43:21.519 D/LauncherActivity: New position: 2
10 seconds later the post delayed runs again but doesnt actually change the viewpager.

2020-05-22 11:43:31.532 D/LauncherActivity: Autoscroll: current position = 2 ending position = 3
2020-05-22 11:43:31.534 D/LauncherActivity: -> OnPageChangeCallback() -> position: 2
2020-05-22 11:43:31.535 D/LauncherActivity: New position: 2
Finally it has changed to the final item.

2020-05-22 11:43:41.548 D/LauncherActivity: Autoscroll: current position = 2 ending position = 3
2020-05-22 11:43:41.550 D/LauncherActivity: New position: 3
Not a clue why it ran again...

Solution

  • With @Mwasz answer above this lead me back to workin on resolving the postDelayed pool problem when a user swipes forward or back or clicks a button to move forward. This is a complete solution as the onPageScrollStateSchanged indicated that a user physically swiped and the button onClick handles invalidating separately.

    Edit: There is no need for onPageScrollStateChanged() callback. The handler callback can be cleared at the beginning of onPageSelected() with the same result.

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val handler = Handler()
        var origPosition: Int = 0
        ...
    //    pager.autoScroll(timerDelay)
        ...
        pager.registerOnPageChangeCallback(object : OnPageChangeCallback() {
            override fun onPageSelected(position: Int) {
                super.onPageSelected(position)
    
                handler.removeMessages(0)
    
                val runnable = Runnable { pager.currentItem = ++pager.currentItem) }
                if (position < pager.adapter?.itemCount ?: 0) {
                    handler.postDelayed(runnable, timerDelay)
                }
            }
        ...
        btnContinue.setOnClickListener {
            ...
            pager.currentItem = ++pager.currentItem
            ...
        }
    }
    

    Only answer:

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val handler = Handler()
        var origPosition: Int = 0
        ...
    //    pager.autoScroll(timerDelay)
        ...
        pager.registerOnPageChangeCallback(object : OnPageChangeCallback() {
            override fun onPageSelected(position: Int) {
                super.onPageSelected(position)
    
                val runnable = Runnable { pager.setCurrentItem(position + 1) }
                if (position < pager.adapter?.itemCount ?: 0) {
                    handler.postDelayed(runnable, timerDelay)
                }
            }
    
            override fun onPageScrollStateChanged(state: Int) {
                super.onPageScrollStateChanged(state)
    
                /**
                * The user swiped forward or back and we need to
                * invalidate the previous handler.
                */
                if (state == SCROLL_STATE_DRAGGING) handler.removeMessages(0)
            }
        })
        ...
        btnContinue.setOnClickListener {
            ...
            // We dont want the last delayed runnable jumping us back up the stack.
            handler.removeMessages(0)
    
            pager.setCurrentItem(++pager.currentItem)
            ...
        }
    }