75

I'm changing slide with the following code:

viewPager.setCurrentItem(index++, true);

But it changes too fast. Is there a way to set manually the animation speed?

Lii
  • 11,553
  • 8
  • 64
  • 88
User
  • 31,811
  • 40
  • 131
  • 232
  • Not sure what your search looked like, but take a look at [this answer](http://stackoverflow.com/a/9731345/1029225) here on SO. That should be precisely what you're after. – MH. May 30 '12 at 07:55
  • Actually that's quite similar to my solution, though I used a factor instead of absolute duration (because in a `ViewPager` the duration may depend on how many pages you scroll through). – Oleg Vaskevich Jan 06 '13 at 05:58

7 Answers7

114

I've wanted to do myself and have achieved a solution (using reflection, however). I haven't tested it yet but it should work or need minimal modification. Tested on Galaxy Nexus JB 4.2.1. You need to use a ViewPagerCustomDuration in your XML instead of ViewPager, and then you can do this:

ViewPagerCustomDuration vp = (ViewPagerCustomDuration) findViewById(R.id.myPager);
vp.setScrollDurationFactor(2); // make the animation twice as slow

ViewPagerCustomDuration.java:

import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.animation.Interpolator;

import java.lang.reflect.Field;

public class ViewPagerCustomDuration extends ViewPager {

    public ViewPagerCustomDuration(Context context) {
        super(context);
        postInitViewPager();
    }

    public ViewPagerCustomDuration(Context context, AttributeSet attrs) {
        super(context, attrs);
        postInitViewPager();
    }

    private ScrollerCustomDuration mScroller = null;

    /**
     * Override the Scroller instance with our own class so we can change the
     * duration
     */
    private void postInitViewPager() {
        try {
            Field scroller = ViewPager.class.getDeclaredField("mScroller");
            scroller.setAccessible(true);
            Field interpolator = ViewPager.class.getDeclaredField("sInterpolator");
            interpolator.setAccessible(true);

            mScroller = new ScrollerCustomDuration(getContext(),
                    (Interpolator) interpolator.get(null));
            scroller.set(this, mScroller);
        } catch (Exception e) {
        }
    }

    /**
     * Set the factor by which the duration will change
     */
    public void setScrollDurationFactor(double scrollFactor) {
        mScroller.setScrollDurationFactor(scrollFactor);
    }

}

ScrollerCustomDuration.java:

import android.annotation.SuppressLint;
import android.content.Context;
import android.view.animation.Interpolator;
import android.widget.Scroller;

public class ScrollerCustomDuration extends Scroller {

    private double mScrollFactor = 1;

    public ScrollerCustomDuration(Context context) {
        super(context);
    }

    public ScrollerCustomDuration(Context context, Interpolator interpolator) {
        super(context, interpolator);
    }

    @SuppressLint("NewApi")
    public ScrollerCustomDuration(Context context, Interpolator interpolator, boolean flywheel) {
        super(context, interpolator, flywheel);
    }

    /**
     * Set the factor by which the duration will change
     */
    public void setScrollDurationFactor(double scrollFactor) {
        mScrollFactor = scrollFactor;
    }

    @Override
    public void startScroll(int startX, int startY, int dx, int dy, int duration) {
        super.startScroll(startX, startY, dx, dy, (int) (duration * mScrollFactor));
    }
}
starball
  • 20,030
  • 7
  • 43
  • 238
Oleg Vaskevich
  • 12,444
  • 6
  • 63
  • 80
  • 2
    your solution is the nicest I've found so far, but when the line "scroller.set(this, mScroller);" is executed, I got error "IllegalArgumentException: invalid value for field". I guess this is because of the use of a ViewPager subclass... any fix ? – elgui Jan 07 '13 at 12:25
  • also, how the var ViewPagerCustomDuration .mScroller can be used ? it appears not to be linked to anything... – elgui Jan 07 '13 at 13:32
  • I'll look into it in a bit. It should be possible. Also mScroller is just used internally by `ViewPager`. – Oleg Vaskevich Jan 07 '13 at 14:04
  • Hmm I tested this to work for me; I'm not too sure why you're getting an `IllegalArgumentException`. – Oleg Vaskevich Jan 08 '13 at 06:28
  • thanks a lot for taking the time testing =) I'm going to create a separated question with details about problems I have, so you can have a closer look and get "rewarded" for your answer. I'll post the link here as soon as it's ready. – elgui Jan 08 '13 at 10:32
  • ok here it is =) http://stackoverflow.com/questions/14215757/increase-viewpager-smoothscroll-duration – elgui Jan 08 '13 at 13:13
  • Isn't there a more official way to achieve it? Anyway, I've requested to allow to change the animation duration here: https://code.google.com/p/android/issues/detail?id=178484 . Please cast your vote there, and write your opinion if there is something I've missed. – android developer Jun 30 '15 at 10:15
  • 1
    getting null pointer error on setScrollDurationFactor this , when proguard is enable android – aj0822ArpitJoshi Mar 23 '17 at 11:51
  • @aj0822ArpitJoshi Add proguard rule "-keep class android.support.v4.view.ViewPager { *; }" – Lukas Novak Mar 30 '17 at 20:26
  • For a smoother scrolling experience, like if your doing a slideshow, you can also easily change the interpolator: `Interpolator interp = new LinearInterpolator(); interpolator.set(null, interp); mScroller = new ScrollerCustomDuration(getContext(), interp);` –  Apr 30 '17 at 13:09
  • How come there is nothing in the `catch` statement of `ViewPagerCustomDuration`? if an exception is thrown `mScroller` becomes null and then calling `setScrollDurationFactor` throws `NullPointerException` – Hossein Shahdoost Jul 02 '17 at 11:12
  • @HosseinShahdoost It's been a while, but the `catch` statement is needed for checked exceptions w/Java when using reflection. `mScroller` is initialized in `ViewPager` so it wouldn't be null. But you're right - it would be best to catch only certain exceptions and handle `mScroller` not being updated, so that in case the private API changes at some point, the code still runs instead of crashing. Feel free to update. – Oleg Vaskevich Jul 06 '17 at 23:13
  • @OlegVaskevich it can actually happen, in my case (since I forgot about the reflection in code) after obfuscation the name of class was changed, and mScroller was not being created anymore. it took me hours to figure out why this is happening partly because I was getting the error at some other part of the code – Hossein Shahdoost Aug 13 '17 at 09:20
  • I've noticed an issue with this code. If I start the slide, and then quickly tap the viewPager while it is animating the sliding effect, it stops the animation, so you are now stuck between 2 pages of the ViewPager (seeing both of them, each partially). How can I fix this? – android developer Sep 07 '17 at 11:03
  • Perfect answer !! Thank you for sharing. – Archa Nov 08 '17 at 23:04
42

I have found better solution, based on @df778899's answer and the Android ValueAnimator API. It works fine without reflection and is very flexible. Also there is no need for making custom ViewPager and putting it into android.support.v4.view package. Here is an example:

private void animatePagerTransition(final boolean forward) {

    ValueAnimator animator = ValueAnimator.ofInt(0, viewPager.getWidth());
    animator.addListener(new Animator.AnimatorListener() {
        @Override
        public void onAnimationStart(Animator animation) {
        }

        @Override
        public void onAnimationEnd(Animator animation) {
            viewPager.endFakeDrag();
        }

        @Override
        public void onAnimationCancel(Animator animation) {
            viewPager.endFakeDrag();
        }

        @Override
        public void onAnimationRepeat(Animator animation) {
        }
    });

    animator.setInterpolator(new AccelerateInterpolator());
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

        private int oldDragPosition = 0;

        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            int dragPosition = (Integer) animation.getAnimatedValue();
            int dragOffset = dragPosition - oldDragPosition;
            oldDragPosition = dragPosition;
            viewPager.fakeDragBy(dragOffset * (forward ? -1 : 1));
        }
    });

    animator.setDuration(AppConstants.PAGER_TRANSITION_DURATION_MS);
    if (viewPager.beginFakeDrag()) {
        animator.start();
    }
}

UPDATE:

Just checked if this solution can be used to swipe several pages at once (for example if first page should be showed after the last one). This is slightly modified code to handle specified page count:

private int oldDragPosition = 0;

private void animatePagerTransition(final boolean forward, int pageCount) {
    // if previous animation have not finished we can get exception
    if (pagerAnimation != null) {
        pagerAnimation.cancel();
    }
    pagerAnimation = getPagerTransitionAnimation(forward, pageCount);
    if (viewPager.beginFakeDrag()) {    // checking that started drag correctly
        pagerAnimation.start();
    }
}

private Animator getPagerTransitionAnimation(final boolean forward, int pageCount) {
    ValueAnimator animator = ValueAnimator.ofInt(0, viewPager.getWidth() - 1);
    animator.addListener(new Animator.AnimatorListener() {
        @Override
        public void onAnimationStart(Animator animation) {
        }

        @Override
        public void onAnimationEnd(Animator animation) {
            viewPager.endFakeDrag();
        }

        @Override
        public void onAnimationCancel(Animator animation) {
            viewPager.endFakeDrag();
        }

        @Override
        public void onAnimationRepeat(Animator animation) {
            viewPager.endFakeDrag();
            oldDragPosition = 0;
            viewPager.beginFakeDrag();
        }
    });

    animator.setInterpolator(new AccelerateInterpolator());
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            int dragPosition = (Integer) animation.getAnimatedValue();
            int dragOffset = dragPosition - oldDragPosition;
            oldDragPosition = dragPosition;
            viewPager.fakeDragBy(dragOffset * (forward ? -1 : 1));
        }
    });

    animator.setDuration(AppConstants.PAGER_TRANSITION_DURATION_MS / pageCount); // remove divider if you want to make each transition have the same speed as single page transition
    animator.setRepeatCount(pageCount);

    return animator;
}
darnmason
  • 2,672
  • 1
  • 17
  • 23
lobzik
  • 10,974
  • 1
  • 27
  • 32
  • so it does fake dragging? seems like a clever way to achieve it, but what happens if the user starts dragging during this operation? Also, what if I want it to scroll to the first page (because when it reaches the last page, I'd like to be able to make it reach the first page again) ? – android developer Jun 30 '15 at 10:17
  • @androiddeveloper, I've used this solution with NonSwipeableViewPager (http://stackoverflow.com/a/9650884/2923816). So I have no issues with user interaction during fake dragging. But if someone needs to keep swipe gesture, it could be achieved by overriding ViewPager and controlling the animation when the user touches the ViewPager. I didn't try it, but I suppose it is possible and should work fine. For a looping pager you should adjust the animation code, but the adjustments depend on the concrete implementation of looping. – lobzik Jun 30 '15 at 11:05
  • About the user interaction, ok (since the animation isn't that slow, I don't mind). About scrolling to the first page, I don't understand. Suppose there are just 3 pages, and I'm on the last one, how can I make it scroll to the first page using the customized duration you've set? would using the repeating property of the animator help? – android developer Jun 30 '15 at 11:31
  • @androiddeveloper yes you can use repeating property. In case when you want to get back to the first page by the same time as usual page transition gets, you should also change transition duration for this returning animation. – lobzik Jun 30 '15 at 12:08
  • I don't understand. Can you please modify the code to show how to do this? Maybe instead of "forward" have an integer that says how many pages to go (and if negative, go backwards) ? – android developer Jun 30 '15 at 12:48
  • Thank you, but I see a weird issue. I have just 3 pages. When it tries scrolling to the first page (from the last one), it actually scrolls from the last page to the one before, twice. – android developer Jun 30 '15 at 13:29
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/81976/discussion-between-lobzik-and-android-developer). – lobzik Jun 30 '15 at 13:33
  • Excellent answer, this is a much better solution than using reflection. Thank you. – Mark Oct 25 '15 at 07:32
  • Works very well, especially in combination with a circular modification of the ViewPager for an endless forward animation, see [link](http://stackoverflow.com/a/24244144/2418367). Thanks a lot. Once more it was a good idea to not only read the top answer =) – Dominikus K. Jan 08 '16 at 18:56
  • Hi, I'm using this solution to scroll one page, didn't copied the updated answer. It's working fine on several phones but on the nexus 10 tablet it's scrolling two pages at a time. I don't really understand what's going one. Any ideas? – Roberto Jun 03 '16 at 11:20
  • @lobzik Could you please help me how to use above solution in viewpager or how to call? – rupesh Dec 07 '18 at 10:23
  • In the first example, it will only work with 2 pages. I you have more you need to divide the width by the number of pages ;) – Ben-J Apr 02 '19 at 15:36
  • The problem with this solution is that I trigger transition on a button click. But if I click on the button *while* the transition is in progress, then it results in a mess in my program. Need to rebuild the whole logic with disabling buttons and stuff... A no-go, unfortunately... – Eugene Kartoyev Apr 04 '20 at 20:54
14
 public class PresentationViewPager extends ViewPager {

    public static final int DEFAULT_SCROLL_DURATION = 250;
    public static final int PRESENTATION_MODE_SCROLL_DURATION = 1000;

    public PresentationViewPager (Context context) {
        super(context);
    }

    public PresentationViewPager (Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public void setDurationScroll(int millis) {
        try {
            Class<?> viewpager = ViewPager.class;
            Field scroller = viewpager.getDeclaredField("mScroller");
            scroller.setAccessible(true);
            scroller.set(this, new OwnScroller(getContext(), millis));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public class OwnScroller extends Scroller {

        private int durationScrollMillis = 1;

        public OwnScroller(Context context, int durationScroll) {
            super(context, new DecelerateInterpolator());
            this.durationScrollMillis = durationScroll;
        }

        @Override
        public void startScroll(int startX, int startY, int dx, int dy, int duration) {
            super.startScroll(startX, startY, dx, dy, durationScrollMillis);
        }
    }
}
Cícero Moura
  • 2,027
  • 1
  • 24
  • 36
6

Better solution is to simply access the private fields by creating the class in the support package. EDIT This is bound to the MAX_SETTLE_DURATION of 600ms, set by the ViewPagerclass.

package android.support.v4.view;

import android.content.Context;
import android.util.AttributeSet;

public class SlowViewPager extends ViewPager {

    // The speed of the scroll used by setCurrentItem()
    private static final int VELOCITY = 200;

    public SlowViewPager(Context context) {
        super(context);
    }

    public SlowViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) {
        setCurrentItemInternal(item, smoothScroll, always, VELOCITY);
    }

}

You can, of course, then add a custom attribute so this can be set via XML.

Paul Burke
  • 25,496
  • 9
  • 66
  • 62
  • Thanks, this does seem to work however even with VELOCITY set to 1 the scroll is still pretty fast IMHO, I wish I could slow it further. – Oliver Pearmain Feb 27 '14 at 09:08
  • @HaggleLad Yeah, unfortunately, my approach is bound to the max animation duration of 600 ms. If you need it to be longer than that, you'll need to use a different method. I will update my answer to note this. – Paul Burke Feb 27 '14 at 12:48
  • 8
    isn't setCurrentItemInternal a private method? – Marty Miller Nov 25 '14 at 02:28
  • 2
    @MartyMiller Yes, but if the support lib is part of your project, you can create a class in it's package. – Paul Burke Nov 25 '14 at 02:41
  • 1
    editing support library is not a valid manner, if i decided to upgrade it, this code wont work, and also in my personal case, i dont have the source code for support library (and wont edit it if i have) – Ahmed Adel Ismail Mar 23 '15 at 16:45
  • @EL-conteDe-monteTereBentikh This is not editing the library, but it is a hack, of sorts. – Paul Burke Mar 23 '15 at 16:46
  • Best answer if 600 ms is enough for you. – jeff Oct 13 '15 at 09:35
  • Isn't it considered bad practice to edit support library code? – Teilmann Feb 03 '16 at 09:26
  • It's a custom class, in your project sources. You may name your package with any name, including the used one. The `setCurrentItemInternal()` method doesn't have access modifier in the library source code so its visibility is the same package. So if you put your child class to that package you may legally override this type of methods – mortalis Jan 17 '18 at 20:42
0

Here is my code used in Librera Reader

public class MyViewPager extends ViewPager {

  public MyViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
        initMyScroller();
    }

    private void initMyScroller() {
        try {
            Class<?> viewpager = ViewPager.class;
            Field scroller = viewpager.getDeclaredField("mScroller");
            scroller.setAccessible(true);
            scroller.set(this, new MyScroller(getContext())); // my liner scroller

            Field mFlingDistance = viewpager.getDeclaredField("mFlingDistance");
            mFlingDistance.setAccessible(true);
            mFlingDistance.set(this, Dips.DP_10);//10 dip

            Field mMinimumVelocity = viewpager.getDeclaredField("mMinimumVelocity");
            mMinimumVelocity.setAccessible(true);
            mMinimumVelocity.set(this, 0); //0 velocity

        } catch (Exception e) {
            LOG.e(e);
        }

    }

    public class MyScroller extends Scroller {
        public MyScroller(Context context) {
            super(context, new LinearInterpolator()); // my LinearInterpolator
        }

        @Override
        public void startScroll(int startX, int startY, int dx, int dy, int duration) {
            super.startScroll(startX, startY, dx, dy, 175);//175 duration
        }
    }

 }
Foobnix
  • 719
  • 5
  • 7
0

I used Cicero Moura's version to make a Kotlin class that still works perfectly as of Android 10.

import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.animation.DecelerateInterpolator
import android.widget.Scroller
import androidx.viewpager.widget.ViewPager

class CustomViewPager(context: Context, attrs: AttributeSet) :
        ViewPager(context, attrs) {


    private companion object {
        const val DEFAULT_SPEED = 1000
    }

    init {
        setScrollerSpeed(DEFAULT_SPEED)
    }

    var scrollDuration = DEFAULT_SPEED
        set(millis) {
            setScrollerSpeed(millis)
        }

    private fun setScrollerSpeed(millis: Int) {
        try {
            ViewPager::class.java.getDeclaredField("mScroller")
                    .apply {
                        isAccessible = true
                        set(this@CustomViewPager, OwnScroller(millis))
                    }

        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    inner class OwnScroller(private val durationScrollMillis: Int) : Scroller(context, AccelerateDecelerateInterpolator()) {
        override fun startScroll(startX: Int, startY: Int, dx: Int, dy: Int, duration: Int) {
            super.startScroll(startX, startY, dx, dy, durationScrollMillis)
        }
    }
}

Initializing from the activity class:

viewPager.apply {
    scrollDuration = 2000
    adapter = pagerAdapter
}
Eugene Kartoyev
  • 501
  • 4
  • 11
-1

After wasting my whole day I found a solution set offscreenPageLimit to total no. of the page.

In order to keep a constant length ViewPager scrolls smooth, setOffScreenLimit(page.length) will keep all the views in memory. However, this poses a problem for any animations that involves calling View.requestLayout function (e.g. any animation that involves making changes to the margin or bounds). It makes them really slow (as per Romain Guy) because the all of the views that's in memory will be invalidated as well. So I tried a few different ways to make things smooth but overriding requestLayout and other invalidate methods will cause many other problems.

A good compromise is to dynamically modify the off screen limit so that most of the scrolls between pages will be very smooth while making sure that all of the in page animations smooth by removing the views when the user. This works really well when you only have 1 or 2 views that will have to make other views off memory.

***Use this when no any solution works because by setting offeset limit u will load all the fragments at the same time