转载自:http://blog.csdn.net/huachao1001/article/details/51654692
上一篇介绍了ViewPager
的onMeasure
和onLayout
两个方法,这是自定义View最基本的两个函数。但是我们的ViewPager
有个需求就是滑动,接下来我们一起去学习ViewPager
在滑动方面做了哪些工作,以及ViewPager
如何处理与子View
之间的滑动冲突。由于ViewPager的子View有Decor View还有普通的子View,而本篇文章讲的主要是普通子View,因此,不再去刻意区分,以下所说的子View不包括DecorView。
我们知道,Android
内置了Scroller
对象,用于实现渐近式的滑动。假设我们自定义一个函数smoothScrollTo(int destX,int destY)
,用于让ViewPager
渐近式的滑动到(destX,destY)
这个坐标位置,那么使用Scroller
实现步骤一般如下:
- 创建Scroller对象:
Scroller scroller=new Scroller(context);
- 重写
computeScroll()
方法 - 最后,在我们的
smoothScrollTo
方法中调用startScroll
方法
参考如下代码:
@Override
public void computeScroll(){
if(scroller.computeScrollOffset()){
scrollTo(scroller.getCurrX(),scroller.getCurrY());
postInvalidate();
}
}
public void smoothScrollTo(int destX,int destY){
int scrollX=getScrollX();
int deltaX=destX-scrollX;
scroller.startScroll(scrollX,0,deltaX,0,1000);
}
以上的smoothScrollTo实现的是x方向的平滑,其中startScroll函数的形参分别表示:起始位置的x坐标、起始位置的y坐标、x方向要移动的距离、y方向上要移动的距离以及整个滑动过程完成所需的时间。
参照我们上一节提到的Scroller典型用法,我们进入到ViewPager源码。我们在ViewPager的initViewPager方法中找到:
void initViewPager() {
final Context context = getContext();
mScroller = new Scroller(context, sInterpolator);
}
它跟我们上一节使用到的Scroller构造器不同,他选择使用2个形参的构造器。其实,第二个形参就是插值器(interpolator
),对插值器不熟悉的童鞋可以去搜索一下动画插值器相关内容。其实这个插值器就是根据不同的时间控制滑动的速度,就像高中物理中的物体变速运动。我们继续看看ViewPager
中自定义的插值器sInterpolator
,从变量名称中以s开头,就知道sInterpolator
是个static属性:
private static final Interpolator sInterpolator = new Interpolator() {
public float getInterpolation(float t) {
t -= 1.0f;
return t * t * t * t * t + 1.0f;
}
};
Interpolator是一个接口,它继承自TimeInterpolator这个接口,而Interpolator没有添加新的抽象方法,TimeInterpolator只有一个抽象方法:float getInterpolation(float input);
其中,input形参是取值范围为0到1,表示当前的动画时间点,0表示动画开始,1表示动画结束。返回值表示移动到目标位置的比值,如果大于1,则表示超出了最大位置,小于0表示比最小位置还要小。怎么理解呢?举个例子,假设我们要实现变速动画,我们要持续的时间是[0,1000],要滑动的距离是[0,100],那么假设当前时间是200,则传入到getInterpolation的形参就是200/1000=0.2,表示时间过了0.2,具体的返回值可以根据你的变速需求计算,假设你的返回值是0.8,那么表示当前位置要处于100 * 0.8=80这个位置。如果你的返回值是1.8 ,那么肯定就是超出100了:100*1.8=180。
ViewPager实现的功能已经兼容性都是比较健全的,所有computeScroll()不会像我们所写的那么简单,我们一起”膜拜”一下官方代码吧:
@Override
public void computeScroll() {
mIsScrollStarted = true;
if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
int oldX = getScrollX();
int oldY = getScrollY();
int x = mScroller.getCurrX();
int y = mScroller.getCurrY();
if (oldX != x || oldY != y) {
scrollTo(x, y);
if (!pageScrolled(x)) {
mScroller.abortAnimation();
scrollTo(0, y);
}
}
ViewCompat.postInvalidateOnAnimation(this);
return;
}
completeScroll(true);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
computeScroll
函数里面大部分代码比较清晰,只有两个函数,需要我们进去深究:pageScrolled
以及completeScroll
。
2.2.1 pageScrolled
先看看pageScrolled
函数,这个函数主要的作用是回调onPageScrolled
,虽然做了很多计算,但这些计算的结果最终是为了作为形参传给onPageScrolled
,看看他的源码:
private boolean pageScrolled(int xpos) {
if (mItems.size() == 0) {
mCalledSuper = false;
onPageScrolled(0, 0, 0);
if (!mCalledSuper) {
throw new IllegalStateException(
"onPageScrolled did not call superclass implementation");
}
return false;
}
final ItemInfo ii = infoForCurrentScrollPosition();
final int width = getClientWidth();
final int widthWithMargin = width + mPageMargin;
final float marginOffset = (float) mPageMargin / width;
final int currentPage = ii.position;
final float pageOffset = (((float) xpos / width) - ii.offset) /
(ii.widthFactor + marginOffset);
final int offsetPixels = (int) (pageOffset * widthWithMargin);
mCalledSuper = false;
onPageScrolled(currentPage, pageOffset, offsetPixels);
if (!mCalledSuper) {
throw new IllegalStateException(
"onPageScrolled did not call superclass implementation");
}
return true;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
我们定位到第6个注释,我提到infoForCurrentScrollPosition
函数是据当前滑动的位置,得到当前显示的子View的抽象描述类ItemInfo,如果当前滑动位置显示的恰好是一个完整的页面,这个页面的前一个页面和后一个页面都没有显示,那么很容易理解,返回的就是这个页面。可是如果当前显示区域是同时显示2个页面(两个页面都显示一部分出现在显示区域),那这个函数应该返回哪一个页面呢?从infoForCurrentScrollPosition
源码看出每次是返回左边的页面,如下图所示:
换句话说,只会是存在当前页面与下一个页面同时出现在显示区域,不可能是当前页面与上一个页面同时出现。关于infoForCurrentScrollPosition
的具体实现,我们不要去关心,我们只要知道它帮我们实现了什么功能,如果对其感兴趣可以去看源码。
2.2.2 onPageScrolled
上面我们知道,pageScrolled
函数是为了调用onPageScrolled
做前期计算,并将计算结果作为onPageScrolled
的形参,最终是为了回调onPageScrolled
函数,那么我们看看onPageScrolled
函数到底是干了啥~,从函数名看的出来,它是一个回调函数,那么是什么情况下回调呢?其实,在我们手指滑动或者是通过代码直接滑动到指定位置过程中,会使得一些页面滑动,如果我们想要在每个页面在显示区域滑动过程中实现某些效果,可以重写这个函数,当然了,我们前面分析pageScrolled
函数时就提到,重写onPageScrolled
时,必须先调用super.onPageScrolled(position, offset, offsetPixels)
,我们的ViewPager在滑动过程中,会不断回调onPageScrolled函数,这个“不断”是从这里体现:computeScroll—>onPageScrolled->onPageScrolled。滑动过程不断调用computeScroll
,而computeScroll
调用onPageScrolled
,onPageScrolled
又调用onPageScrolled
。好了,我们去看看onPageScrolled
吧~首先看看三个参数:
int position
,表示当前是第几个页面float offset
表示当前页面移动的距离,其实就是个相对实际宽度比例值,取值为[0,1)。0表示整个页面在显示区域,1表示整个页面已经完全左移出显示区域。int offsetPixels
, 表示当前页面左移的像素个数。
我们已经了解形参的含义,接下来看看源码:
@CallSuper
protected void onPageScrolled(int position, float offset, int offsetPixels) {
if (mDecorChildCount > 0) {
}
dispatchOnPageScrolled(position, offset, offsetPixels);
if (mPageTransformer != null) {
final int scrollX = getScrollX();
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp.isDecor) continue;
final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth();
mPageTransformer.transformPage(child, transformPos);
}
}
mCalledSuper = true;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
从源码上我们知道,onPageScrolled
做了3件事,首先把Decor View
固定在显示区域,其次,将滚动事件进行分发,即dispatchOnPageScrolled
函数,dispatchOnPageScrolled
函数内部就是调用OnPageChangeListener
的onPageScrolled
函数,我们添加的监听器就是此时被回调onPageScrolled
函数,dispatchOnPageScrolled
函数代码比较简单,不去追究。最后,就是判断是否设置了mPageTransformer
,如果设置了,就去回调mPageTransformer
的transformPage
函数,我们知道,我们可以通过自定义PageTransformer
来实现每个页面的“出场动画”和“离场动画”,就是这里回调transformPage
来实现的。
把目光回到computeScroll
函数,我们前面说道,在computeScroll
函数最后调用了completeScroll
函数,这个函数是做滑动结束后的清理复位等工作。比如:确保滚动已经到最终位置,如果没有到最终位置,则滚动到最终位置。还有就是将每个页面对应的ItemInfo
对象的scrolling
设为false
等等。
根据第1节,我们知道,重写了computeScroll
函数后,需要自定义一种平滑到指定位置的函数,一般命名为smoothScrollTo
,当然咯,你也可以取其他名字,你开心就好~。但是在这个函数里面需要调用startScroll
函数。我们来看看ViewPager
的smoothScrollTo
函数源码,其中x,y
表示要移动到的位置,velocity
表示手指移动速度,如果不是用户的手指触发的平滑操作,则velocity
设为0即可:
void smoothScrollTo(int x, int y, int velocity) {
if (getChildCount() == 0) {
setScrollingCacheEnabled(false);
return;
}
int sx;
boolean wasScrolling = (mScroller != null) && !mScroller.isFinished();
if (wasScrolling) {
sx = mIsScrollStarted ? mScroller.getCurrX() : mScroller.getStartX();
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);
mIsScrollStarted = false;
mScroller.startScroll(sx, sy, dx, dy, duration);
ViewCompat.postInvalidateOnAnimation(this);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
从上面可以看到,ViewPager
的smoothScrollTo
的实现还是挺复杂的,代码实现出来的效果体验非常好以及所考虑的功能很全面。感觉非常值得去学习!另外,ViewPager提供了只有x
,y
两个参数的smoothScrollTo
,其内部也是调用上面这个smoothScrollTo
,只是将velocity
参数设置为0。
3 滑动冲突
现在为止,ViewPager
的滑动部分已经分析完毕,但是用过ViewPager
都知道,ViewPager
帮我们处理了滑动冲突。我们知道,ViewPager
只关注水平方向的手指滑动,根据水平方向的手指滑动来切换页面。在垂直方向上,ViewPager
并不关心,因此,ViewPager
很有必要解决一下滑动冲突,把竖直方向的滑动传递给子View来处理。
我们知道,ViewGroup
是在onInterceptTouchEvent
函数中决定是否拦截触摸事件,那么我们就去学习一下ViewPager
的onInterceptTouchEvent
函数。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
if (DEBUG) Log.v(TAG, "Intercept done!");
resetTouch();
return false;
}
if (action != MotionEvent.ACTION_DOWN) {
if (mIsBeingDragged) {
if (DEBUG) Log.v(TAG, "Intercept returning true!");
return true;
}
if (mIsUnableToDrag) {
if (DEBUG) Log.v(TAG, "Intercept returning false!");
return false;
}
}
switch (action) {
case MotionEvent.ACTION_MOVE: {
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
break;
}
final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
final float x = MotionEventCompat.getX(ev, pointerIndex);
final float dx = x - mLastMotionX;
final float xDiff = Math.abs(dx);
final float y = MotionEventCompat.getY(ev, pointerIndex);
final float yDiff = Math.abs(y - mInitialMotionY);
if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
if (dx != 0 && !isGutterDrag(mLastMotionX, dx) &&
canScroll(this, false, (int) dx, (int) x, (int) y)) {
mLastMotionX = x;
mLastMotionY = y;
mIsUnableToDrag = true;
return false;
}
if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {
if (DEBUG) Log.v(TAG, "Starting drag!");
mIsBeingDragged = true;
requestParentDisallowInterceptTouchEvent(true);
setScrollState(SCROLL_STATE_DRAGGING);
mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop :
mInitialMotionX - mTouchSlop;
mLastMotionY = y;
setScrollingCacheEnabled(true);
} else if (yDiff > mTouchSlop) {
if (DEBUG) Log.v(TAG, "Starting unable to drag!");
mIsUnableToDrag = true;
}
if (mIsBeingDragged) {
if (performDrag(x)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
break;
}
case MotionEvent.ACTION_DOWN: {
mLastMotionX = mInitialMotionX = ev.getX();
mLastMotionY = mInitialMotionY = ev.getY();
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
mIsUnableToDrag = false;
mIsScrollStarted = true;
mScroller.computeScrollOffset();
if (mScrollState == SCROLL_STATE_SETTLING &&
Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {
mScroller.abortAnimation();
mPopulatePending = false;
populate();
mIsBeingDragged = true;
requestParentDisallowInterceptTouchEvent(true);
setScrollState(SCROLL_STATE_DRAGGING);
} else {
completeScroll(false);
mIsBeingDragged = false;
}
if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY
+ " mIsBeingDragged=" + mIsBeingDragged
+ "mIsUnableToDrag=" + mIsUnableToDrag);
break;
}
case MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
return mIsBeingDragged;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
我们看看ViewPager
是如何决定是拦截还是不拦截,从源码上面看出,但斜率小于0.5时,则要拦截,否则不拦截,斜率是什么情况呢?高中数学可知,在第一象限中,越靠近y轴的直线,斜率越大,越靠近x轴直线斜率越小,先看简单图示:
也就是说,手指滑动的倾斜度比0.5小,就去拦截事件,由ViewPager
来响应切换页面。