4

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...
DevinM
  • 1,112
  • 1
  • 12
  • 29

2 Answers2

4

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)
        ...
    }
}
DevinM
  • 1,112
  • 1
  • 12
  • 29
2

I would delete the autoScroll(Long) method and do it like that:

  1. At the end of registerOnPageChangeCallback's onPageSelected() add:

handler.postDelayed(pager.setCurrentItem(position + 1), timerDelay)

This way you will start your loop of scrolling the items.

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
        if (position < count) {
            handler.postDelayed(pager.setCurrentItem(position + 1), timerDelay) // repeat
        }
    }
})

Also i don't know why do you use a remainder in

setCurrentItem((position++ % count)

It feels like it shouldn't be there.

Mwasz
  • 176
  • 6
  • I did attempt this previously. What happens in this solution is (assume 3 items in viewpager adapter) when the user swipes back to item 1 from item 3 it kicks off 2 postDelayed runnables. at the timer delay it jumps 1 and a few seconds later (delay between swipes) it jumps again. Note on modulus, Doesnt seem to make a difference. I found this in quite a few other solutions so I continued with it. Also note that OnPageChangeCallback() fires by default when view is inflated due to assigning data to the viewpager adapter in onCreate() – DevinM May 22 '20 at 17:39
  • the modulus was used in solutions that continued to rotate like a carousel. You were correct and it is not needed here. – DevinM May 22 '20 at 19:06
  • How about calling handler.removeCallbacks(runnable) at the start of the onPageSelected()? This way if an user swipes the page the handler will reset and only 1 Runnable will be posted at a time. – Mwasz May 22 '20 at 19:58
  • handler.removeCallbacks(runnable) doesnt appear to do anything. I removed all removeMessages(0) and setup as you described but it doesnt actually remove the callback. using removeMessages(0) at the beginning of onPageSelected() does work though so there is no need for onPageScrollStateChanged(). Updating solution. I believe the reason for this is because the runnable is different due to position difference on each callback. – DevinM May 27 '20 at 16:35