当通过编程滑动时改变ViewPager动画持续时间

76

我正在使用以下代码更改幻灯片:

viewPager.setCurrentItem(index++, true);

但是它速度太快了。有没有办法手动设置动画速度?


不确定你的搜索内容如何,但是看一下这里SO上的 这个答案。那应该正是你想要的。 - MH.
实际上,这与我的解决方案非常相似,尽管我使用了一个因子而不是绝对持续时间(因为在ViewPager中,持续时间可能取决于您滚动的页面数量)。 - Oleg Vaskevich
7个回答

115

我一直想自己解决这个问题,现在已经通过反射实现了。虽然我还没有测试过,但是应该能够工作或者只需要进行少量修改。

在 Galaxy Nexus JB 4.2.1 上进行了测试。你需要在你的 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 {
            Field scroller = ViewPager.class.getDeclaredField("mScroller");
            scroller.setAccessible(true);
            Field interpolator = ViewPager.class.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));
    }
}

2
你的解决方案是我目前找到的最好的,但是当执行“scroller.set(this, mScroller);”这一行时,我得到了“IllegalArgumentException: invalid value for field”的错误。我猜这是因为使用了ViewPager子类...有什么解决方法吗? - elgui
1
当启用Proguard时,在setScrollDurationFactor上出现空指针错误,Android。 - aj0822ArpitJoshi
为什么ViewPagerCustomDurationcatch语句中没有任何内容?如果抛出异常,mScroller将变为空值,然后调用setScrollDurationFactor会抛出NullPointerException - Hossein Shahdoost
@OlegVaskevich,这确实可能发生,在我这种情况下(因为我忘记了代码中的反射),在混淆之后类名被更改,mScroller也不再被创建。这花费了我数小时才找出原因,部分原因是因为错误出现在代码的其他部分。 - Hossein Shahdoost
完美的答案!!谢谢你的分享。 - Archa
显示剩余10条评论

42

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

private void animatePagerTransition(final boolean forward) {

    ValueAnimator animator = ValueAnimator.ofInt(0, viewPager.getWidth());
    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);
    if (viewPager.beginFakeDrag()) {
        animator.start();
    }
}

更新:

刚刚检查了一下,这个解决方案是否可以用于一次滑动多个页面(例如,在最后一页后应该显示第一页)。这是稍微修改过的代码来处理指定的页面数量:

private int oldDragPosition = 0;

private void animatePagerTransition(final boolean forward, int pageCount) {
    // if previous animation have not finished we can get exception
    if (pagerAnimation != null) {
        pagerAnimation.cancel();
    }
    pagerAnimation = getPagerTransitionAnimation(forward, pageCount);
    if (viewPager.beginFakeDrag()) {    // checking that started drag correctly
        pagerAnimation.start();
    }
}

private Animator getPagerTransitionAnimation(final boolean forward, int pageCount) {
    ValueAnimator animator = ValueAnimator.ofInt(0, viewPager.getWidth() - 1);
    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) {
            viewPager.endFakeDrag();
            oldDragPosition = 0;
            viewPager.beginFakeDrag();
        }
    });

    animator.setInterpolator(new AccelerateInterpolator());
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

        @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 / pageCount); // remove divider if you want to make each transition have the same speed as single page transition
    animator.setRepeatCount(pageCount);

    return animator;
}

所以它是假的拖动吗?这似乎是一种聪明的方法,但如果用户在此操作期间开始拖动会发生什么?另外,如果我想要它滚动到第一页(因为当它到达最后一页时,我希望能够使它再次到达第一页),该怎么办? - android developer
@androiddeveloper,我已经使用了NonSwipeableViewPager(https://dev59.com/TWkw5IYBdhLWcg3wyNgV#9650884)的解决方案。所以在模拟拖动期间,我没有用户交互的问题。但是,如果有人需要保持滑动手势,则可以通过覆盖ViewPager并在用户触摸ViewPager时控制动画来实现。我没有尝试过,但我认为这是可能的,并且应该可以正常工作。对于循环页面,您应该调整动画代码,但调整取决于循环的具体实现。 - lobzik
关于用户交互,好的(因为动画不是很慢,我不介意)。关于滚动到第一页,我不明白。假设只有3页,而我在最后一页,如何使用您设置的自定义持续时间将其滚动到第一页?使用动画师的重复属性是否有帮助? - android developer
我不明白。您能否修改代码以展示如何实现这一点?也许可以用一个整数代替“前进”,表示要前进多少页(如果是负数,则向后翻页)? - android developer
让我们在聊天中继续这个讨论 - lobzik
显示剩余8条评论

14
 public class PresentationViewPager extends ViewPager {

    public static final int DEFAULT_SCROLL_DURATION = 250;
    public static final int PRESENTATION_MODE_SCROLL_DURATION = 1000;

    public PresentationViewPager (Context context) {
        super(context);
    }

    public PresentationViewPager (Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public void setDurationScroll(int millis) {
        try {
            Class<?> viewpager = ViewPager.class;
            Field scroller = viewpager.getDeclaredField("mScroller");
            scroller.setAccessible(true);
            scroller.set(this, new OwnScroller(getContext(), millis));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public class OwnScroller extends Scroller {

        private int durationScrollMillis = 1;

        public OwnScroller(Context context, int durationScroll) {
            super(context, new DecelerateInterpolator());
            this.durationScrollMillis = durationScroll;
        }

        @Override
        public void startScroll(int startX, int startY, int dx, int dy, int duration) {
            super.startScroll(startX, startY, dx, dy, durationScrollMillis);
        }
    }
}

6
更好的解决方案是通过在支持包中创建类来访问私有字段。编辑 这受到ViewPager类设置的MAX_SETTLE_DURATION 600毫秒的限制。
package android.support.v4.view;

import android.content.Context;
import android.util.AttributeSet;

public class SlowViewPager extends ViewPager {

    // The speed of the scroll used by setCurrentItem()
    private static final int VELOCITY = 200;

    public SlowViewPager(Context context) {
        super(context);
    }

    public SlowViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) {
        setCurrentItemInternal(item, smoothScroll, always, VELOCITY);
    }

}

当然,你可以添加一个自定义属性,以便可以通过XML进行设置。

谢谢,这似乎有效,但即使将VELOCITY设置为1,滚动速度仍然相当快,我希望能进一步减慢它。 - Oliver Pearmain
@HaggleLad 是的,不幸的是,我的方法受到最大动画持续时间 600 毫秒的限制。如果您需要更长的时间,您需要使用其他方法。我会更新我的答案来说明这一点。 - Paul Burke
8
setCurrentItemInternal方法不是私有方法吗? - Marty Miller
2
@MartyMiller 是的,但如果支持库是您项目的一部分,您可以在其包中创建一个类。 - Paul Burke
1
编辑支持库不是一种有效的方式,如果我决定升级它,这段代码将无法工作。而且在我的个人情况下,我没有支持库的源代码(即使有,我也不会编辑它)。 - Ahmed Adel Ismail
显示剩余4条评论

0

我使用Cicero Moura版本制作了一个Kotlin类,在Android 10上仍然完美运行。

import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.animation.DecelerateInterpolator
import android.widget.Scroller
import androidx.viewpager.widget.ViewPager

class CustomViewPager(context: Context, attrs: AttributeSet) :
        ViewPager(context, attrs) {


    private companion object {
        const val DEFAULT_SPEED = 1000
    }

    init {
        setScrollerSpeed(DEFAULT_SPEED)
    }

    var scrollDuration = DEFAULT_SPEED
        set(millis) {
            setScrollerSpeed(millis)
        }

    private fun setScrollerSpeed(millis: Int) {
        try {
            ViewPager::class.java.getDeclaredField("mScroller")
                    .apply {
                        isAccessible = true
                        set(this@CustomViewPager, OwnScroller(millis))
                    }

        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    inner class OwnScroller(private val durationScrollMillis: Int) : Scroller(context, AccelerateDecelerateInterpolator()) {
        override fun startScroll(startX: Int, startY: Int, dx: Int, dy: Int, duration: Int) {
            super.startScroll(startX, startY, dx, dy, durationScrollMillis)
        }
    }
}

从活动类初始化:

viewPager.apply {
    scrollDuration = 2000
    adapter = pagerAdapter
}

0

这是我在Librera Reader中使用的代码。

public class MyViewPager extends ViewPager {

  public MyViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
        initMyScroller();
    }

    private void initMyScroller() {
        try {
            Class<?> viewpager = ViewPager.class;
            Field scroller = viewpager.getDeclaredField("mScroller");
            scroller.setAccessible(true);
            scroller.set(this, new MyScroller(getContext())); // my liner scroller

            Field mFlingDistance = viewpager.getDeclaredField("mFlingDistance");
            mFlingDistance.setAccessible(true);
            mFlingDistance.set(this, Dips.DP_10);//10 dip

            Field mMinimumVelocity = viewpager.getDeclaredField("mMinimumVelocity");
            mMinimumVelocity.setAccessible(true);
            mMinimumVelocity.set(this, 0); //0 velocity

        } catch (Exception e) {
            LOG.e(e);
        }

    }

    public class MyScroller extends Scroller {
        public MyScroller(Context context) {
            super(context, new LinearInterpolator()); // my LinearInterpolator
        }

        @Override
        public void startScroll(int startX, int startY, int dx, int dy, int duration) {
            super.startScroll(startX, startY, dx, dy, 175);//175 duration
        }
    }

 }

-1
在浪费了整个一天之后,我找到了一个解决方案,将 offscreenPageLimit 设置为页面的总数。
为了保持 ViewPager 滚动的平稳,setOffScreenLimit(page.length) 将会保留所有视图在内存中。然而,这对于任何涉及调用 View.requestLayout 函数的动画(例如任何涉及对边距或边界进行更改的动画)都会带来问题。根据 Romain Guy 的说法,它会使它们变得非常缓慢,因为所有在内存中的视图都将被无效化。因此,我尝试了几种不同的方法来使事情变得平滑,但是覆盖 requestLayout 和其他 invalidate 方法会引起许多其他问题。
一个很好的折衷方案是动态修改离屏限制,以便大多数页面之间的滚动都非常平滑,同时确保所有页面内的动画都很平滑,通过在用户移除视图时删除视图。当您只有 1 或 2 个视图需要使其他视图脱离内存时,这种方法非常有效。
*** 当没有任何解决方案可行时,请使用此方法,因为通过设置偏移量限制,您将同时加载所有片段。

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