Android中Viewpager控制器的速度降低


104

有什么办法可以降低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();
} 

它不会改变任何东西,但是没有异常发生吗?



1
child.setNestedScrollingEnabled(false);工作对我来说
皮埃尔

Answers:


230

我从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吗?
Shashank Degloorkar

10
“插值器”是动画运行的速率,例如,动画可以加速,减速等等。有一个没有插值器的构造函数,我认为这应该起作用,因此只需尝试从调用中删除该参数即可。如果不起作用,加: '插补sInterpolator =新AccelerateInterpolator()'
MARMOR

8
我发现DecelerateInterpolator在这种情况下可以更好地工作,它可以保持初始划动的速度,然后缓慢减速。
Quint Stoffers,2012年

嗨,您知道为什么scroller.setFixedDuration(5000);会出错吗?它说“方法setFixedDuration(int)未定义”
jaibatrik

6
如果您使用的是Proguard,请不要忘记添加以下行:-keep class android.support.v4。** {*;}否则,当getDeclaredField()
GuilhE 2014年

61

我想做自己并已找到解决方案(但是使用反射)。它与公认的解决方案相似,但是使用相同的插值器,并且仅根据一个因素更改持续时间。您需要ViewPagerCustomDuration在XML中使用而不是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的静态内部类会使它更加封装。
伊曼纽尔·莫克林

1
如果您使用的是Proguard,请不要忘记添加以下行:-keep class android.support.v4。** {*;}否则,当getDeclaredField()
GuilhE 2014年

我建议更新为三元,因为您不想让任何人通过0因子,这会缩短您的时间。更新要传递给mScrollFactor> 0的最终参数?(INT)(持续时间* mScrollFactor):持续时间
portfoliobuilder

我还建议您在类ViewPagerCustomDuration中设置滚动因子时进行空检查。您将mScroller实例化为空对象,尝试在该对象上设置滚动因子时应确认它仍然不为空。
portfoliobuilder

@portfoliobuilder有效的注释,尽管您似乎更喜欢防御性的编程风格,即IMO比不带SO的代码片段更适合不透明的库。将滚动因子设置为<= 0以及在非实例化视图上调用setScrollDurationFactor是大多数Android开发人员应避免进行的运行时检查。
Oleg Vaskevich '16

43

基于@ 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
我认为这被低估了。它利用startFakeDrag和的endFakeDrag作用是要实现比现成可用的功能更多ViewPager的功能。这对我来说非常
有用,

1
很好,谢谢。不过,这是一个小的增强功能:为了使其能够用于带有填充的viewPager,第一行应该是... ValueAnimator.ofInt(0,viewPager.getWidth()-(forward?viewPager.getPaddingLeft():viewPager.getPaddingRight() ));
德米特里(DmitryO)。

1
到目前为止,这是最简单/有效/可靠的解决方案,因为它忽略了反射并避免了像上面的解决方案那样的密封违规
Ryan

2
@lobzic,您到底在哪里调用您的animatePagerTransition(final boolean forward方法?那就是我的困惑。您是否在一种ViewPager.OnPageChangeListener接口方法中调用它?或者别的地方?
Sakiboy

没有难看的反思黑客。易于配置。一个很好的答案。
奥伦

18

如您在ViewPager源中所看到的,刷新持续时间由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和setAdapter(mAdapter)...我应该创建一个扩展ViewPager的类吗?
亚历克斯·凯利

12
您应该这样写:setContentView(R.layout.main); mPager =(ViewPager)findViewById(R.id.view_pager); 字段mScroller = ViewPager.class.getDeclaredField(“ mScroller”); mScroller.setAccessible(true); mScroller.set(mPager,scroller); //
自行

我刚刚发布了一个答案...它不起作用...您是否有时间给我指导,为什么不呢?
Alex Kelly

在更仔细地查看源代码之后:找到mScroller.startScroll(sx,sy,dx,dy); 在ViewPager源代码中,并查看Scroller实现。startScroll使用DEFAULT_DURATION参数调用其他startScroll。无法为静态最终字段再取一个值,所以还剩下一个机会-覆盖ViewPager的smoothScrollTo
HighFlyer 2011年

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 2014年

3
正是@ElhananMishraky,这就是我的意思Notice the package where the class is
pawelzieba 2014年

3
等待,等待,等待,等待... wuuuut?您可以使用sdk的package-local fields/ methods,如果只是让Android Studio认为您使用的是同一软件包?我是唯一闻些气味的人sealing violation吗?
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();
            }

2

基于公认的解决方案,我创建了带有视图寻呼机扩展名的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何时滚动,并为width提供一个假值。如果我想时间是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。

我的特殊情况可以安全地假设用户无法在页面之间滑动来拖动。如果必须在拖动后支持沉降,则需要在计算中做更多的事情。

缺点之一是,这取决于100ViewPager 中的硬编码基本持续时间值。如果那改变了,那么您的持续时间就会随着这种方法而改变。


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”轻松更改为减少位置。我认为这是执行此任务的最灵活的解决方案。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.