Slowing speed of Viewpager controller in android

Android

Android Problem Overview


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?

Android Solutions


Solution 1 - Android

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.

Hope this helps.

Solution 2 - Android

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!

Solution 3 - Android

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();
}

Solution 4 - Android

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 

Solution 5 - Android

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);
	}
}

Solution 6 - Android

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)

Solution 7 - Android

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();
            }

Solution 8 - Android

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) {
    }

}

Solution 9 - Android

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.

Solution 10 - Android

                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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAlex KellyView Question on Stackoverflow
Solution 1 - AndroidmarmorView Answer on Stackoverflow
Solution 2 - AndroidOleg VaskevichView Answer on Stackoverflow
Solution 3 - AndroidlobzikView Answer on Stackoverflow
Solution 4 - AndroidHighFlyerView Answer on Stackoverflow
Solution 5 - AndroidpawelziebaView Answer on Stackoverflow
Solution 6 - Androiddf778899View Answer on Stackoverflow
Solution 7 - AndroidMladen RakonjacView Answer on Stackoverflow
Solution 8 - AndroidJanusz HainView Answer on Stackoverflow
Solution 9 - AndroidandroidguyView Answer on Stackoverflow
Solution 10 - AndroidRubinView Answer on Stackoverflow