27

I know there is way to change animation duration of ViewPager programmatical slide (here).

But its not working on ViewPager2

I tried this:

try {
        final Field scrollerField = ViewPager2.class.getDeclaredField("mScroller");
        scrollerField.setAccessible(true);
        final ResizeViewPagerScroller scroller = new ResizeViewPagerScroller(getContext());
        scrollerField.set(mViewPager, scroller);
    } catch (Exception e) {
        e.printStackTrace();
    }

IDE gives me warning on "mScroller":

Cannot resolve field 'mScroller'

If we Run This code thats not going to work and give us Error below:

No field mScroller in class Landroidx/viewpager2/widget/ViewPager2; (declaration of 'androidx.viewpager2.widget.ViewPager2' appears in /data/app/{packagename}-RWJhF9Gydojax8zFyFwFXg==/base.apk)

So how we can acheive this functionality?

Saeed Arianmanesh
  • 1,269
  • 2
  • 22
  • 33

4 Answers4

37

Based on this issue ticket Android team is not planning to support such behavior for ViewPager2, advise from the ticket is to use ViewPager2.fakeDragBy(). Disadvantage of this method is that you have to supply page width in pixels, although if your page width is the same as ViewPager's width then you can use that value instead.

Here's sample implementation

fun ViewPager2.setCurrentItem(
    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()
}

To support RTL you have to flip the value supplied to ViewPager2.fakeDragBy(), so from above example instead of fakeDragBy(-currentPxToDrag) use fakeDragBy(currentPxToDrag) when using RTL.

Few things to keep in mind when using this, based on official docs:

  • negative values scroll forward, positive backward (flipped with RTL)

  • before calling fakeDragBy() use beginFakeDrag() and after you're finished endFakeDrag()

  • this API can be easily used with onPageScrollStateChanged from ViewPager2.OnPageChangeCallback, where you can distinguish between programmatical drag and user drag thanks to isFakeDragging() method
  • sample implementation from above doesn't have security checks if the given item is correct. Also consider adding cancellation capabilities for UI's lifecycle, it can be easily achieved with RxJava.
M Tomczynski
  • 15,099
  • 1
  • 15
  • 25
  • This example worked on my project, but , for some reason the items between the first and last one have some giggle effect when they slide, but the last item just slides into the right way, is this caused by the px of the offset? – Gastón Saillén Dec 30 '19 at 16:04
  • @GastónSaillén this behavior indicates that you might have used wrong value for page width in pixels. Jiggle was from overscrolling or not scrolling far enough? – M Tomczynski Dec 31 '19 at 12:16
  • or you meant that jiggle is in the middle of the scroll? – M Tomczynski Dec 31 '19 at 12:20
  • when it finishes to slide , it jiggles before stop a little bit at the end, the jiggle is at the end of the scroll – Gastón Saillén Dec 31 '19 at 22:05
  • @GastónSaillén try playing with pagePxWidth value, modify it slightly and see what happens. If by jiggle you mean scroll past the item you selected and snap back to center it'd mean that setCurrentItem method is overscrolling and viewpager is snapping back to selected item. – M Tomczynski Jan 01 '20 at 09:56
  • yes, thats the jiggle effect, if I change pagePxWidth will it adjust to all screen sizes as well ? – Gastón Saillén Jan 02 '20 at 18:20
  • @MTomczyński, That makes sense. Can you share the java code that has same functionality? – Gary Chen Oct 28 '20 at 02:58
  • @MTomczyński I'm using this code in conjunction with code that essentially "auto-scrolls" items every 1 sec, plus a viewpager that shows a portion of adjacent items. What happens is that sometimes viewpager overscrolls, ending up 1 item away from the intended item. What can you suggest I tweak? – Alvin Dizon Mar 10 '21 at 02:41
  • nvm @MTomczyński I figured out the correct pagePxWidth value – Alvin Dizon Mar 12 '21 at 01:44
  • @akubi Sorry! I didn't had a chance to look into it. Out of curiosity, what was the solution? – M Tomczynski Mar 13 '21 at 06:29
  • 1
    @MTomczyński no worries, I arrived at the correct pagePxWidth by subtracting the left and right padding I applied to the inner recyclerview of ViewPager2 from the actual ViewPager2 width in pixels. – Alvin Dizon Mar 15 '21 at 00:23
  • I am using this in conjunction with `setPageTransformer` that adds offsets so that adjacent pages are "peeking", and it's not working so well :( Sometimes this method results in shaky animation, and sometimes it isn't enough to reach target item – Alvin Dizon Mar 30 '21 at 06:37
  • you have to calculate offsets and paddings into the `pagePxWidth` value. Given implementation is only a sample one for the case where your page is the same width as the ViewPager view itself. When you'll calculate `pagePxWidth` correctly there won't be any jiggle – M Tomczynski Mar 31 '21 at 07:22
  • @MTomczyński yep I tried incorporating the correct offsets and paddings but still no success. I guess this is just a limitation of the ViewPager2 itself :( – Alvin Dizon Apr 11 '21 at 23:44
  • @akubi you could try passing the width of the displayed child view instead of calculating it with padding? You could double-check the calculations that way ;) – M Tomczynski Apr 12 '21 at 09:42
  • Can that code be applicable to Java? Since ViewPager2 is final, we can't? – chitgoks Oct 23 '21 at 03:58
  • @chitgoks oh for sure, move the ViewPager2 receiver to argument list of the method and transform the code in the method itself to Java. Because you can't use default parameters you'll have to specify them explicitly on the method call, like for pagePxWidth – M Tomczynski Oct 23 '21 at 06:41
  • @MTomczynski how can this be applied with a ViewPager2 TabLayout combo? – chitgoks Nov 05 '21 at 13:10
  • 1
    I am getting this error randomly not everytime after using this approach : "Fatal Exception: java.lang.IllegalStateException Cannot change current item when ViewPager2 is fake dragging" . – Waqar Vicky Feb 15 '23 at 09:25
  • just reiterating here that `fakeDragBy` is incredibly unstable, when I had it in my app it would always in one way or another for about 0.5% of users. I strongly advise against implementing a custom solution like this – avalancha May 23 '23 at 13:23
6

ViewPager2 team made it REALLY hard to change the scrolling speed. If you look at the method setCurrentItemInternal, they instantiate their own private ScrollToPosition(..) object. along with state management code, so this would be the method that you would have to somehow override.

As a solution from here: https://issuetracker.google.com/issues/122656759, they say use (ViewPager2).fakeDragBy() which is super ugly.

Not the best, just have to wait form them to give us an API to set duration or copy their ViewPager2 code and directly modify their LinearLayoutImpl class.

Daniel Kim
  • 899
  • 7
  • 11
2

The solution given by M Tomczynski (Refer: https://stackoverflow.com/a/59235979/7608625) is good one. But I faced app randomly crashing when used along with inbuilt setCurrentItem() because of fakeDrag even though i ensured to call endDrag() before using setCurrentItem(). So I modified the solution to use viewpager's embeded recycler view's scrollBy() function which is working very well without any issues.

fun ViewPager2.setCurrentItem(
    index: Int,
    duration: Long,
    interpolator: TimeInterpolator = PathInterpolator(0.8f, 0f, 0.35f, 1f),
    pageWidth: Int = width - paddingLeft - paddingRight,
) {
    val pxToDrag: Int = pageWidth * (index - currentItem)
    val animator = ValueAnimator.ofInt(0, pxToDrag)
    var previousValue = 0

    val scrollView = (getChildAt(0) as? RecyclerView)
    animator.addUpdateListener { valueAnimator ->
        val currentValue = valueAnimator.animatedValue as Int
        val currentPxToDrag = (currentValue - previousValue).toFloat()
        scrollView?.scrollBy(currentPxToDrag.toInt(), 0)
        previousValue = currentValue
    }

    animator.addListener(object : Animator.AnimatorListener {
        override fun onAnimationStart(animation: Animator) { }
        override fun onAnimationEnd(animation: Animator) {
            // Fallback to fix minor offset inconsistency while scrolling
            setCurrentItem(index, true)
            post { requestTransform() } // To make sure custom transforms are applied
        }
        override fun onAnimationCancel(animation: Animator) { /* Ignored */ }
        override fun onAnimationRepeat(animation: Animator) { /* Ignored */ }
    })

    animator.interpolator = interpolator
    animator.duration = duration
    animator.start()
}
1

When you want your ViewPager2 to scroll with your speed, and in your direction, and by your number of pages, on some button click, call this function with your parameters. Direction could be leftToRight = true if you want it to be that, or false if you want from right to left, duration is in miliseconds, numberOfPages should be 1, except when you want to go all the way back, when it should be your number of pages for that viewPager:

fun fakeDrag(viewPager: ViewPager2, leftToRight: Boolean, duration: Long, numberOfPages: Int) {
    val pxToDrag: Int = viewPager.width
    val animator = ValueAnimator.ofInt(0, pxToDrag)
    var previousValue = 0
    animator.addUpdateListener { valueAnimator ->
        val currentValue = valueAnimator.animatedValue as Int
        var currentPxToDrag: Float = (currentValue - previousValue).toFloat() * numberOfPages
        when {
            leftToRight -> {
                currentPxToDrag *= -1
            }
        }
        viewPager.fakeDragBy(currentPxToDrag)
        previousValue = currentValue
    }
    animator.addListener(object : Animator.AnimatorListener {
        override fun onAnimationStart(animation: Animator?) { viewPager.beginFakeDrag() }
        override fun onAnimationEnd(animation: Animator?) { viewPager.endFakeDrag() }
        override fun onAnimationCancel(animation: Animator?) { /* Ignored */ }
        override fun onAnimationRepeat(animation: Animator?) { /* Ignored */ }
    })
    animator.interpolator = AccelerateDecelerateInterpolator()
    animator.duration = duration
    animator.start()
}
Reza Rahemtola
  • 1,182
  • 7
  • 16
  • 30
Dragan N
  • 21
  • 4