Reputation: 1603
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?
Upvotes: 110
Views: 90141
Reputation: 28179
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.
Upvotes: 238
Reputation: 617
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) {
}
}
Upvotes: 3
Reputation: 212
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.
Upvotes: 0
Reputation: 3177
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.
Upvotes: 1
Reputation: 9812
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();
}
Upvotes: 4
Reputation: 11064
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();
}
Upvotes: 46
Reputation: 1655
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
Upvotes: 18
Reputation: 10931
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)
Upvotes: 8
Reputation: 16082
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);
}
}
Upvotes: 16
Reputation: 12672
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!
Upvotes: 62