Android中Viewpager控制器速度减慢

109

有没有办法在 Android 中使用 Viewpager 适配器来减慢滚动速度?


你知道吗,我一直在看这段代码。我想不出我做错了什么。

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

它还没有改变任何东西,但也没有出现异常吗?


尝试一下这个:https://antoniocappiello.com/2015/10/31/spicy-up-your-viewpager-part-1-tune-the-scroll-duration/ - Rushi Ayyappa
1
child.setNestedScrollingEnabled(false); 对我起了作用。 - Pierre
10个回答

236

我从HighFlyer的代码开始,确实改变了mScroller字段(这是一个很好的开始),但是并没有帮助延长滚动的持续时间,因为ViewPager在请求滚动时会明确地将持续时间传递给mScroller。

扩展ViewPager并不起作用,因为重要的方法(smoothScrollTo)无法被覆盖。

最终,我通过扩展Scroller修复了这个问题,使用以下代码:

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

并且像这样使用它:

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

我已经将持续时间硬编码为5秒,并让我的ViewPager使用它。


16
您好,sInterpolator是什么意思? sInterpolator是一个插值器,用于在Android开发中实现动画效果。 - Shashank Degloorkar
10
“插值器”是指动画播放的速率,例如它们可以加速、减速等等。有一个没有插值器的构造函数,我认为那应该可以工作,所以只需尝试从调用中删除该参数。如果这不起作用,请添加:“Interpolator sInterpolator = new AccelerateInterpolator()”。 - marmor
9
我发现在这种情况下使用DecelerateInterpolator效果更好,它可以保持初始滑动的速度并逐渐减缓。 - Quint Stoffers
它没有平稳地停止,该怎么办? - mohitum
6
如果您正在使用Proguard,请不要忘记添加以下这行代码:-keep class android.support.v4.** {*;} 否则在调用getDeclaredField()时可能会出现空指针异常。请注意保持翻译后的语句与原文意思一致,尽量保持简洁易懂。 - GuilhE
显示剩余8条评论

61

我想要自己实现并且已经找到了一个解决方案(使用反射)。它类似于已被接受的解决方案,但是只是基于因素修改了持续时间,却使用相同的插值器。在你的XML中需要使用 ViewPagerCustomDuration 而不是 ViewPager,然后你可以这样做:

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

}

希望这能帮助到某些人!


3
我喜欢这个解决方案胜过采纳的答案,因为这个自定义ViewPager是一个可以直接使用且对现有代码进行最少更改的类。将自定义滚动条作为自定义ViewPager的静态内部类,使其更加封装化。 - Emanuel Moecklin
1
如果您正在使用Proguard,请不要忘记添加以下行:-keep class android.support.v4.** {*;},否则在使用getDeclaredField()时会得到空指针异常。 - GuilhE
我建议更新为三元运算符,因为您不希望允许任何人传入0因子,这将破坏您的时间持续性。请更新您传递给mScrollFactor的最终参数,使其变为mScrollFactor> 0?(int)(duration * mScrollFactor):duration。 - portfoliobuilder
我还建议在设置您的类 ViewPagerCustomDuration 中的滚动系数时进行 null 检查。您使用 mScroller 作为 null 对象进行实例化,因此在尝试在该对象上设置滚动系数时,应确认它仍然不为 null。 - portfoliobuilder
@portfoliobuilder 你的评论很有道理,尽管我认为你更偏向于一种更为保守的编程风格,这种风格适用于不透明的库,而不是 SO 上的代码片段。将滚动因子设置为 <=0,以及在未实例化视图上调用 setScrollDurationFactor 是大多数 Android 开发人员应该能够避免在运行时进行检查的事情。 - Oleg Vaskevich
在启用Proguard时,Android上setScrollDurationFactor出现空指针问题。 - aj0822ArpitJoshi

46

我找到了更好的解决方案,基于@df778899的答案Android ValueAnimator API。 它可以在不使用反射的情况下正常工作,并且非常灵活。也不需要创建自定义ViewPager并将其放入android.support.v4.view包中。以下是一个示例:

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

7
我认为这个东西被低估了。它使用了 startFakeDragendFakeDrag,它们的用途是在 ViewPager 初始状态下提供更多功能。对我来说效果很好,正是我想要的。 - user3280133
1
这是一个很好的解决方案,谢谢。不过有一个小改进:为了使它适用于带填充的viewPager,第一行应该是... ValueAnimator.ofInt(0, viewPager.getWidth() - ( forward ? viewPager.getPaddingLeft() : viewPager.getPaddingRight() )); - DmitryO.
1
这是迄今为止最简单/高效/可靠的解决方案,因为它忽略了反射并避免了像上面的解决方案中的密封违规问题。 - Ryhan
3
@lobzic,你的 animatePagerTransition(final boolean forward) 方法是在哪里调用的?我有些困惑。你是在 ViewPager.OnPageChangeListener 接口方法中的某个位置调用它吗?还是在其他地方? - Sakiboy
没有丑陋的反射黑客。易于配置。非常好的答案。 - Oren
1
对于使用此代码的任何人,我建议您非常小心地处理动画师的距离,并考虑您实际想要滚动的宽度。如果您滚动太远,并且PAGER_TRANSITION_DURATION_MS的值相当快(即低),那么您可能会意外地将翻页器向前滑动两页而不是一页。 - avalancha

18

从ViewPager的源代码中可以看到,fling的持续时间由mScroller对象控制。 在文档中我们可以读到:

滚动的持续时间可以在构造函数中传递,并指定滚动动画应该花费的最长时间

因此,如果您想控制速度,可以通过反射更改mScroller对象。

您应该编写类似于以下内容的代码:

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 

我有些困惑如何操作 -我正在XML文件中设置ViewPager,并将其分配给mPager,然后设置适配器(mAdapter)...我应该创建一个扩展ViewPager的类吗? - Alex Kelly
13
您应该写出如下内容:setContentView(R.layout.main); mPager = (ViewPager)findViewById(R.id.view_pager);Field mScroller = ViewPager.class.getDeclaredField("mScroller"); mScroller.setAccessible(true); mScroller.set(mPager, scroller);//通过自己初始化滚动对象,设置可访问性为真的mScroller字段值。 - HighFlyer
我刚刚发布了另一个答案...但它不起作用...你有时间指导我为什么吗? - Alex Kelly
在仔细查看源代码后:在ViewPager源代码中找到mScroller.startScroll(sx, sy, dx, dy);并查看Scroller的实现。 startScroll会调用另一个带有DEFAULT_DURATION参数的startScroll。无法为静态final字段设置其他值,所以只剩下一种可能 - 重写ViewPager的smoothScrollTo方法。 - HighFlyer

16

这并不是完美的解决方案,因为它是一个 int,所以你无法使速度变慢。但对于我来说,速度已经足够缓慢了,而且我不必使用反射。

请注意类所在的包。 smoothScrollTo 具有包可见性。

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

请注意,第一行“package android.support.v4.view;”是必需的,您应该在src根目录下创建这样的包。如果您不这样做,您的代码将无法编译。 - Elhanan Mishraky
3
@ElhananMishraky,就是这个意思,要注意类所在的包。 - pawelzieba
3
等等,等等,等等......什么?如果你让Android Studio认为你正在使用相同的包,那么你就可以使用sdk的“package-local”“fields”/“methods”吗?我是唯一一个闻到一些“密封违规”的人吗? - Bartek Lipinski
这是一个非常好的解决方案,但对我来说也不起作用,因为ViewPager Scroller使用的Interpolator在这种情况下表现不佳。因此,我将您的解决方案与其他使用反射获取mScroller的解决方案相结合。这样,当用户进行分页时,我可以使用原始的Scroller,而当我使用setCurrentItem()程序更改页面时,则可以使用自定义的Scroller。 - rolgalan

8
ViewPager的fakeDrag方法似乎提供了一种替代解决方案。
例如,这将从项目0翻页到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();
      }
    }
  }
}

count * count 只是为了让拖动速度随着拖动加快)

4

我曾经使用过

DecelerateInterpolator()

以下是示例:

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

3

基于已接受的解决方案,我创建了一个Kotlin类,并为视图页面创建了扩展。享受吧!:)

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

}

1
这里提供一种完全不同的解决方案。有人可能认为这种方法有些巧妙,但它并没有使用反射技术,我认为它总是有效的。
我们在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);

它基于一些因素计算持续时间。看到我们能控制的东西了吗?mAdapter.getPageWidth。让我们在适配器中实现ViewPager.OnPageChangeListener。我们将检测ViewPager何时滚动,并为其提供虚假的宽度值。如果我想要持续时间为k*100,那么我将返回1.0/(k-1)作为getPageWidth。以下是该部分适配器的Kotlin实现,将持续时间转换为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) {
    }

不要忘记将适配器添加为OnPageChangedListener。
在我的情况下,可以安全地假定用户无法通过滑动在页面之间拖动。如果必须支持拖动后的稳定,则需要在计算中进行更多操作。
缺点之一是这取决于ViewPager中硬编码的100个基础持续时间值。如果发生更改,则使用此方法会更改您的持续时间。

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

我想解决与你们所有人相同的问题,这是我针对同样问题的解决方案,但我认为这种方式更加灵活,因为您可以根据需要更改持续时间并更改值的插值以实现不同的视觉效果。我的解决方案从页面“1”滑动到页面“2”,所以只增加位置,但可以通过执行“animProgress - lastFakeDrag”而不是“lastFakeDrag - animProgress”轻松地更改为减少位置。我认为这是执行此任务最灵活的解决方案。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接