欣賞一下
功能點
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<getChildCount(); i++) { View child = getChildAt(i); ii = infoForChild(child); if (ii != null && ii.position == mCurItem) { if (child.requestFocus(focusDirection)) { break; } } } } } }
- 總結一下下: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的緩存對象,就是通過這個方法來實現的。繪個圖吧:
vp_滑翔滾動圖.png
-
上面情形一,向右滑動,計算出來的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<mItems.size(); i++) { ItemInfo ii = mItems.get(i); if (ii.scrolling) { needPopulate = true; ii.scrolling = false; } } //這下面兩個,一個是觸發重繪,一個不是,但是都要執行mEndScrollRunnable,這個就是 //調用我們的populate大法了,真不容易。 if (needPopulate) { if (postEvents) { ViewCompat.postOnAnimation(this, mEndScrollRunnable); } else { mEndScrollRunnable.run(); } } }
看看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))
組合。解決也不是很難,只不過要針對不同的模式進行自定義測量策略,后面如果有時間,綜合寫一下系統控件各種測量存在的問題吧....