Slowing speed of Viewpager controller in android
Asked Answered
C

10

110

Is there any way to slow the scroll speed with the viewpager adaptor in android?


You know, I've been looking at this code. I can't figure out what I'm dong wrong.

try{ 
    Field mScroller = mPager.getClass().getDeclaredField("mScroller"); 
    mScroller.setAccessible(true); 
    Scroller scroll = new Scroller(cxt);
    Field scrollDuration = scroll.getClass().getDeclaredField("mDuration");
    scrollDuration.setAccessible(true);
    scrollDuration.set(scroll, 1000);
    mScroller.set(mPager, scroll);
}catch (Exception e){
    Toast.makeText(cxt, "something happened", Toast.LENGTH_LONG).show();
} 

It doesn't change anything yet no exceptions occur?

Councilor answered 16/11, 2011 at 16:37 Comment(2)
try this antoniocappiello.com/2015/10/31/…Adiathermancy
child.setNestedScrollingEnabled(false); worked for meTachylyte
S
238

I've started with HighFlyer's code which indeed changed the mScroller field (which is a great start) but didn't help extend the duration of the scroll because ViewPager explicitly passes the duration to the mScroller when requesting to scroll.

Extending ViewPager didn't work as the important method (smoothScrollTo) can't be overridden.

I ended up fixing this by extending Scroller with this code:

public class FixedSpeedScroller extends Scroller {
    
    private int mDuration = 5000;

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

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

    public FixedSpeedScroller(Context context, Interpolator interpolator, boolean flywheel) {
        super(context, interpolator, flywheel);
    }
    
    
    @Override
    public void startScroll(int startX, int startY, int dx, int dy, int duration) {
        // Ignore received duration, use fixed one instead
        super.startScroll(startX, startY, dx, dy, mDuration);
    }
    
    @Override
    public void startScroll(int startX, int startY, int dx, int dy) {
        // Ignore received duration, use fixed one instead
        super.startScroll(startX, startY, dx, dy, mDuration);
    }
}

And using it like this:

try {
    Field mScroller;
    mScroller = ViewPager.class.getDeclaredField("mScroller");
    mScroller.setAccessible(true); 
    FixedSpeedScroller scroller = new FixedSpeedScroller(mPager.getContext(), sInterpolator);
    // scroller.setFixedDuration(5000);
    mScroller.set(mPager, scroller);
} catch (NoSuchFieldException e) {
} catch (IllegalArgumentException e) {
} catch (IllegalAccessException e) {
}

I've basically hardcoded the duration to 5 seconds and made my ViewPager use it.

Splenomegaly answered 16/3, 2012 at 3:17 Comment(13)
can u please tell me what is sInterpolator??Endodontics
'Interpolator' is the rate at which animations are run, e.g they can accelerate, decelerate, and much more. There is a constructor without an interpolator, I think that should work, so simply try to remove that argument from the call. If that doesn't work, add: 'Interpolator sInterpolator = new AccelerateInterpolator()'Splenomegaly
I found DecelerateInterpolator to work better in this case, it maintains the speed of the initial swipe and then slowly decelerates.Rhearheba
Hi, do you have any idea why scroller.setFixedDuration(5000); gives an error? It says "The method setFixedDuration(int) is undefined"Tibia
Yeah, I had a version where I can control the speed via a new method added to FixedSpeedScroller, but I didn't copy it to this example, just delete that line, it should work. See my edit.Splenomegaly
I ended up writing my own solution (similar to this one) but it uses a factor rather than setting an explicit duration; see below.Poteat
@Splenomegaly Note that you can override smoothScrollTo if you put your FixedSpeedScroller in package android.support.v4.viewKeratinize
it is not stopping smoothly, what to do for thatGloucestershire
you should use the FixedSpeedScroller(context, interpolator) constructor, and supply an interpolator the eases, for example Android's DecelerateInterpolator. Check out developer.android.com/reference/android/view/animation/… for lots of interpolators you can useSplenomegaly
If you are using proguard don't forget to add this line: -keep class android.support.v4.** {*;} otherwise you'll get null pointer when getDeclaredField()Propagandism
Good solution! Initially i've started to subclass ViewPager, but seems it's not necessary! Just enough to subclass Scroller.Filamentary
what should be the value of sInterpolator??Corvus
you can try Interpolator sInterpolator = new AccelerateInterpolator()Splenomegaly
P
62

I've wanted to do myself and have achieved a solution (using reflection, however). It's similar to the accepted solution but uses the same interpolator and only changes the duration based on a factor. 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 {
            Class<?> viewpager = ViewPager.class;
            Field scroller = viewpager.getDeclaredField("mScroller");
            scroller.setAccessible(true);
            Field interpolator = viewpager.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));
    }

}

Hope this helps someone!

Poteat answered 8/1, 2013 at 6:30 Comment(6)
I like this solution better than the accepted answer because the custom ViewPager is a drop-in class with minimal changes to existing code. Having the custom scroller as a static inner class of the custom ViewPager makes it even more encapsulated.Vouge
If you are using proguard don't forget to add this line: -keep class android.support.v4.** {*;} otherwise you'll get null pointer when getDeclaredField()Propagandism
I suggest updating to a ternary, because you don't want to allow anyone to pass in a 0 factor, this will break your time duration. Update the final parameter that you are passing in to mScrollFactor > 0 ? (int) (duration * mScrollFactor) : durationPhotographer
I also suggest that you do a null check when setting the scroll factor in you class, ViewPagerCustomDuration. You instantiate with mScroller as a null object, you should confirm that it is still not null when trying to set the scroll factor on that object.Photographer
@Photographer valid comments, although it seems you favor a more defensive programming style that's IMO suited more for opaque libraries than a snippet on SO. Setting the scroll factor to <= 0 as well as calling setScrollDurationFactor on a non-instantiated view is something that most Android devs should be able to avoid doing without runtime checks.Poteat
getting null pointer issue on setScrollDurationFactor when prouguard is enable androidWarrior
S
46

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() - ( forward ? viewPager.getPaddingLeft() : viewPager.getPaddingRight() ));
    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);
    viewPager.beginFakeDrag();
    animator.start();
}
Subjunctive answered 22/6, 2015 at 9:44 Comment(6)
I think this is underrated. It makes use of startFakeDrag and endFakeDrag which are meant to be for doing more than is available in ViewPager out of the box. This works great for me and is exactly what I was looking forUrba
That's a great solution, thank you. One small enhancement, though: to make it work for a viewPager with padding, the first line should be ... ValueAnimator.ofInt(0, viewPager.getWidth() - ( forward ? viewPager.getPaddingLeft() : viewPager.getPaddingRight() ));Battista
this was by far the simplest/effcient/liable solution since it disregards reflection and avoids sealing violations like the solutions aboveBrakesman
@lobzic, Where exactly do you call your animatePagerTransition(final boolean forward method? That's what I'm confused about. Do you call it in one of the ViewPager.OnPageChangeListener interface methods? Or somewhere else?Injured
No ugly reflection hacks. Easily configurable. A very good answer.Exculpate
To anybody using this code, I would advise you to be very careful with the distance of the animator and think about the width you actually want to scroll. If you scroll too far, with a rather fast (i.e. low) value for PAGER_TRANSITION_DURATION_MS then you may fling your pager two pages instead of one by accidentTrunks
A
18

As you can see in ViewPager sources, duration of fling controlled by mScroller object. In documantation we may read:

The duration of the scroll can be passed in the constructor and specifies the maximum time that the scrolling animation should take

So, if you want to control speed, you may change mScroller object via reflection.

You should write something like this:

setContentView(R.layout.main);
mPager = (ViewPager)findViewById(R.id.view_pager);
Field mScroller = ViewPager.class.getDeclaredField("mScroller");   
mScroller.setAccessible(true);
mScroller.set(mPager, scroller); // initialize scroller object by yourself 
Alleviate answered 16/11, 2011 at 18:1 Comment(4)
I'm a little confused on how to do this - I'm setting up the ViewPager in the XML file and I assigned it to a mPager, and setAdapter (mAdapter)... Should I create a class which extends ViewPager?Councilor
You should write something like this: setContentView(R.layout.main); mPager = (ViewPager)findViewById(R.id.view_pager); Field mScroller = ViewPager.class.getDeclaredField("mScroller"); mScroller.setAccessible(true); mScroller.set(mPager, scroller); // initialize scroller object by yourselfAlleviate
I just posted one more answer... It's not working... Would you have some time to give me direction as to why not?Councilor
After more careful look at sources: find mScroller.startScroll(sx, sy, dx, dy); in ViewPager source and take look at Scroller realization. startScroll calls other startScroll with DEFAULT_DURATION paramater. Its impossible to make another value for static final field, so one opportunity left - override smoothScrollTo of ViewPagerAlleviate
C
16

This is not perfect solution, you can't make velocity slower because it's an int. But for me it's slow enough and I don't have to use reflection.

Notice the package where the class is. smoothScrollTo has package visibility.

package android.support.v4.view;

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

public class SmoothViewPager extends ViewPager {
    private int mVelocity = 1;

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

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

    @Override
    void smoothScrollTo(int x, int y, int velocity) {
        //ignore passed velocity, use one defined here
        super.smoothScrollTo(x, y, mVelocity);
    }
}
Countercharge answered 21/11, 2013 at 14:23 Comment(4)
Note that the first line : "package android.support.v4.view;" is mandatory and you should create such package at your src root. If you won't do it your code won't compile.Album
@ElhananMishraky exactly, that's what I meant with Notice the package where the class is.Countercharge
Wait, wait, wait, wait... wuuuut? You can use sdk's package-local fields/methods, if you just make Android Studio think you're using the same package? Am I the only one who's smelling some sealing violation?Apartment
This is a really nice solution, but it wasn't working for me neither since the Interpolator used by the ViewPager Scroller doesn't behave nice in this case. So I combined your solution with the others using reflection to get the mScroller. This way, I can use the original Scroller when the user paginates, and my custom Scroller when I change pages programmatically with setCurrentItem().Pawsner
C
8

The fakeDrag methods on ViewPager seem to provide an alternative solution.

For example this will page from item 0 to 1:

rootView.setOnClickListener(new OnClickListener() {
  @Override
  public void onClick(View v) {
      ViewPager pager = (ViewPager) getActivity().findViewById(R.id.pager);
      //pager.setCurrentItem(1, true);
      pager.beginFakeDrag();
      Handler handler = new Handler();
      handler.post(new PageTurner(handler, pager));
  }
});


private static class PageTurner implements Runnable {
  private final Handler handler;
  private final ViewPager pager;
  private int count = 0;

  private PageTurner(Handler handler, ViewPager pager) {
    this.handler = handler;
    this.pager = pager;
  }

  @Override
  public void run() {
    if (pager.isFakeDragging()) {
      if (count < 20) {
        count++;
        pager.fakeDragBy(-count * count);
        handler.postDelayed(this, 20);
      } else {
        pager.endFakeDrag();
      }
    }
  }
}

(The count * count is just there to make the drag speed up as it goes)

Coadjutant answered 19/6, 2014 at 6:7 Comment(0)
D
4

I have used

DecelerateInterpolator()

Here is the example:

  mViewPager = (ViewPager) findViewById(R.id.container);
            mViewPager.setAdapter(mSectionsPagerAdapter);
            Field mScroller = null;
            try {
                mScroller = ViewPager.class.getDeclaredField("mScroller");
                mScroller.setAccessible(true);
                Scroller scroller = new Scroller(this, new DecelerateInterpolator());
                mScroller.set(mViewPager, scroller);
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
Dichroscope answered 22/7, 2016 at 8:19 Comment(0)
W
3

Based on accepted solution I have created kotlin class with extension for view pager. Enjoy! :)

class ViewPageScroller : Scroller {

    var fixedDuration = 1500 //time to scroll in milliseconds

    constructor(context: Context) : super(context)

    constructor(context: Context, interpolator: Interpolator) : super(context, interpolator)

    constructor(context: Context, interpolator: Interpolator, flywheel: Boolean) : super(context, interpolator, flywheel)


    override fun startScroll(startX: Int, startY: Int, dx: Int, dy: Int, duration: Int) {
        // Ignore received duration, use fixed one instead
        super.startScroll(startX, startY, dx, dy, fixedDuration)
    }

    override fun startScroll(startX: Int, startY: Int, dx: Int, dy: Int) {
        // Ignore received duration, use fixed one instead
        super.startScroll(startX, startY, dx, dy, fixedDuration)
    }
}

fun ViewPager.setViewPageScroller(viewPageScroller: ViewPageScroller) {
    try {
        val mScroller: Field = ViewPager::class.java.getDeclaredField("mScroller")
        mScroller.isAccessible = true
        mScroller.set(this, viewPageScroller)
    } catch (e: NoSuchFieldException) {
    } catch (e: IllegalArgumentException) {
    } catch (e: IllegalAccessException) {
    }

}
Whitewash answered 18/12, 2018 at 9:57 Comment(0)
Z
1

Here's an answer using an entirely different approach. Someone might say this has a hacky feel; however, it doesn't use reflection, and I would argue that it will always work.

We find the following code inside ViewPager.smoothScrollTo:

    if (velocity > 0) {
        duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
    } else {
        final float pageWidth = width * mAdapter.getPageWidth(mCurItem);
        final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin);
        duration = (int) ((pageDelta + 1) * 100);
    }
    duration = Math.min(duration, MAX_SETTLE_DURATION);

It calculates the duration based on a few things. See anything we can control? mAdapter.getPageWidth. Let's implement ViewPager.OnPageChangeListener in our adapter. We're going to detect when the ViewPager is scrolling, and give a fake value for width. If I want the duration to be k*100, then I will return 1.0/(k-1) for getPageWidth. The following is Kotlin impl of this part of the adapter which turns the duration into 400:

    var scrolling = false
    override fun getPageWidth(position: Int): Float {
        return if (scrolling) 0.333f else 1f
    }

    // OnPageChangeListener to detect scroll state
    override fun onPageScrollStateChanged(state: Int) {
        scrolling = state == ViewPager.SCROLL_STATE_SETTLING
    }

    override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
    }

    override fun onPageSelected(position: Int) {
    }

Don't forget to add the adapter as an OnPageChangedListener.

My particular case can safely assume that the user can't swipe to drag between pages. If you have to support settling after a drag, then you need to do a bit more in your calculation.

One downside is that this depends on that hardcoded 100 base duration value in ViewPager. If that changes, then your durations change with this approach.

Zeller answered 18/5, 2017 at 1:45 Comment(0)
D
0
                binding.vpTour.beginFakeDrag();
                lastFakeDrag = 0;
                ValueAnimator va = ValueAnimator.ofInt(0, binding.vpTour.getWidth());
                va.setDuration(1000);
                va.addUpdateListener(animation -> {
                    if (binding.vpTour.isFakeDragging()) {
                        int animProgress = (Integer) animation.getAnimatedValue();
                        binding.vpTour.fakeDragBy(lastFakeDrag - animProgress);
                        lastFakeDrag = animProgress;
                    }
                });
                va.addListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        super.onAnimationEnd(animation);
                        if (binding.vpTour.isFakeDragging()) {
                            binding.vpTour.endFakeDrag();
                        }
                    }
                });
                va.start();

I wanted to solve the same issue as all of you and this is my solution for the same problem, but I think this way the solution is more flexible as you can change the duration however you like and also change the interpolation of the values as desired to achieve different visual effects. My solution swipes from page "1" to page "2" , so only in increasing positions, but can easily be changed to go in decreasing positions by doing "animProgress - lastFakeDrag" instead of "lastFakeDrag - animProgress". I think this is the most flexible solution for performing this task.

Decompose answered 6/10, 2017 at 11:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.