欣赏一下
功能点
API 21
- 测量,布局,绘制;
- 事件的处理机制, viewPager的主动消耗,拦截等;
- 页面滚动计算,手动滚动;
- viewPager设计带来的问题;
0. 核心变量和标记
- mItems: 已经缓存过的page, 按照page的position从小到大来排列。
- mCurItem: 当前显示的page的position, 这是全局的。全局是针对mItems来说的.假如有5个page,
mItems存储的可能是最后的三个页面,那他缓存的第一个页面并不是系统中的第一个page,而是全局的第三个page.
- mAdapter: 动态加载子page。
- ItemInfo: page控件构建的对象,里面的position即为全局page的position。
- mOffscreenPageLimit: 离屏限制数量,默认是1,也就是除了当前page左右缓存各一个,总数是3;如果是2,那么就左右各缓存两个,总数是5。
- Scroller: 一个平滑滚动效果的计算工具类,类似的有Overscroller.他是根据起始坐标,终点坐标,以及时间这几个变量来计算不同时间的view的x, y坐标的哦,从而实现滚动计算。
1. 测量:
ViewPager我们一般是不会在它的内部主动添加子view的,而是通过Adapter的形式去动态注入。其实除此之外,他还可以在xml添加他的DecorView, 这种特殊的view和adapter中添加的view的测量,布局都是不一样,他一般是固定在viewPager的页面中的不像page view一样随着手势滚动,比如ViewPager的indicator这种就是DecorView。
-
onMeasure: 测量
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //简单的一行代码告诉了我,这viewPager的大小在这里就已经确定了。 //如果viewpager是wrap和match的结果都一样就是父容器剩下的宽高,如果是设定了dimense那 //就是他自己的dimense宽高了。viewPager的这种设定就和通常的控件测量不一样了,他完全忽略了自己的 //pageview自己设定的宽与高了,这种设计存在这一些缺陷. //比如不要轻易地将viewPager放到ScrollView中,你会发现viewpager没有高度。 setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), getDefaultSize(0, heightMeasureSpec)); final int measuredWidth = getMeasuredWidth(); final int maxGutterSize = measuredWidth / 10; mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize); //这是viewPager测量之后,得到的剩余可用的宽与高 int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight(); int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); //先测量装饰的Decor,viewager的pageview的空间是扣除Decor之后的空间的哦; int size = getChildCount(); for (int i = 0; i < size; ++i) { final View child = getChildAt(i); if (child.getVisibility() != GONE) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (lp != null && lp.isDecor) {//如果是Decor final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; //Decor的测量规则大概是这样的,如果是top/bottom就是横向填充,宽的规格mode是 //EXACTLY, 规格size根据match/wrap,dimense来定。也就是如果size是 //match,wrap, 他们最后的规格尺寸都是一样的即viewpager的可用宽度。dimense就是 //设定的宽。高的规格mode则要根据layoutParams来定,如果不是wrap,那么就是 //EXACTLY, 是就是AT_MOST,size在match/wrap的状态下都一样的。所以呢,他这个测量 //原则和标准的测量行为是保持一致的。一个方向的规格在wrap/match情况下size都是相同 //的,只有在dimense情形下不同。 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); //得出了Decor的测量规格之后,就可以对Decor进行测量啦; child.measure(widthSpec, heightSpec); if (consumeVertical) { //剩余的高就是page view的高度 childHeightSize -= child.getMeasuredHeight(); } else if (consumeHorizontal) { //剩余的宽就是page view的宽 childWidthSize -= child.getMeasuredWidth(); } } } } //看到了,这就是page view的测量规格,这里已经确定了,他和具体的page view所设定的尺寸 //没有半毛钱的关系,全靠viewpager除去Decor之后剩余宽,高决定。 mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY); mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY); // Make sure we have created all fragments that we need to have shown. mInLayout = true; //这里是再次确定下要创建page view; populate(); mInLayout = false; // Page views next. size = getChildCount(); for (int i = 0; i < size; ++i) { final View child = getChildAt(i); if (child.getVisibility() != GONE) { if (DEBUG) Log.v(TAG, "Measuring #" + i + " " + child + ": " + mChildWidthMeasureSpec); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (lp == null || !lp.isDecor) { //对于pageView的测量啊,我们要计算一下页面的宽度因子,这个是0-1.0之间,1是全部的 //宽,0.5是一半这样子.... final int widthSpec = MeasureSpec.makeMeasureSpec( (int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY); //测量子page啦; child.measure(widthSpec, mChildHeightMeasureSpec); } } } }
- 总结一下, 在测量的时候,一开始是没有子page view的,所以需要调用populate来创建和加载子page view, 然后才能测量子page。所以测量的功能大体分为三步骤:一先测量ViewPager大小, 二加载子page, 三测量子page。
-
populate:创建和销毁page view的核心方法
void populate(int newCurrentItem) { ItemInfo oldCurInfo = null; int focusDirection = View.FOCUS_FORWARD; //新的page的position和老的不同,那么将新赋值给mCurItem if (mCurItem != newCurrentItem) { focusDirection = mCurItem < newCurrentItem ? View.FOCUS_RIGHT : View.FOCUS_LEFT; //记录老的position对应的page, 这些都缓存在了mItems中呢。 oldCurInfo = infoForPosition(mCurItem); mCurItem = newCurrentItem; } if (mAdapter == null) {//如果没设置adapter, 那么就没法创建和加载子page啦,简单地排下Decor //顺序,然后跳过啦. sortChildDrawingOrder(); return; } //这个是在当用户抬起的手指的时候,page还在计算滚动,我们不去创建和更改子page,为了安全起见。跳过啦。 if (mPopulatePending) { if (DEBUG) Log.i(TAG, "populate is pending, skipping for now..."); sortChildDrawingOrder(); return; } // Also, don't populate until we are attached to a window. This is to // avoid trying to populate before we have restored our view hierarchy // state and conflicting with what is restored. if (getWindowToken() == null) { return; } //这是adapter的一个回调,用来告诉外界,已经开始加载子page了。 mAdapter.startUpdate(this); final int pageLimit = mOffscreenPageLimit; //计算出最左端的全局position,最小肯定是0啦; final int startPos = Math.max(0, mCurItem - pageLimit); final int N = mAdapter.getCount(); //计算出最右边的全局positon, 最大肯定是N-1; final int endPos = Math.min(N-1, mCurItem + pageLimit); //这是保证当更新了adapter的数据之后,你要手动地去notifyDataSetChanged,否则数据不会更新; if (N != mExpectedAdapterCount) { String resName; try { resName = getResources().getResourceName(getId()); } catch (Resources.NotFoundException e) { resName = Integer.toHexString(getId()); } throw new IllegalStateException("The application's PagerAdapter changed the adapter's" + " contents without calling PagerAdapter#notifyDataSetChanged!" + " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N + " Pager id: " + resName + " Pager class: " + getClass() + " Problematic adapter: " + mAdapter.getClass()); } // curIndex不是page对应的position, 而是items集合中存储的位置,这个要和mCurItem区分开来哦 int curIndex = -1; //curItem,当前要display的page. ItemInfo curItem = null; //在mItems寻找当前display的page for (curIndex = 0; curIndex < mItems.size(); curIndex++) { final ItemInfo ii = mItems.get(curIndex); if (ii.position >= mCurItem) {//如果我写的话肯定就是直接判等,但是没这样好,这样大于的 //时候会立即终止遍历没必要啦。如果都小于mCurItem就在最后一个位置加上新的item. //算是效率上的一次小优化吧,值得学习 if (ii.position == mCurItem) curItem = ii; break; } } //只要当前curItem为null, page view设置了数量,创建新的item,添加到mItems集合中相应的位置中去呢; //这种一般是在第一次创建的时候才有的,后面就不会走的了.... if (curItem == null && N > 0) { curItem = addNewItem(mCurItem, curIndex); } //接下来就是根据当前的page来左右去增删page了,这里也是vp的核心思路啦 if (curItem != null) { //这个是用来累计左边的item的所处的宽度; float extraWidthLeft = 0.f; int itemIndex = curIndex - 1; ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; final int clientWidth = getClientWidth(); //这是一个左边的限定因子,用来决定最左边可以使用的宽度是多少,以决定怎么缓存page //如果是widthFactor是1,那么左边因子就是1(忽略padding),也就是左边至少能缓存一个page, //如果widthFactor是0.5,那么左边因子就是1.5, 也就是左边至少可以缓存3个page final float leftWidthNeeded = clientWidth <= 0 ? 0 : 2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth; //从当前的page前一个开始往左遍历,在全局的position为0停下来,这样当当前page是0时候,就不会再 //浪费时间往前去排查了。 for (int pos = mCurItem - 1; pos >= 0; pos--) { //除了限定因子,还有一个我们设置的mOffscreenPageLimit哦 if (extraWidthLeft >= leftWidthNeeded && pos < startPos) { //当满足了限定,并且位置是小于了最左边的position,就需要destory了; //如果查到前面有null了,说明这个位置已经destory过了。就不需要去destory了,可以停下 //了。 if (ii == null) { break; } // if (pos == ii.position && !ii.scrolling) { //缓存中清除 mItems.remove(itemIndex); //从viewPager中清除子page mAdapter.destroyItem(this, pos, ii.object); if (DEBUG) { Log.i(TAG, "populate() - destroyItem() with pos: " + pos + " view: " + ((View) ii.object)); } //因为要往前遍历去destory啦!保证找到一个为null的page. itemIndex--; curIndex--; ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; } //如果缓存中的该page正好是需要的page } else if (ii != null && pos == ii.position) { //累计一个widthFactor extraWidthLeft += ii.widthFactor; //在缓存中向前查page, itemIndex--; ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; } else {//如果缓存中没有需要的page,那么就要创建了哦 //没有需要的page是一般因为itemIndex为-1,当前缓存的最左边的就是当前page,所以需要 //在0位置上再添加一个page.也还有可能是不符合的page,那么也要添加一个page,因此要在 //itemIndex偏移一个位置。 ii = addNewItem(pos, itemIndex + 1); extraWidthLeft += ii.widthFactor; //当前page在mItems位置增加1 curIndex++; //取出当前不符合的page遍历,下一次他可能就需要destory了。 ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; } } float extraWidthRight = curItem.widthFactor; //右边的一个缓存page itemIndex = curIndex + 1; //下面的处理和左边几乎就是一模一样啦。 if (extraWidthRight < 2.f) { ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; final float rightWidthNeeded = clientWidth <= 0 ? 0 : (float) getPaddingRight() / (float) clientWidth + 2.f; for (int pos = mCurItem + 1; pos < N; pos++) { if (extraWidthRight >= rightWidthNeeded && pos > endPos) { if (ii == null) { break; } if (pos == ii.position && !ii.scrolling) { mItems.remove(itemIndex); mAdapter.destroyItem(this, pos, ii.object); if (DEBUG) { Log.i(TAG, "populate() - destroyItem() with pos: " + pos + " view: " + ((View) ii.object)); } ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; } } else if (ii != null && pos == ii.position) { extraWidthRight += ii.widthFactor; itemIndex++; ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; } else { //左边要偏移一个位置,这里是不需要的 ii = addNewItem(pos, itemIndex); itemIndex++; extraWidthRight += ii.widthFactor; ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; } } } //这是最后一个难度计算了;快结束了..... calculatePageOffsets(curItem, curIndex, oldCurInfo); } ......略 //告诉外面当前display的page是谁。 mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null); //到这里,添加页面删除页面的动作就结束了啦。后面的东西和添加删除page关系不是太大; mAdapter.finishUpdate(this); // 这里是用来设定child的布局参数的,因为child的布局参数是源自于pageadpter的设定的;所以在读取了 //adapter内容之后,这里要把他的widthFactor和position给到LayoutParams中去,以便后续使用 final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); lp.childIndex = i; if (!lp.isDecor && lp.widthFactor == 0.f) { // 0 means requery the adapter for this, it doesn't have a valid width. final ItemInfo ii = infoForChild(child); if (ii != null) { lp.widthFactor = ii.widthFactor; lp.position = ii.position; } } } //这里将缓存的所有view排好绘制顺序,Decor装饰元素是最后绘的. sortChildDrawingOrder(); //如果viewPager有焦点,必须将焦点view放在当前显示的page的结构树上; if (hasFocus()) { View currentFocused = findFocus(); ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null; if (ii == null || ii.position != mCurItem) { for (int i=0; i
- 总结一下下:populate的实现比较繁琐略带复杂,但是他的目的是很单纯的,就是在初次加载page或者滑动viewpager的时候在布局容器中加载对应的子page, 同时删除超过限定位置的page,以达到内存的优化啦。比如我们限定的mOffscreenPageLimit是1, 那么内存中缓存的就是3个page, 我们会计算出当前page的左右两个缓存起来的,其他的页面删除掉。随着页面的滚动,动态更新缓存内容page.
-
addNewItem: 添加子Item元素
//创建新的item到mItems集合中去;position是page在所有的page中对应的位置,全局。index是在mItems中缓存的位置。 ItemInfo addNewItem(int position, int index) { ItemInfo ii = new ItemInfo(); ii.position = position; //看到没,这里是调用我们复写adapter的instantiateItem来创建子page的哦; ii.object = mAdapter.instantiateItem(this, position); //也是调用我们的adapter来加载宽度因子 ii.widthFactor = mAdapter.getPageWidth(position); if (index < 0 || index >= mItems.size()) { mItems.add(ii); } else { mItems.add(index, ii); } return ii; }
- calculatePageOffsets:计算各个page的offset的偏移量。
//curItem-当前display的page, curIndex-他在mItems中的位置, oldInfo上一次display的page private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) { final int N = mAdapter.getCount(); final int width = getClientWidth(); final float marginOffset = width > 0 ? (float) mPageMargin / width : 0; // Fix up offsets for later layout. if (oldCurInfo != null) { final int oldCurPosition = oldCurInfo.position; //其实下面的逻辑是计算当前的page和原来显示的page之间的page的offset偏移量 // 如果当前是向左侧滑动 if (oldCurPosition < curItem.position) { int itemIndex = 0; ItemInfo ii = null; //当前page的offset是左边一个page的offset+他的宽度因子+margin因子 float offset = oldCurInfo.offset + oldCurInfo.widthFactor + marginOffset; for (int pos = oldCurPosition + 1; pos <= curItem.position && itemIndex < mItems.size(); pos++) { ii = mItems.get(itemIndex); while (pos > ii.position && itemIndex < mItems.size() - 1) { itemIndex++; ii = mItems.get(itemIndex); } while (pos < ii.position) { // We don't have an item populated for this, // ask the adapter for an offset. offset += mAdapter.getPageWidth(pos) + marginOffset; pos++; } ii.offset = offset; //下一个page的offset同样累加上当前page的宽度因子和margin因子 offset += ii.widthFactor + marginOffset; } //如果是向右边滑动 } else if (oldCurPosition > curItem.position) { int itemIndex = mItems.size() - 1; ItemInfo ii = null; float offset = oldCurInfo.offset; for (int pos = oldCurPosition - 1; pos >= curItem.position && itemIndex >= 0; pos--) { ii = mItems.get(itemIndex); while (pos < ii.position && itemIndex > 0) { itemIndex--; ii = mItems.get(itemIndex); } while (pos > ii.position) { // We don't have an item populated for this, // ask the adapter for an offset. offset -= mAdapter.getPageWidth(pos) + marginOffset; pos--; } //当前page的offset = 后一个page的offset - 当前page的width因子- margin因子 offset -= ii.widthFactor + marginOffset; ii.offset = offset; } } } //接下来计算所有的缓存的page的偏移因子;根据前面的原则,难度也不大。 // 除此之外,还计算了第一个缓存的page的偏移因子,最后一个page的偏移因子。 final int itemCount = mItems.size(); float offset = curItem.offset; int pos = curItem.position - 1; mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE; mLastOffset = curItem.position == N - 1 ? curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE; // Previous pages for (int i = curIndex - 1; i >= 0; i--, pos--) { final ItemInfo ii = mItems.get(i); while (pos > ii.position) { offset -= mAdapter.getPageWidth(pos--) + marginOffset; } offset -= ii.widthFactor + marginOffset; ii.offset = offset; if (ii.position == 0) mFirstOffset = offset; } offset = curItem.offset + curItem.widthFactor + marginOffset; pos = curItem.position + 1; // Next pages for (int i = curIndex + 1; i < itemCount; i++, pos++) { final ItemInfo ii = mItems.get(i); while (pos < ii.position) { offset += mAdapter.getPageWidth(pos++) + marginOffset; } if (ii.position == N - 1) { mLastOffset = offset + ii.widthFactor - 1; } ii.offset = offset; offset += ii.widthFactor + marginOffset; } mNeedCalculatePageOffsets = false; }
- 简单总结一下, page的offset计算也是挺繁琐的,这个玩意是来干嘛的, 有什么用呢? 它其实是用来布局子page元素的,定位每个page的位置,每个page的定位都和前面的page息息相关,这里用每个page的offset来标识。接下来看viewPager是如何给子page来布局, 就会明白这个offset的实际用途呢。
2. ViewPager的布局过程:
-
onLayout: 布局ViewPager的子page以及装饰的Decor.如title
protected void onLayout(boolean changed, int l, int t, int r, int b) { final int count = getChildCount(); int width = r - l; int height = b - t; int paddingLeft = getPaddingLeft(); int paddingTop = getPaddingTop(); int paddingRight = getPaddingRight(); int paddingBottom = getPaddingBottom(); final int scrollX = getScrollX(); int decorCount = 0; //先计算Decor这类的view的位置,这种view一般都是固定的,不会随之viewPager去移动的, //这种使用的不多,略吧.... for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() != GONE) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); int childLeft = 0; int childTop = 0; if (lp.isDecor) {//如果是Decor final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; switch (hgrav) {//如果是垂直填充 default: childLeft = paddingLeft; break; case Gravity.LEFT: childLeft = paddingLeft; paddingLeft += child.getMeasuredWidth(); break; case Gravity.CENTER_HORIZONTAL: childLeft = Math.max((width - child.getMeasuredWidth()) / 2, paddingLeft); break; case Gravity.RIGHT: childLeft = width - paddingRight - child.getMeasuredWidth(); paddingRight += child.getMeasuredWidth(); break; } switch (vgrav) {//如果是水平填充, default: childTop = paddingTop; break; case Gravity.TOP: childTop = paddingTop; paddingTop += child.getMeasuredHeight(); break; case Gravity.CENTER_VERTICAL: childTop = Math.max((height - child.getMeasuredHeight()) / 2, paddingTop); break; case Gravity.BOTTOM: childTop = height - paddingBottom - child.getMeasuredHeight(); paddingBottom += child.getMeasuredHeight(); break; } //累计scrollx,可以看到,随着scroll移动,childLeft的位置是会跟着移动,以达到 //Decor保持在屏幕原来的位置; childLeft += scrollX; child.layout(childLeft, childTop, childLeft + child.getMeasuredWidth(), childTop + child.getMeasuredHeight()); decorCount++; } } } //如果没有Decor就是除去viewpager自己的左右padding,这个宽度就是child的宽度啦。 final int childWidth = width - paddingLeft - paddingRight; //真正开始布局我们的page了; for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() != GONE) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); ItemInfo ii; //从缓存中找到对应的view.因为有offset的数值呀,child中没有哦; if (!lp.isDecor && (ii = infoForChild(child)) != null) { //看到了吗,offset乘以childWidth,来计算当前page的偏移量。 int loff = (int) (childWidth * ii.offset); //每个page的left等于paddingleft + 自己的偏移量 int childLeft = paddingLeft + loff; int childTop = paddingTop; //当在第一次Populate时候,添加的子page,那个时候创建的page添加进去的page //的needsMeasure是true. 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; //这个其实在onMeasure中已经测量过了,这里没有必要重测 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()); //根据他的left, top,然后测量的宽高就可以给page布局啦 child.layout(childLeft, childTop, childLeft + child.getMeasuredWidth(), childTop + child.getMeasuredHeight()); } } } mTopPageBounds = paddingTop; mBottomPageBounds = height - paddingBottom; mDecorChildCount = decorCount; //第一次布局会在这里滚动到指定位置; if (mFirstLayout) { scrollToItem(mCurItem, false, 0, false); } mFirstLayout = false; }
- 总结一下: 布局其实就是利用前面计算的page偏移量来和page的测量宽度来布局子page的位置哦。这里说下偏移量,比如第一个page的偏移量是0, 那么第二个page的偏移量就是第一个page的width + margiin , 后面的page就这样累计叠加。布局里面虽然有测量,但我认为这只是一个安全措施,第一次应该已经实现了对子page的测量了。这里的测量结果和前面应该是一致的。
3. 绘制的地方
- onDraw:主要是绘制view本身,因为Viewpager本身并没有什么东西,他的子view由子child本身绘制。但是当我们设置了marginDrawable的时候,这个drawable就要由我们的ViewPager来绘制啦,我们看看他的实现。
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 存在着margin,并且设定了drawable.
if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0
&& mAdapter != null) {
final int scrollX = getScrollX();
final int width = getWidth();
// 计算margin与viewager的宽度的比例
final float marginOffset = (float) mPageMargin / width;
int itemIndex = 0;
// 取出缓存的第一个子View.
ItemInfo ii = mItems.get(0);
float offset = ii.offset;
final int itemCount = mItems.size();
final int firstPos = ii.position;
final int lastPos = mItems.get(itemCount - 1).position;
//遍历缓存中所有的view.
for (int pos = firstPos; pos < lastPos; pos++) {
// 这个写法其实有点不好看,意思就是不停地从mItems缓存中取出新的View.
while (pos > ii.position && itemIndex < itemCount) {
ii = mItems.get(++itemIndex);
}
float drawAt;
//通过view的宽度因子和左边的便宜来计算marginDrawable绘制的开始位置;
if (pos == ii.position) {
drawAt = (ii.offset + ii.widthFactor) * width;
offset = ii.offset + ii.widthFactor + marginOffset;
} else {
float widthFactor = mAdapter.getPageWidth(pos);
drawAt = (offset + widthFactor) * width;
offset += widthFactor + marginOffset;
}
if (drawAt + mPageMargin > scrollX) {
mMarginDrawable.setBounds((int) drawAt, mTopPageBounds,
(int) (drawAt + mPageMargin + 0.5f), mBottomPageBounds);
mMarginDrawable.draw(canvas);
}
//其实前面已经绘制过了,这个忽略的绘制本意却没有达到
if (drawAt > scrollX + width) {
break; // No more visible, no sense in continuing
}
}
}
}
说完了基本的测量、布局、绘制,就要来看看viewPager的内容滚动吧,毕竟这不只是一个静态的容器.
4. 事件的拦截与触摸消耗
-
onInterceptTouchEvent: 表示在什么情况下的用户操作,会将手势操作拦截下来给到我们viewpager来用的意思。 在一次手势中如果拦截成功后面就不会再触发该方法,如果没有拦截成功会不停地调用该方法来检测拦截策略.
public boolean onInterceptTouchEvent(MotionEvent ev) { final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; //4.1 up,cancel不拦截; //在View的拦截机制中啊, 如果发生了拦截,那么当次手势是不会再触发onInterceptTouchEvent啦 //来到这里,说明down,move事件都没有发生过拦截,这里cacel,up自然不要拦截啦, //其次这里主要做了一些viewpager任务清理工作. if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { // Release the drag. if (DEBUG) Log.v(TAG, "Intercept done!"); //清理工作; mIsBeingDragged = false; mIsUnableToDrag = false; mActivePointerId = INVALID_POINTER; if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } //down, move没有拦截,这里自然不会拦截。 return false; } //4.2, 如果是move事件, if (action != MotionEvent.ACTION_DOWN) { //虽然没拦截,但是vp如果在ontouch中被认为是拖拽了。这里就拦截下来了。毕竟也不一定拦截 //才能消耗的,如果vp没有子view或者子view不消耗,那么vp就有机会消耗啦呀。 if (mIsBeingDragged) { if (DEBUG) Log.v(TAG, "Intercept returning true!"); return true; } //如果之前是纵行滚动,当次手势是不会被Viewpager去拦截的; if (mIsUnableToDrag) { if (DEBUG) Log.v(TAG, "Intercept returning false!"); return false; } } //下面看看,如果第一次来到vp中,什么时候会主动拦截 switch (action) { case MotionEvent.ACTION_MOVE: { final int activePointerId = mActivePointerId; if (activePointerId == INVALID_POINTER) { // If we don't have a valid id, the touch down wasn't on content. 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)) { // Nested view has scrollable area under this point. Let it be handled there. mLastMotionX = x; mLastMotionY = y; mIsUnableToDrag = true; return false; } //在这里检测到拦截了,条件是横向的move达到了滚到阈值, //并且横向滚动值达到超过了纵向滚动的两倍; if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) { if (DEBUG) Log.v(TAG, "Starting drag!"); mIsBeingDragged = true; //申请父容器不要拦截vp requestParentDisallowInterceptTouchEvent(true); setScrollState(SCROLL_STATE_DRAGGING); mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop : mInitialMotionX - mTouchSlop; mLastMotionY = y; setScrollingCacheEnabled(true); } else if (yDiff > mTouchSlop) { //纵向滚动了,该次手势后面就不会让viewpager尝试拦截哦; //所以识别到了纵行滚动,该次就不会尝试viewPager拦截事件了; if (DEBUG) Log.v(TAG, "Starting unable to drag!"); //看这里; mIsUnableToDrag = true; } if (mIsBeingDragged) { // Scroll to follow the motion event if (performDrag(x)) { ViewCompat.postInvalidateOnAnimation(this); } } break; } case MotionEvent.ACTION_DOWN: {//down手势会拦截吗?会的啊 /* * Remember location of down touch. * ACTION_DOWN always refers to pointer index 0. */ mLastMotionX = mInitialMotionX = ev.getX(); mLastMotionY = mInitialMotionY = ev.getY(); mActivePointerId = MotionEventCompat.getPointerId(ev, 0); //down说明是一次新的手势啦,要清除掉请面的纵向滚动标记; mIsUnableToDrag = false; mScroller.computeScrollOffset(); //当前viewPager还在滚动没停下来,还没靠边,down手势下来了,就要拦截呢。 if (mScrollState == SCROLL_STATE_SETTLING && Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough){ // 终止滚动 mScroller.abortAnimation(); mPopulatePending = false; //计算页面 populate(); //当前要拦截了 mIsBeingDragged = true; //请求父容器不要拦截。这里可以看出vp后面来消耗滚动事件,因此就让他的父容器不要 //拦截后续的move事件,让他们能顺利地来到vp中. requestParentDisallowInterceptTouchEvent(true); //装态为dragging 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); //vp认为是否是拖拽,是否要拦截下来 return mIsBeingDragged; }
-
总结一下,啥情况下会拦截呢, 我认为主要有以下三种情况:
1. 在down事件的时候,一般情况呢,view体系的拦截策略是不应该在down中设定的,因为在down事件中拦截的话,后续子view请求父容器不要拦截是无效的, 这样就限定了子view的功能了。但是我们的ViewPager中却在这拦截了: 如果当前还在滚动状态并且还没靠边,手势down来了, 那么就要拦截下来,这个时候就vp就想自己使用后面的move, up事件了,而且子view也不可能在档次手势中有机会使用了。
2. 在move事件时候, 如果move达到了滚到阈值,并且横向滚动值达到超过了纵向滚动的两倍,就会将事件拦截下来自己使用了。
3. 如果vp的子view不消耗相应滚动, 在vp的onTouchEvent中消耗了滚动事件,并且认为是横向拖拽那么这里就会直接拦截下来,不做多余地判断;
-
-
onTouchEvent: Viewpager对滚动事件的消耗,主要逻辑是处理页面的滚动,滚动计算和发起都在这里面;
//当事件由vp消耗有两种可能,其一是被vp拦截,其二vp的子view不能消耗对应的事件。 public boolean onTouchEvent(MotionEvent ev) { ...... //没有子pager,那还滚动啥子哟,直接返回false; if (mAdapter == null || mAdapter.getCount() == 0) { // Nothing to present or scroll; nothing to touch. return false; } //构建速度拾取器,通过它可以获得手势的速度,坐标点等 if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } //跟踪手势,为了计算速度; mVelocityTracker.addMovement(ev); final int action = ev.getAction(); boolean needsInvalidate = false; switch (action & MotionEventCompat.ACTION_MASK) { case MotionEvent.ACTION_DOWN: { //手指落下就要终止滚动 mScroller.abortAnimation(); mPopulatePending = false; //重新计算页面量 populate(); // 记录down位置的x, y位置; mLastMotionX = mInitialMotionX = ev.getX(); mLastMotionY = mInitialMotionY = ev.getY(); mActivePointerId = MotionEventCompat.getPointerId(ev, 0); break; } case MotionEvent.ACTION_MOVE:// if (!mIsBeingDragged) {//走到这里说明并未发生拦截,且vp子view不能来识别这个 //down-move事件。那就上报给我们的vp来处理了,后面肯定要将mIsBeingDragged设定 //true表示vp自己来处理滚动事件。 final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); 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!"); //vp拖拽状态,后面的move就直接来使用 mIsBeingDragged = true; //move已经达到了vp可以识别的滚动了,那么就告诉父容器后面的滚动事件就不能拦截了 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); } } } // 少废话,这里执行拖拽动作; if (mIsBeingDragged) { // Scroll to follow the motion event final int activePointerIndex = MotionEventCompat.findPointerIndex( ev, mActivePointerId); final float x = MotionEventCompat.getX(ev, activePointerIndex); //当viewPager滚动page的时候就是通过performDrag来实现滚动到当前滑动的位置的; needsInvalidate |= performDrag(x); } break; case MotionEvent.ACTION_UP://这里主要是根据vp滑动的位置来计算最后要滚到vp的哪个子page if (mIsBeingDragged) {//只有是vp识别的拖拽,才会计算vp最后停靠的页面。 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(); //计算出的这个item是vp显示区域最左边的page final ItemInfo ii = infoForCurrentScrollPosition(); //在viewpager中显示的最左边的page final int currentPage = ii.position; //计算当前scrollX在当前page中的偏移与当前page的with的比例,来看看后面该滚动到哪一 //页。 final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor; final int activePointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); final float x = MotionEventCompat.getX(ev, activePointerIndex); //从down-up过程中移动的距离 final int totalDelta = (int) (x - mInitialMotionX); //判断即将停靠的页面 int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, totalDelta); //设置滚动到对应的页面; setCurrentItemInternal(nextPage, true, true, initialVelocity); mActivePointerId = INVALID_POINTER; endDrag(); needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease(); } break; .... //viewpager默认都返回true;就是说滚动事件只要来到我身上了,那么我肯定不会拒绝的,来吧! return true; }
-
infoForCurrentScrollPosition: 这个函数看得有点烦,在前面手指抬起的时候会计算出当前vp最左边界处出现的page的缓存对象,就是通过这个方法来实现的。绘个图吧:
-
上面情形一,向右滑动,计算出来的item是page0, 它是vp左边界中显示的页面。情形二,向左滑动,计算出来的item是page1.
- determineTargetPage:顾名思义就是计算出手指抬起后,vp将要停靠的页面; 看下实现吧。
//currentPage指的是vp最左边对应的页面哦,不是当前mCurItem哦;
private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) {
int targetPage;
//这是快速滑动的判断,当速度达到了滑翔条件(view往右滑动速度为负,向左滑动速度才是正数。)
if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
//向左快速滑的话,就停靠在当前vp左边界的page位置。向右快速滑,就停靠在下一个页面上。
//参照上图,向右快速滑停靠的页面是page0,向左快速滑动停靠的页面是page2
targetPage = velocity > 0 ? currentPage : currentPage + 1;
} else {
//从这里看到,如果往右边滑动,truncator = 0.4f,要想选中下一个page,必须要划过下一个page
//0.6的宽度因子哦;如果往左边滑动currentPage会小于mCurItem,那么必须也要划出来0.6因子
//那么余下的pageOffset会小于0.4,这样家起来小于1,会跳到前面的页面;
final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f;
targetPage = (int) (currentPage + pageOffset + truncator);
}
if (mItems.size() > 0) {//这里是确保page都是我们缓存中的page.
final ItemInfo firstItem = mItems.get(0);
final ItemInfo lastItem = mItems.get(mItems.size() - 1);
// Only let the user target pages we have items for
targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position));
}
return targetPage;
}
5. page页面的滚动处理:
-
当手指慢慢滑动,页面需要跟随手指去滑动,它是由 performDrag 来负责的, 来看源代码吧:
//参数x是将要滚动到的x坐标; private boolean performDrag(float x) { boolean needsInvalidate = false; //需要滚动的距离 final float deltaX = mLastMotionX - x; mLastMotionX = x; float oldScrollX = getScrollX(); //计算最终的scrollX, vp的滚动是通过scoll内容来实现的哦; float scrollX = oldScrollX + deltaX; final int width = getClientWidth(); //这里的firstoffset并不是指第一个全局的page,而是内存中缓存的第一个page,mLastOffset同理如此; float leftBound = width * mFirstOffset; 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) {//如果不是第一个全局page. leftAbsolute = false;//就不会绘制边缘拖拽效果 leftBound = firstItem.offset * width; } if (lastItem.position != mAdapter.getCount() - 1) {//如果不是最后一个全局page. 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; } mLastMotionX += scrollX - (int) scrollX; //通过View.scollTo来滚动到指定的位置;触发之后,系统会不停地调用我们vp中重写的computeScroll //方法,在该方法中会调用completeScroll(true),他做了一件重要的事情,就是 //重新计算内存中应该缓存的page,即populate方法触发。 scrollTo((int) scrollX, getScrollY()); //这里会不停地回调onPageScrolled,告诉使用者当前在滚动的位置是多少..... pageScrolled((int) scrollX); //返回数值表示是否需要重绘制,即调用vp自身的onDaw方法。从前面看到只有到达了边缘才需要重绘制,难道 //我们滚动的时候不需要重新绘制ui吗,不符合view绘制策略呀。实际上vp的ondraw只负责marginDrawable //和边缘滚动效果,vp自身内容的绘制是交给View来做的,所以在边缘触发只是绘制边缘效果。其他的绘制会在 //scrollTo中主动触发呢。 return needsInvalidate; }
- 总结一下performDrag方法吧: 当在滚动过程中,即onTouch的move中会不停地调用该方法来实现内容的滚动,它根据手势的位置计算滚动的距离,然后还会不断地去计算内存中应该重新存储哪些新的page页面。这就是他的主要目的啦......
-
手动设置滚动的页面或者手指抬起要停靠的页面,由 setCurrentItemInternal,setCurrentItem这类方法族来实现, 在onTouchEvent中的手指抬起的时候会有这么一段,
//等待计算page内存页 mPopulatePending = true; //计算抬起手指后要滚动到的页面 int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, totalDelta); //设置滚动到对应的页面; setCurrentItemInternal(nextPage, true, true, initialVelocity);
来看看setCurrentItemInternal的源码吧:
//决定是否回调onPageSelected方法,可以看出只有不等的时候才会回调,因此 //第一次显示page时候是不会调的哦; final boolean dispatchSelected = mCurItem != item; if (mFirstLayout) { mCurItem = item; if (dispatchSelected && mOnPageChangeListener != null) { mOnPageChangeListener.onPageSelected(item); } if (dispatchSelected && mInternalPageChangeListener != null) { mInternalPageChangeListener.onPageSelected(item); } requestLayout(); } else { //重新计算page内存页面集合,但是由于前面mPopulatePending=true,up这里其实会跳过内部的计算的。 populate(item); //滚动到特定的页面,这里会利用到Vp自带的Scroller去实现平滑滚动效果; scrollToItem(item, smoothScroll, velocity, dispatchSelected); }
继续来看看scollToItem怎么来实现滚动页面的吧:
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) {//up手势走的是这里; //根据距离和初速度来实现平滑地滚动; smoothScrollTo(destX, 0, velocity); if (dispatchSelected && mOnPageChangeListener != null) { //告诉使用者我们的变化到了哪个页面; mOnPageChangeListener.onPageSelected(item); } if (dispatchSelected && mInternalPageChangeListener != null) { mInternalPageChangeListener.onPageSelected(item); } } else {//非平滑滚动 if (dispatchSelected && mOnPageChangeListener != null) { mOnPageChangeListener.onPageSelected(item); } if (dispatchSelected && mInternalPageChangeListener != null) { mInternalPageChangeListener.onPageSelected(item); } completeScroll(false); //调用View.scrollTo来实现滚动 scrollTo(destX, 0); pageScrolled(destX); } }
来吧,接着看smoothScrollTo方法, 看他怎么来实现平滑滚动:
void smoothScrollTo(int x, int y, int velocity) { ....... //如果已经滚动结束了,就设置SCROLL_STATE_IDLE状态, 然后使用populate计算内存页 //如果还没到滚动结束点呢? if (dx == 0 && dy == 0) { completeScroll(false); populate(); setScrollState(SCROLL_STATE_IDLE); return; } setScrollingCacheEnabled(true); //设置滚动状态SCROLL_STATE_SETTLING,表示还在自己滑动 setScrollState(SCROLL_STATE_SETTLING); //下面就是计算慢性滑动的时间,最终的x,y坐标: 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 = 0; //根据速度来计算时间 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); //调用辅助来Scoller来计算不同时间的坐标 mScroller.startScroll(sx, sy, dx, dy, duration); //发命令给系统做重绘制操作,系统接着会调用computeScroll方法,来根据滚动位置来滑动内容到指定位置; ViewCompat.postInvalidateOnAnimation(this); }
来吧,来看看ViewPager重写的computeScroll方法;
public void computeScroll() { //当是我们的滚动Scroller来负责计算,这里如果还没有滚动结束 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); } } // 执行重新绘制操作,这里保证边缘效果能有机会绘制,vp的滚动位置绘制由scrollTo //自己去负责的; ViewCompat.postInvalidateOnAnimation(this); return; } // 如果滚动结束了,那么要干什么呢? completeScroll(true); }
继续,快结束了, completeScroll:
private void completeScroll(boolean postEvents) { //如果是还在滚动状态,就要计算page内存内容啦; boolean needPopulate = mScrollState == SCROLL_STATE_SETTLING; ....... mPopulatePending = false; for (int i=0; i
看看mEndScrollRunnable实现,在前面手指抬起的时候,我们其实是没有计算内存中的page页的,有一个mPopulatePending状态跳过了实际计算,所以在最后页面滚动结束的时候来一次最终的计算,就是在这里了。
private final Runnable mEndScrollRunnable = new Runnable() { public void run() { //设置SCROLL_STATE_IDLE状态 setScrollState(SCROLL_STATE_IDLE); //计算内存中的page缓存内容; populate(); } };
6. 存在的问题
- 当同时设置viewpager的padding和page item之间的margin, page的marginDrawable会绘制在错误的地方,他累计了对应的对应的padding,这是错误的计算;
2. 在ScrollView中直接使用viewager,宽高不生效。原因是ScrollView给子view的测量规格模式是UNSPECIFIED,而我们的Viewpager测量又是setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), etDefaultSize(0, heightMeasureSpec))
组合。解决也不是很难,只不过要针对不同的模式进行自定义测量策略,后面如果有时间,综合写一下系统控件各种测量存在的问题吧....