ViewPager禁止滑动和修改滑动速度
1. 简介
实际开发中,我们有时候需要禁止 ViewPager 滑动,和改变 ViewPager 切换页面时的滑动速率。下面总结了 禁止ViewPager滑动和通过Viewpager 的 scroller 修改滑动速度的实现。非常简单。
2. ViewPager 禁止滑动
禁止 ViewPager 滑动,最简单的方式就是拦截 ViewPager 触摸滑动事件。如下代码所示:
import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
public class NoScrollViewPager extends ViewPager {
private boolean scrollable = true;
public NoScrollViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
public NoScrollViewPager(Context context) {
super(context);
}
public void setScrollable(boolean scrollable) {
this.scrollable = scrollable;
}
public boolean isScrollable() {
return scrollable;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (scrollable) {
return super.onTouchEvent(ev);
} else {
return false;
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (scrollable) {
return super.onInterceptTouchEvent(ev);
} else {
return false;
}
}
}
使用时,调用 setScrollable(false) 就可以禁止 ViewPager 滑动。
3. ViewPager 修改滑动速度
我们都知道,ViewPager 切换页面有两种方式:一是,手指触摸滑动切换页面;二是,调用方法 setCurrentItem(int) 切换页面。用手指触摸滑动切换页面时,页面的滑动与手指滑动速度有关,过渡动画很明显,体验很好。调用方法切换页面时,由于页面切换速度较快,过渡动画不明显甚至没有,往往是直接闪过去。当我们需要 ViewPager 自动切换页面,例如自动轮播时,ViewPager 原有的切换速度有时候难以适应我们的需求,尤其在我们定制了 ViewPager 的切换效果时,就更加难以适应我们对交互体验的需求了。这时我们就需要修改这种情况下页面滑动速度了。如何修改页面切换速度呢?咱们深入了解一下 ViewPager。
首先,我们看一下 setCurrentItem(int) 方法的源码:
public void setCurrentItem(int item) {
mPopulatePending = false;
setCurrentItemInternal(item, !mFirstLayout, false);
}
调用了 setCurrentItemInternal(int, boolean, boolean) 方法,再看 setCurrentItemInternal(int, boolean, boolean) 方法;
void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) {
setCurrentItemInternal(item, smoothScroll, always, 0);
}
继续往下看:
void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
if (mAdapter == null || mAdapter.getCount() <= 0) {
setScrollingCacheEnabled(false);
return;
}
if (!always && mCurItem == item && mItems.size() != 0) {
setScrollingCacheEnabled(false);
return;
}
if (item < 0) {
item = 0;
} else if (item >= mAdapter.getCount()) {
item = mAdapter.getCount() - 1;
}
final int pageLimit = mOffscreenPageLimit;
if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) {
// We are doing a jump by more than one page. To avoid
// glitches, we want to keep all current pages in the view
// until the scroll ends.
for (int i = 0; i < mItems.size(); i++) {
mItems.get(i).scrolling = true;
}
}
final boolean dispatchSelected = mCurItem != item;
if (mFirstLayout) {
// We don't have any idea how big we are yet and shouldn't have any pages either.
// Just set things up and let the pending layout handle things.
mCurItem = item;
if (dispatchSelected) {
dispatchOnPageSelected(item);
}
requestLayout();
} else {
populate(item);
scrollToItem(item, smoothScroll, velocity, dispatchSelected);
}
}
整段代码和滑动相关的大概只有这一行:
scrollToItem(item, smoothScroll, velocity, dispatchSelected);
我们看一下此方法:
private void scrollToItem(int item, boolean smoothScroll, int velocity,
boolean dispatchSelected) {
final ItemInfo curInfo = infoForPosition(item);
int destX = 0;
if (curInfo != null) {
final int width = getClientWidth();
destX = (int) (width * Math.max(mFirstOffset,
Math.min(curInfo.offset, mLastOffset)));
}
if (smoothScroll) {
smoothScrollTo(destX, 0, velocity);
if (dispatchSelected) {
dispatchOnPageSelected(item);
}
} else {
if (dispatchSelected) {
dispatchOnPageSelected(item);
}
completeScroll(false);
scrollTo(destX, 0);
pageScrolled(destX);
}
}
在此方法中,velocity 表示速度,与速度有关的是 smoothScrollTo(destX, 0, velocity),我们只需关注 smoothScrollTo() 方法。
void smoothScrollTo(int x, int y, int velocity) {
if (getChildCount() == 0) {
// Nothing to do.
setScrollingCacheEnabled(false);
return;
}
int sx;
boolean wasScrolling = (mScroller != null) && !mScroller.isFinished();
if (wasScrolling) {
// We're in the middle of a previously initiated scrolling. Check to see
// whether that scrolling has actually started (if we always call getStartX
// we can get a stale value from the scroller if it hadn't yet had its first
// computeScrollOffset call) to decide what is the current scrolling position.
sx = mIsScrollStarted ? mScroller.getCurrX() : mScroller.getStartX();
// And abort the current scrolling.
mScroller.abortAnimation();
setScrollingCacheEnabled(false);
} else {
sx = getScrollX();
}
int sy = getScrollY();
int dx = x - sx;
int dy = y - sy;
if (dx == 0 && dy == 0) {
completeScroll(false);
populate();
setScrollState(SCROLL_STATE_IDLE);
return;
}
setScrollingCacheEnabled(true);
setScrollState(SCROLL_STATE_SETTLING);
final int width = getClientWidth();
final int halfWidth = width / 2;
final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width);
final float distance = halfWidth + halfWidth
* distanceInfluenceForSnapDuration(distanceRatio);
int duration;
velocity = Math.abs(velocity);
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);
// Reset the "scroll started" flag. It will be flipped to true in all places
// where we call computeScrollOffset().
mIsScrollStarted = false;
mScroller.startScroll(sx, sy, dx, dy, duration);
ViewCompat.postInvalidateOnAnimation(this);
}
这个方法就是关键了!我们知道 velocity 代表速度,找到 velocity,发现 velocity > 0 时,通过 velocity 和 滑动距离(这个应该是手指滑动的)计算出了一个 duration,与设定的最大时间间隔 MAX_SETTLE_DURATION 比较,取较小的,然后再参与滚动。我们向上找 velocity 的传值,发现在 void
setCurrentItemInternal(int item, boolean smoothScroll, boolean always) 方法中我们传了 0,duration 会根据 将要滚动的距离 和 页面占用的宽度 计算出滚动的页数,duration 为 100*(滚动页数 + 1) 毫秒,最后还是会取 duration 和 MAX_SETTLE_DURATION 的较小值。最终,duration 最大为 MAX_SETTLE_DURATION ,MAX_SETTLE_DURATION 的值是 600。而 duration 最终传入了 mScroller.startScroll(sx, sy, dx, dy, duration) 。两个页面切换时,滚动距离是固定的,duration 越小,速度越快;duration 越大,速度越慢。速度越慢,我越是有时间展示我们的页面切换动画,交互体验就越好。
那么,如何改变这个duration 呢?我们还要看 mScroller。发现,mScroller 是 ViewPager 的私有属性,而且没有提供对外的方法去修改这个 mScroller。没有办法,我们只能借助于强大的反射机制,简单粗暴但实用。
既然要改变 mScroller ,那么就需要为 mScroller 重新赋值,我们先继承 Scroller 重写 startScroll() 方法,使用自己的 duration,弃用上面代码传入的 duration 如下代码所示:
import android.content.Context;
import android.view.animation.Interpolator;
import android.widget.Scroller;
public class FixedSpeedScroller extends Scroller {
private static final int DURATION_DEF = 1500;
private int mDuration = DURATION_DEF;
public FixedSpeedScroller(Context context) {
this(context, DURATION_DEF);
}
public FixedSpeedScroller(Context context, int duration) {
this(context, null, duration);
}
public FixedSpeedScroller(Context context, Interpolator interpolator) {
this(context, interpolator, DURATION_DEF);
}
public FixedSpeedScroller(Context context, Interpolator interpolator, int duration) {
super(context, interpolator);
this.mDuration = duration;
}
@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
this.startScroll(startX, startY, dx, dy, mDuration);
}
public void setDuration(int duration) {
mDuration = duration;
}
}
这样的话,我们就可以自己自定义滑动的时间,控制滑动的速度。下一步就是如何修改 mScroller值了,上面提到,我们只能使用反射,那我们就用反射为 mScroller 重新赋上我们想要的值,如下代码:
try {
Field field = ViewPager.class.getDeclaredField("mScroller");
field.setAccessible(true);
FixedSpeedScroller scroller = new FixedSpeedScroller(viewPager.getContext(), new AccelerateInterpolator());
field.set(viewPager, scroller);
scroller.setDuration(2000);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
4. 参考
·1> android之ViewPager修改滑动速度:
https://www.cnblogs.com/cmai/p/7705190.html