路是一步一步走的,代码也是一行一行敲的,只要你始终把握好前行的方向,终究会到达你想要去的地方。
您还可以查看上一篇文章:
《viewpaper系列之子view的缓存基本原理》
onMeasure
在上一节中,还有一个地方没有说到,就是 lp.isDecor这个参数,指不是从adapter中添加子view时返回true,业务逻辑也就进入下面代码进行子view的测量。
int size = getChildCount();
Log.d(TAG_TEST,"size: "+ size);
for (int i = 0; i < size; ++i) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 这里的isDecor参数是指当你不是从中adapter添加子view时返回为true
if (lp != null && lp.isDecor) {
final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
int widthMode = MeasureSpec.AT_MOST;
int heightMode = MeasureSpec.AT_MOST;
boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM;
boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT;
if (consumeVertical) {
widthMode = MeasureSpec.EXACTLY;
} else if (consumeHorizontal) {
heightMode = MeasureSpec.EXACTLY;
}
int widthSize = childWidthSize;
int heightSize = childHeightSize;
if (lp.width != LayoutParams.WRAP_CONTENT) {
widthMode = MeasureSpec.EXACTLY;
if (lp.width != LayoutParams.FILL_PARENT) {
widthSize = lp.width;
}
}
if (lp.height != LayoutParams.WRAP_CONTENT) {
heightMode = MeasureSpec.EXACTLY;
if (lp.height != LayoutParams.FILL_PARENT) {
heightSize = lp.height;
}
}
final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode);
final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode);
child.measure(widthSpec, heightSpec);
if (consumeVertical) {
childHeightSize -= child.getMeasuredHeight();
} else if (consumeHorizontal) {
childWidthSize -= child.getMeasuredWidth();
}
}
}
}
onLyout
final int childWidth = width - paddingLeft - paddingRight;
// Page views. Do this once we have the right padding offsets from above.
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
// 判断子view是否为显示状态
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
ItemInfo ii;
if (!lp.isDecor && (ii = infoForChild(child)) != null) {
int loff = (int) (childWidth * ii.offset);
int childLeft = paddingLeft + loff;
int childTop = paddingTop;
if (lp.needsMeasure) {
// This was added during layout and needs measurement.
// Do it now that we know what we're working with.
lp.needsMeasure = false;
final int widthSpec = MeasureSpec.makeMeasureSpec(
(int) (childWidth * lp.widthFactor),
MeasureSpec.EXACTLY);
final int heightSpec = MeasureSpec.makeMeasureSpec(
(int) (height - paddingTop - paddingBottom),
MeasureSpec.EXACTLY);
child.measure(widthSpec, heightSpec);
}
if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object
+ ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth()
+ "x" + child.getMeasuredHeight());
// 给每个子view进行布局
child.layout(childLeft, childTop,
childLeft + child.getMeasuredWidth(),
childTop + child.getMeasuredHeight());
}
}
}
看完上面的代码,我们从上到下依次来说说注意点
第一点
我们来看看这个方法它的具体执行,因为其返回值直接影响到if语句的逻辑走向,导致是否执行child的layout操作。
ii = infoForChild(child)
ItemInfo infoForChild(View child) {
for (int i=0; i
看到上面if中的判断条件,是不是有种熟悉的感觉,对的,他就是adapter中我们时常要复写的一个方法的调用。现在知道为什么要这样写了吧,因为这个返回值代表着当前这个view你是否给它去进行layout的布局方法的调用,那你肯定会问,为什么不直接返回true呢?仔细看你就会明白,因为返回true会导致所有的子view都会被布局,而viewpager的设计就是,只加载当前页面和预加载页面。所以没有必要给所有的子view进行布局。
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
第二点
这个条件
lp.needsMeasure
从命名就知道,是是否需要进行测量,主要是为了在layout进行时中添加子view时进行使用,在添加时给子view的layoutparam属性进行needsMeasure的赋值。
到此,子view的layout基本完毕,接下来就可以进入onTouchEvent来查看滑动时的逻辑了。
onTouchEvent
这个方法比较长,我们分别来看。
action_down
case MotionEvent.ACTION_DOWN: {
mScroller.abortAnimation();
mPopulatePending = false;
populate();
// Remember where the motion event started
mLastMotionX = mInitialMotionX = ev.getX();
mLastMotionY = mInitialMotionY = ev.getY();
// 这个point为活动id
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
Log.d("zp_test","mActivePointerId: " + mActivePointerId);
break;
}
什么是活动id呢?也就是当前能操作界面的这个点击事件的id,这个值在同一系列的点击事件中是不会发生改变的,所以通过这个id就可以始终获取到能够操作界面的那个手指的点击事件(比如,我第一个手指点下时,这是是手指1操作,这时,如果我再次用另一根手指2点击屏幕,那么这个时候能够操作屏幕的就是手指2,通过这个id就可以获取到手指2的位置信息等),这里主要是为了兼容多点触控的情况。
action_move
move就是很常见一系列移动view的操作,主要就是记录住手指移动的距离;
需了解的点:
- 移动距离记得跟mTouchSlop(手指移动最小距离)进行比较
- 回调监听的调用,手指状态发生改变,setScrollState(SCROLL_STATE_DRAGGING);
- 从父元素那取得滑动事件的处理权,requestParentDisallowInterceptTouchEvent(true);
case MotionEvent.ACTION_MOVE:
if (!mIsBeingDragged) {
final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
if (pointerIndex == -1) {
// 如果viewpager的子元素处理某个点击事件,有可能导致进入这个判断
needsInvalidate = resetTouch();
break;
}
final float x = MotionEventCompat.getX(ev, pointerIndex);
final float xDiff = Math.abs(x - mLastMotionX);
final float y = MotionEventCompat.getY(ev, pointerIndex);
final float yDiff = Math.abs(y - mLastMotionY);
if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
if (xDiff > mTouchSlop && xDiff > yDiff) {
if (DEBUG) Log.v(TAG, "Starting drag!");
mIsBeingDragged = true;
requestParentDisallowInterceptTouchEvent(true);
mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop :
mInitialMotionX - mTouchSlop;
mLastMotionY = y;
// 手指状态发生改变,这时进行回调监听的调用。
setScrollState(SCROLL_STATE_DRAGGING);
setScrollingCacheEnabled(true);
// Disallow Parent Intercept, just in case
ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
}
// Not else! Note that mIsBeingDragged can be set above.
if (mIsBeingDragged) {
// Scroll to follow the motion event
final int activePointerIndex = MotionEventCompat.findPointerIndex(
ev, mActivePointerId);
final float x = MotionEventCompat.getX(ev, activePointerIndex);
needsInvalidate |= performDrag(x);
}
最后真正执行滑动操作的代码在performDrag(x);
中
private boolean performDrag(float x) {
boolean needsInvalidate = false;
// 手势滑动的距离
final float deltaX = mLastMotionX - x;
mLastMotionX = x;
// 当前位置到该view的偏移量
float oldScrollX = getScrollX();
float scrollX = oldScrollX + deltaX;
final int width = getClientWidth();
// viewpager的左边界位置
float leftBound = width * mFirstOffset;
// viewpager的右边界位置
float rightBound = width * mLastOffset;
boolean leftAbsolute = true;
boolean rightAbsolute = true;
final ItemInfo firstItem = mItems.get(0);
final ItemInfo lastItem = mItems.get(mItems.size() - 1);
if (firstItem.position != 0) {
leftAbsolute = false;
leftBound = firstItem.offset * width;
}
if (lastItem.position != mAdapter.getCount() - 1) {
rightAbsolute = false;
rightBound = lastItem.offset * width;
}
if (scrollX < leftBound) {
if (leftAbsolute) {
float over = leftBound - scrollX;
needsInvalidate = mLeftEdge.onPull(Math.abs(over) / width);
}
scrollX = leftBound;
} else if (scrollX > rightBound) {
if (rightAbsolute) {
float over = scrollX - rightBound;
needsInvalidate = mRightEdge.onPull(Math.abs(over) / width);
}
scrollX = rightBound;
}
// Don't lose the rounded component
mLastMotionX += scrollX - (int) scrollX;
// 最终实现随手指滑动的代码
scrollTo((int) scrollX, getScrollY());
pageScrolled((int) scrollX);
return needsInvalidate;
}
到这里,我们可以知道,随手指进行滑动这类操作,主要使用到的方法就是scrollTo(),而传递进入的参数一般都是可以通过(当前偏移量)+(手指滑动距离)这样的组合进行传递。比如:getScrollX()+deltaX等等。
action_up
if (mIsBeingDragged) {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) VelocityTrackerCompat.getXVelocity(
velocityTracker, mActivePointerId);
mPopulatePending = true;
final int width = getClientWidth();
final int scrollX = getScrollX();
final ItemInfo ii = infoForCurrentScrollPosition();
final float marginOffset = (float) mPageMargin / width;
final int currentPage = ii.position;
final float pageOffset = (((float) scrollX / width) - ii.offset)
/ (ii.widthFactor + marginOffset);
Log.d("zp_test","pageOffset: " + pageOffset);
final int activePointerIndex =
MotionEventCompat.findPointerIndex(ev, mActivePointerId);
final float x = MotionEventCompat.getX(ev, activePointerIndex);
final int totalDelta = (int) (x - mInitialMotionX);
// 根据滑动手势的快慢,偏移量来判断是否需要滑动到下一页
int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
totalDelta);
setCurrentItemInternal(nextPage, true, true, initialVelocity);
needsInvalidate = resetTouch();
}
接下来看 setCurrentItemInternal(nextPage, true, true, initialVelocity)
中有一部分代码如下:
...
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);
}
在非第一次layout的情况下会进行else中的代码执行,先计算所有子view,然后平滑移动到目的地。最终定位到以下代码
...
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;
// 平滑移动到指定的sx位置
mScroller.startScroll(sx, sy, dx, dy, duration);
ViewCompat.postInvalidateOnAnimation(this);
...
移动的任务就交给了mScroller.startScroll(sx, sy, dx, dy, duration);这句代码了。关于Scroller的原理,可以自行了解,其实现过程其实就是在指定时间内,把需要移动的距离等分为sx/duration段,然后在duration时间段内进行不断更新当前偏移量。