Reputation: 1428
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?
Upvotes: 31
Views: 17192
Reputation: 133
For those who are using ViewPager2..
Make CustomPagerSnapHelper by extending PagerSnapHelper and override the createSnapScroller function
Below is the code for the same. note: In my case I just multiplied time 2x which result in slower animation. change the time variable according to your need. I recommend just multiple to further slower the animation instead of hardcoded value
public class CustomPagerSnapHelper extends PagerSnapHelper {
RecyclerView mRecyclerView;
static final float MILLISECONDS_PER_INCH = 100f;
private static final int MAX_SCROLL_ON_FLING_DURATION = 100; // ms
CustomPagerSnapHelper(RecyclerView mRecyclerView) {
this.mRecyclerView = mRecyclerView;
}
@Override
protected LinearSmoothScroller createSnapScroller(RecyclerView.LayoutManager layoutManager) {
if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
return null;
}
return new LinearSmoothScroller(mRecyclerView.getContext()) {
@Override
protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(),
targetView);
final int dx = snapDistances[0];
final int dy = snapDistances[1];
final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
if (time > 0) {
action.update(dx, dy, time*2/* time represent the duration of scroll animation */, mDecelerateInterpolator);
}
}
@Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
@Override
protected int calculateTimeForScrolling(int dx) {
return Math.min(MAX_SCROLL_ON_FLING_DURATION, super.calculateTimeForScrolling(dx));
}
};
}
}
use reflection to replace PagerSnapHelper with our CustomPagerSnapHelper object. our object will to injected here >>>>>> ViewPager.mRecylerView.mPagerSnapHelper >> then attach recyclerview to our CustomPagerSnapHelper
viewPager = findViewById(R.id.viewPagerMain);
try {
Field mScroller = ViewPager2.class.getDeclaredField("mRecyclerView");
mScroller.setAccessible(true);
RecyclerView recyclerView = (RecyclerView) mScroller.get(viewPager);
Field pagerSnap = ViewPager2.class.getDeclaredField("mPagerSnapHelper");
pagerSnap.setAccessible(true);
CustomPagerSnapHelper customPagerSnapHelper = new CustomPagerSnapHelper(recyclerView);
pagerSnap.set(viewPager,customPagerSnapHelper);
recyclerView.setOnFlingListener(null);
customPagerSnapHelper.attachToRecyclerView(recyclerView);
} catch (Exception e) {
e.printStackTrace();
}
viewPager.setAdapter(new RecyclerViewAdapter(this, images));
Upvotes: 1
Reputation: 146
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()
}
Upvotes: 3
Reputation: 61
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()
}
Upvotes: 2
Reputation: 15714
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()
onPageScrollStateChanged
from ViewPager2.OnPageChangeCallback
, where you can distinguish between programmatical drag and user drag thanks to isFakeDragging()
methodUpvotes: 40
Reputation: 929
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.
Upvotes: 6