42

I'm creating a slideshow with ViewPager2. For example, the slideshow has 3 items and I want to show the second item when the activity opens. I use setCurrentItem(int item, boolean smoothScroll) method but it doesn't work and nothing happens. How can I achieve it?

viewPager.adapter = adapter
viewPager.setCurrentItem(1, true)
hosseinAmini
  • 2,184
  • 2
  • 20
  • 46
  • second item is 1. count it from 0. – shb May 26 '19 at 08:30
  • @shb thank you for replying but I wrote the sample code wrong. In my code, this is 1 and it doesn't work – hosseinAmini May 26 '19 at 08:33
  • You might wanna share some more code then. The code you shared works perfectly. Share your viewPager adapter class and where also from where you're calling it. – shb May 26 '19 at 08:34

13 Answers13

60

I think an easier more reliable fix is to defer to next run cycle instead of unsecure delay e.g

viewPager.post {
  viewPager.setCurrentItem(1, true)
}
Rune
  • 631
  • 1
  • 5
  • 3
  • I independently reached this solution after wasting days looking for a bug in **my** code. Tip: If all else fails, try making the field that holds the initial item position `final`. [ Don't ask me why ! ] – Bad Loser Nov 30 '20 at 21:06
  • 6
    You can also use viewPager.doOnAttach { ... } instead of viewPager.post{ ... }, this may make it a bit clearer what's going on, because the problem seems not to be directly related to the run cycle, but to the viewPager not being attached to the window. See also Davide's explanation. – josias Dec 17 '20 at 10:16
  • saved my ass. 3 hours delay in rollout due to this. Thanks a lot.. – uddi baba Sep 20 '22 at 13:43
  • @josias your solution isn't working for me, but the answer does ;) – Sir NIkolay Cesar The First Aug 16 '23 at 11:10
23

setCurrentItem(int item, boolean smoothScroll) works correctly in ViewPager but in ViewPager2 it does not work as expected. Finally, I faced this problem by adding setCurrentItem(int item, boolean smoothScroll) method into a delay like this:

Handler().postDelayed({
     view.viewPager.setCurrentItem(startPosition, false)
}, 100)
Davide Cannizzo
  • 2,826
  • 1
  • 29
  • 31
hosseinAmini
  • 2,184
  • 2
  • 20
  • 46
  • 7
    This actually works to me too. But to me this is not a solution, it is a hack (who knows how long shall be the delay to let it work reliably). Hack working around the buggy Android component... Hopefully Google will fix it soon, so that we do not need to hack it any more... – Jiří Křivánek Apr 22 '20 at 18:49
  • 1
    I actually spent quite a bit of time trying to figure out why my tab is not being changed when I call this. Awful that we need workarounds for basic functionality – Vucko Apr 26 '20 at 10:37
  • 4
    Although I've already explained what's wrong with this answer in my post, I'll mention the reason here so to justify my downvote. That's really as plain and simple as how could you come out with delaying things arbitrarily??? That's detrimental for the end–user, who'll have to wait 1/10 a second when its phone may be faster, or may have a slow phone that'll take more to get things ready, letting the bug show up. That's a shame that your answer got accepted – Davide Cannizzo Oct 05 '20 at 19:25
14

Do not use timers, you will run into a lot of probable states in which the user has a slow phone and it actually takes a lot longer than 100 ms to run, also, you wouldn't want too slow of a timer making it ridiculously un-reliable.

Below we do the following, we set a listener to our ViewTreeObserver and wait until a set number of children have been laid out in our ViewPager2's RecyclerView (it's inner working). Once we are sure x number of items have been laid out, we start our no-animation scroll to start at the position.

val recyclerView = (Your ViewPager2).getChildAt(0)

recyclerView.apply {
   val itemCount = adapter?.itemCount ?: 0
   if(itemCount >= #(Position you want to scroll to)) {
      viewTreeObserver.addOnGlobalLayoutListener(object: ViewTreeObserver.OnGlobalLayoutListener {
      override fun onGlobalLayout() {
         viewTreeObserver.removeOnGlobalLayoutListener(this)

         // False for without animation scroll
         (Your ViewPager2).scrollToPosition(#PositionToStartAt, false)
      }
   }
}
Daniel Kim
  • 899
  • 7
  • 11
14

First off, I think that the accepted answer shouldn't be @hosseinAmini 's, since it's suggesting to use a delay to work around the problem. You should first be looking for what the assumed bug is caused by, rather than trusting unreasonable solutions like that.

@Rune's proposal is correct, instead; so I'm quoting their code in my answer:

viewPager.post {
    viewPager.setCurrentItem(1, true)
}

The only thing I'd argue about is the aforementioned one's belief that their solution is just deferring the execution of that lambda in the next run cycle. This wouldn't make anything buggy work properly. Rather, what it is actually being done is deferring the execution of that lambda to once the view has been attached to a window, which implies it's also been added to a parent view. Indeed, there looks to be an issue as to changing the current ViewPager2 item before being attached to a window. Some evidence to support this claim follows:

  • Using whichever Handler won't work nearly as effectively.

    Handler(Looper.getMainLooper()).post {
        viewPager.setCurrentItem(1, true) // Not working properly
    }
    

    From a theoretical standpoint, it might incidentally work due to the ViewPager2 being attached to a window acquiring priority in the message queue of the main looper, but this shouldn't ever be relied upon as there's just no guarantee that it'll work (it's even more likely it won't) and if it even turned out to be working, further investigation running multiple tests should make my point clear.

  • View.handler gets null, which means the view hasn't been attached to any window yet.

    View.handler // = null
    

    Despite Android UI being tied to the main looper, which will always uniquely correspond to the main thread –hence also called the UI thread,– a weird design choice stands in the handler not being associated to each view until they get attached to a window. A reason why this may lay on the consequent inability of views to schedule any work on the main thread while they're not part of the hierarchy, which may turn useful when implementing a view controller that schedules view updates while unaware of their lifecycle (in which case it would employ the View's handler, if any — or just skip scheduling whatever it was going to if none).

EDIT:

Also, @josias has pointed out in a comment that it'd be clearer to use:

viewPager.doOnAttach {
    viewPager.setCurrentItem(1, true)
}

Thanks for that suggestion! It expresses better the actual intent, rather than relying on the behavior of the View.post method.

Davide Cannizzo
  • 2,826
  • 1
  • 29
  • 31
11

Do not use timers and all that stuff with 'post', it's not the reliable solution and just a piece of code that smells.

Instead, try use viewPager.setCurrentItem(1, false). That 'false' is about smoothScroll, you can't smooth scroll your viewPager2 when your activity is just opened. Tested it on a fragment in onViewCreated() method, it also didn't work with "true", but works with "false"

blinker
  • 599
  • 5
  • 18
  • yes works fine for me when setting `false` at starting of the page – Mahmoud Ayman Jul 01 '21 at 21:40
  • Yeah, in my case with `true` my fragment wasn't getting `onResume` and it messed thing up. With `false` it works fine. Definitely a `ViewPager2` bug, but at least we have a working solution... – algrid May 23 '22 at 15:29
7

As it was mentioned above you have to use setCurrentItem(position, smoothScroll) method on ViewPager2 in order to show selected item. To make it work you have to define a callback, here is an example:

ViewPager2.OnPageChangeCallback callback = new ViewPager2.OnPageChangeCallback() {
    @Override
    public void onPageSelected(int position) {
        super.onPageSelected(position);
    }
};

And then you have to register it as follow:

viewPager.registerOnPageChangeCallback(callback);

Also do not forget to unregister it:

viewPager.unregisterOnPageChangeCallback(callback);

When you call setCurrentItem(position) method it will call onPageSelected(int position) method from your callback passing your argument, and then method createFragment(int position) from FragmentStateAdapter class will be called to show your fragment.

Ibrokhim Kholmatov
  • 1,079
  • 2
  • 13
  • 16
  • Unfortunately, this is exactly what I do and it does not work (version 2 is probably still buggy). Actually the callback is called properly, but the adapter is requested to create the first fragment (most of the times, sometimes it works - therefore, there is probably a race condition in the event driven Android approach). Using version 1.0.0... – Jiří Křivánek Apr 22 '20 at 18:40
  • I would recommend you to follow this article https://www.raywenderlich.com/8192680-viewpager2-in-android-getting-started – Ibrokhim Kholmatov Apr 23 '20 at 09:07
5

I tried changing viewpager2 page in Handler().dely() and viewPager2.post{} and even 'viewPager2.get(0).post all didn't work for me, I'm using ViewPager with FragmentStateAdapter with Tablayout.

What worked for me is changing the position of the RecylerView in ViewPager2 after binding FragmentStateAdapter to yourViewPager2View.adapter manually:

 (yourViewPager2View[0] as RecyclerView).scrollToPosition(moveToTabNumber)

Why

My problem is onCreateFragment(position:Int):Fragmeet function in FragmentStateAdapter always starting fragment at 0 position no matter what pageNumber I set the page

viewPager.setCurrentItem = pageNumber

I checked where it's called in FragmentStateAdapter it's called in FragmentStateAdapter:

onBindViewHolder(final @NonNull FragmentViewHolder holder, int position)`

so all I needed is to force onBindViewHolder to call onCreateFragment(position:Int) with the page number I wanted.

Abed
  • 3,999
  • 1
  • 17
  • 28
  • 4
    You can try using `viewPager.setCurrentItem(pageNumber, false)` before the view appears. In my case, I called these inside `onViewCreated` method of my Fragment. hope it works for you as it did on my end. The method call won't work if you set the second parameter to `true`, not sure why.. – nadinnnnnne Aug 09 '20 at 08:05
  • 1
    Thanks a bunch! Your trick helped me to solve yet another ViewPaer2 mystery: setCurrentItem(x,false) results in double onPageSelected() call when x=0; interestingly enough, when x!=0 everything works as expected. – DmitryO. Mar 30 '21 at 18:54
4

mViewPager.setCurrentItem(1, true); ---> this is sufficient as you written above

That should work, in doubt, just check your position:

        @Override
        public void onPageSelected(int i) {
            if (LOG_DEBUG) Log.v(TAG, " ++++++++    onPageSelected:  " + i);
            mViewPager.setCurrentItem(i);
            //TODO You can use this position: to write other dependent logic

        }

and also check

getItem(int position) in PagerAdapter

or else paste your code.

hemen
  • 1,460
  • 2
  • 16
  • 35
3

I noticed that it works fine when the view is initially created if you opt to not animate it.

viewPager2.setCurrentItem(index, false)

This is usually fine depending on your use case - this initial/default item probably doesn't need to be animated in.

Tom
  • 6,946
  • 2
  • 47
  • 63
  • 1
    It is so simple it seems silly. Tried everything for initial default position on first creation or on restarting a viewpager2 activity to populate saved data. Haven't tested everything as of yet, but this is a very good answer. Thank you. – Hmerman6006 Aug 05 '22 at 20:56
1

I met the same problem. In my case, I make the viewPager2 Gone by default until network requests succeed, I fix it by setting the CurrentItem after I make the viewPager2 visible.

shopkeeper
  • 189
  • 1
  • 9
1

My answer may not be helpful now but i see no harm to post my expreince, i just came to this problem using ViewPager and ViewPager2 and unexpectedly solved it by just changing some line codes order.

Here is (java) solution for ViewPager:

    reviewWordViewPager.addOnPageChangeListener(changeListener);
    reviewWordViewPager.setCurrentItem(viewPosition, true/false);
    reviewWordTabIndicator.setupWithViewPager(reviewWordViewPager, true);

(Java) solution for ViewPager2:

wordViewPager.registerOnPageChangeCallback(viewPager2OnPageChangeCallback);
wordViewPager.setCurrentItem(vpPosition, true/false);
new TabLayoutMediator(tabIndicator, wordViewPager,
            ((tab, position) -> tab.setText(viewPagerTitle[position]))).attach();

I did not look up for ViewPager2 whether it needs the following old code used in ViewPager

    @Override
public int getItemPosition(@NonNull Object object) {
    // refresh all fragments when data set changed
    return POSITION_NONE;
}

But surprisingly no need for it in ViewPager2 to solve the problem i've been having, hope it helps others

In case you use context.startActivity to start new activities no need to use wordViewPager.setCurrentItem(item, smoothScroll) in your onResume function to get back to the last selected tab before you started new activity you just save ViewPager/ViewPager2 position like vpPisition = wordViewPager.getCurrentItem(); in onStop function.

vpPisition is a global variable.

Adrian Mole
  • 49,934
  • 160
  • 51
  • 83
StackOverflower
  • 369
  • 2
  • 12
1

as @Daniel Kim but a java version

RecyclerView rvOfViewPager2 = (RecyclerView) viewPager2.getChildAt(0);
    rvOfViewPager2.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener()
    {
        @Override
        public void onGlobalLayout()
        {
            rvOfViewPager2.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            viewPager2.setCurrentItem(currentTabId, false);
        }
    });
sucicf1
  • 335
  • 3
  • 6
0

First You need to Initilaze the Main activity under any listener or button You want then After that You need to put this Line.. here MainActvity is the Viewpager Main Class You are using and and 2 is the position where you want to move

 MainActivity main = (MainActivity ) mContext;
    main.selectTab(2, true);
Usman Ali
  • 425
  • 1
  • 9
  • 31