欣賞一下
功能點
API 21
- 測量根穷,布局柏靶,繪制;
- 事件的處理機(jī)制, viewPager的主動消耗物邑,攔截等;
- 頁面滾動計算溜哮,手動滾動;
- viewPager設(shè)計帶來的問題;
0. 核心變量和標(biāo)記
- mItems: 已經(jīng)緩存過的page, 按照page的position從小到大來排列。
- mCurItem: 當(dāng)前顯示的page的position, 這是全局的色解。全局是針對mItems來說的.假如有5個page茂嗓,
mItems存儲的可能是最后的三個頁面,那他緩存的第一個頁面并不是系統(tǒng)中的第一個page科阎,而是全局的第三個page.
- mAdapter: 動態(tài)加載子page述吸。
- ItemInfo: page控件構(gòu)建的對象,里面的position即為全局page的position锣笨。
- mOffscreenPageLimit: 離屏限制數(shù)量蝌矛,默認(rèn)是1,也就是除了當(dāng)前page左右緩存各一個,總數(shù)是3;如果是2,那么就左右各緩存兩個错英,總數(shù)是5入撒。
- Scroller: 一個平滑滾動效果的計算工具類,類似的有Overscroller.他是根據(jù)起始坐標(biāo)椭岩,終點坐標(biāo)茅逮,以及時間這幾個變量來計算不同時間的view的x, y坐標(biāo)的哦,從而實現(xiàn)滾動計算。
1. 測量:
ViewPager我們一般是不會在它的內(nèi)部主動添加子view的判哥,而是通過Adapter的形式去動態(tài)注入献雅。其實除此之外,他還可以在xml添加他的DecorView, 這種特殊的view和adapter中添加的view的測量塌计,布局都是不一樣挺身,他一般是固定在viewPager的頁面中的不像page view一樣隨著手勢滾動,比如ViewPager的indicator這種就是DecorView锌仅。
-
onMeasure: 測量
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //簡單的一行代碼告訴了我瞒渠,這viewPager的大小在這里就已經(jīng)確定了良蒸。 //如果viewpager是wrap和match的結(jié)果都一樣就是父容器剩下的寬高,如果是設(shè)定了dimense那 //就是他自己的dimense寬高了。viewPager的這種設(shè)定就和通常的控件測量不一樣了伍玖,他完全忽略了自己的 //pageview自己設(shè)定的寬與高了嫩痰,這種設(shè)計存在這一些缺陷. //比如不要輕易地將viewPager放到ScrollView中,你會發(fā)現(xiàn)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的測量規(guī)則大概是這樣的椰棘,如果是top/bottom就是橫向填充纺棺,寬的規(guī)格mode是 //EXACTLY, 規(guī)格size根據(jù)match/wrap,dimense來定邪狞。也就是如果size是 //match,wrap, 他們最后的規(guī)格尺寸都是一樣的即viewpager的可用寬度祷蝌。dimense就是 //設(shè)定的寬。高的規(guī)格mode則要根據(jù)layoutParams來定帆卓,如果不是wrap,那么就是 //EXACTLY, 是就是AT_MOST,size在match/wrap的狀態(tài)下都一樣的巨朦。所以呢,他這個測量 //原則和標(biāo)準(zhǔn)的測量行為是保持一致的剑令。一個方向的規(guī)格在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的測量規(guī)格之后吁津,就可以對Decor進(jìn)行測量啦; child.measure(widthSpec, heightSpec); if (consumeVertical) { //剩余的高就是page view的高度 childHeightSize -= child.getMeasuredHeight(); } else if (consumeHorizontal) { //剩余的寬就是page view的寬 childWidthSize -= child.getMeasuredWidth(); } } } } //看到了棚蓄,這就是page view的測量規(guī)格,這里已經(jīng)確定了碍脏,他和具體的page view所設(shè)定的尺寸 //沒有半毛錢的關(guān)系梭依,全靠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; //這里是再次確定下要創(chuàng)建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); } } } }
- 總結(jié)一下侧到, 在測量的時候勃教,一開始是沒有子page view的,所以需要調(diào)用populate來創(chuàng)建和加載子page view, 然后才能測量子page匠抗。所以測量的功能大體分為三步驟:一先測量ViewPager大小, 二加載子page, 三測量子page故源。
-
populate:創(chuàng)建和銷毀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對應(yīng)的page, 這些都緩存在了mItems中呢汞贸。 oldCurInfo = infoForPosition(mCurItem); mCurItem = newCurrentItem; } if (mAdapter == null) {//如果沒設(shè)置adapter, 那么就沒法創(chuàng)建和加載子page啦绳军,簡單地排下Decor //順序印机,然后跳過啦. sortChildDrawingOrder(); return; } //這個是在當(dāng)用戶抬起的手指的時候,page還在計算滾動门驾,我們不去創(chuàng)建和更改子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的一個回調(diào)奶是,用來告訴外界楣责,已經(jīng)開始加載子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); //這是保證當(dāng)更新了adapter的數(shù)據(jù)之后聂沙,你要手動地去notifyDataSetChanged,否則數(shù)據(jù)不會更新; 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對應(yīng)的position, 而是items集合中存儲的位置秆麸,這個要和mCurItem區(qū)分開來哦 int curIndex = -1; //curItem,當(dāng)前要display的page. ItemInfo curItem = null; //在mItems尋找當(dāng)前display的page for (curIndex = 0; curIndex < mItems.size(); curIndex++) { final ItemInfo ii = mItems.get(curIndex); if (ii.position >= mCurItem) {//如果我寫的話肯定就是直接判等及汉,但是沒這樣好沮趣,這樣大于的 //時候會立即終止遍歷沒必要啦。如果都小于mCurItem就在最后一個位置加上新的item. //算是效率上的一次小優(yōu)化吧坷随,值得學(xué)習(xí) if (ii.position == mCurItem) curItem = ii; break; } } //只要當(dāng)前curItem為null, page view設(shè)置了數(shù)量,創(chuàng)建新的item,添加到mItems集合中相應(yīng)的位置中去呢; //這種一般是在第一次創(chuàng)建的時候才有的房铭,后面就不會走的了.... if (curItem == null && N > 0) { curItem = addNewItem(mCurItem, curIndex); } //接下來就是根據(jù)當(dāng)前的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; //從當(dāng)前的page前一個開始往左遍歷芍殖,在全局的position為0停下來豪嗽,這樣當(dāng)當(dāng)前page是0時候,就不會再 //浪費時間往前去排查了豌骏。 for (int pos = mCurItem - 1; pos >= 0; pos--) { //除了限定因子龟梦,還有一個我們設(shè)置的mOffscreenPageLimit哦 if (extraWidthLeft >= leftWidthNeeded && pos < startPos) { //當(dāng)滿足了限定,并且位置是小于了最左邊的position,就需要destory了; //如果查到前面有null了窃躲,說明這個位置已經(jīng)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,那么就要創(chuàng)建了哦 //沒有需要的page是一般因為itemIndex為-1,當(dāng)前緩存的最左邊的就是當(dāng)前page,所以需要 //在0位置上再添加一個page.也還有可能是不符合的page,那么也要添加一個page躁倒,因此要在 //itemIndex偏移一個位置。 ii = addNewItem(pos, itemIndex + 1); extraWidthLeft += ii.widthFactor; //當(dāng)前page在mItems位置增加1 curIndex++; //取出當(dāng)前不符合的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; } } } //這是最后一個難度計算了;快結(jié)束了..... calculatePageOffsets(curItem, curIndex, oldCurInfo); } ......略 //告訴外面當(dāng)前display的page是誰象迎。 mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null); //到這里,添加頁面刪除頁面的動作就結(jié)束了啦呛踊。后面的東西和添加刪除page關(guān)系不是太大; mAdapter.finishUpdate(this); // 這里是用來設(shè)定child的布局參數(shù)的砾淌,因為child的布局參數(shù)是源自于pageadpter的設(shè)定的;所以在讀取了 //adapter內(nèi)容之后啦撮,這里要把他的widthFactor和position給到LayoutParams中去,以便后續(xù)使用 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放在當(dāng)前顯示的page的結(jié)構(gòu)樹上; 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; } } } } } }
- 總結(jié)一下下:populate的實現(xiàn)比較繁瑣略帶復(fù)雜赃春,但是他的目的是很單純的,就是在初次加載page或者滑動viewpager的時候在布局容器中加載對應(yīng)的子page, 同時刪除超過限定位置的page,以達(dá)到內(nèi)存的優(yōu)化啦骄崩。比如我們限定的mOffscreenPageLimit是1, 那么內(nèi)存中緩存的就是3個page, 我們會計算出當(dāng)前page的左右兩個緩存起來的聘鳞,其他的頁面刪除掉。隨著頁面的滾動要拂,動態(tài)更新緩存內(nèi)容page.
-
addNewItem: 添加子Item元素
//創(chuàng)建新的item到mItems集合中去;position是page在所有的page中對應(yīng)的位置抠璃,全局。index是在mItems中緩存的位置脱惰。 ItemInfo addNewItem(int position, int index) { ItemInfo ii = new ItemInfo(); ii.position = position; //看到?jīng)]搏嗡,這里是調(diào)用我們復(fù)寫adapter的instantiateItem來創(chuàng)建子page的哦; ii.object = mAdapter.instantiateItem(this, position); //也是調(diào)用我們的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-當(dāng)前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; //其實下面的邏輯是計算當(dāng)前的page和原來顯示的page之間的page的offset偏移量 // 如果當(dāng)前是向左側(cè)滑動 if (oldCurPosition < curItem.position) { int itemIndex = 0; ItemInfo ii = null; //當(dāng)前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同樣累加上當(dāng)前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--; } //當(dāng)前page的offset = 后一個page的offset - 當(dāng)前page的width因子- margin因子 offset -= ii.widthFactor + marginOffset; ii.offset = offset; } } } //接下來計算所有的緩存的page的偏移因子;根據(jù)前面的原則采盒,難度也不大。 // 除此之外蔚润,還計算了第一個緩存的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; }
- 簡單總結(jié)一下, page的offset計算也是挺繁瑣的嫡纠,這個玩意是來干嘛的, 有什么用呢烦租? 它其實是用來布局子page元素的,定位每個page的位置除盏,每個page的定位都和前面的page息息相關(guān)叉橱,這里用每個page的offset來標(biāo)識。接下來看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的位置是會跟著移動探膊,以達(dá)到 //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; //從緩存中找到對應(yīng)的view.因為有offset的數(shù)值呀,child中沒有哦; if (!lp.isDecor && (ii = infoForChild(child)) != null) { //看到了嗎殴蹄,offset乘以childWidth究抓,來計算當(dāng)前page的偏移量猾担。 int loff = (int) (childWidth * ii.offset); //每個page的left等于paddingleft + 自己的偏移量 int childLeft = paddingLeft + loff; int childTop = paddingTop; //當(dāng)在第一次Populate時候,添加的子page,那個時候創(chuàng)建的page添加進(jìn)去的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中已經(jīng)測量過了刺下,這里沒有必要重測 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()); //根據(jù)他的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; }
- 總結(jié)一下: 布局其實就是利用前面計算的page偏移量來和page的測量寬度來布局子page的位置哦。這里說下偏移量橘茉,比如第一個page的偏移量是0, 那么第二個page的偏移量就是第一個page的width + margiin , 后面的page就這樣累計疊加工腋。布局里面雖然有測量,但我認(rèn)為這只是一個安全措施畅卓,第一次應(yīng)該已經(jīng)實現(xiàn)了對子page的測量了擅腰。這里的測量結(jié)果和前面應(yīng)該是一致的。
3. 繪制的地方
- onDraw:主要是繪制view本身翁潘,因為Viewpager本身并沒有什么東西趁冈,他的子view由子child本身繪制。但是當(dāng)我們設(shè)置了marginDrawable的時候拜马,這個drawable就要由我們的ViewPager來繪制啦渗勘,我們看看他的實現(xiàn)。
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 存在著margin俩莽,并且設(shè)定了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);
}
//其實前面已經(jīng)繪制過了,這個忽略的繪制本意卻沒有達(dá)到
if (drawAt > scrollX + width) {
break; // No more visible, no sense in continuing
}
}
}
}
說完了基本的測量扮超、布局取刃、繪制,就要來看看viewPager的內(nèi)容滾動吧瞒津,畢竟這不只是一個靜態(tài)的容器.
4. 事件的攔截與觸摸消耗
-
onInterceptTouchEvent: 表示在什么情況下的用戶操作蝉衣,會將手勢操作攔截下來給到我們viewpager來用的意思。 在一次手勢中如果攔截成功后面就不會再觸發(fā)該方法巷蚪,如果沒有攔截成功會不停地調(diào)用該方法來檢測攔截策略.
public boolean onInterceptTouchEvent(MotionEvent ev) { final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; //4.1 up,cancel不攔截; //在View的攔截機(jī)制中啊病毡, 如果發(fā)生了攔截,那么當(dāng)次手勢是不會再觸發(fā)onInterceptTouchEvent啦 //來到這里屁柏,說明down,move事件都沒有發(fā)生過攔截啦膜,這里cacel,up自然不要攔截啦, //其次這里主要做了一些viewpager任務(wù)清理工作. 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中被認(rèn)為是拖拽了八拱。這里就攔截下來了。畢竟也不一定攔截 //才能消耗的,如果vp沒有子view或者子view不消耗肌稻,那么vp就有機(jī)會消耗啦呀清蚀。 if (mIsBeingDragged) { if (DEBUG) Log.v(TAG, "Intercept returning true!"); return true; } //如果之前是縱行滾動,當(dāng)次手勢是不會被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達(dá)到了滾到閾值, //并且橫向滾動值達(dá)到超過了縱向滾動的兩倍; 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說明是一次新的手勢啦腹泌,要清除掉請面的縱向滾動標(biāo)記; mIsUnableToDrag = false; mScroller.computeScrollOffset(); //當(dāng)前viewPager還在滾動沒停下來,還沒靠邊嘶卧,down手勢下來了,就要攔截呢真屯。 if (mScrollState == SCROLL_STATE_SETTLING && Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough){ // 終止?jié)L動 mScroller.abortAnimation(); mPopulatePending = false; //計算頁面 populate(); //當(dāng)前要攔截了 mIsBeingDragged = true; //請求父容器不要攔截脸候。這里可以看出vp后面來消耗滾動事件,因此就讓他的父容器不要 //攔截后續(xù)的move事件绑蔫,讓他們能順利地來到vp中. requestParentDisallowInterceptTouchEvent(true); //裝態(tài)為dragging setScrollState(SCROLL_STATE_DRAGGING); } else {//非上述情況就不攔截了运沦,也是默認(rèn)處理。 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認(rèn)為是否是拖拽烈掠,是否要攔截下來 return mIsBeingDragged; }
-
總結(jié)一下,啥情況下會攔截呢, 我認(rèn)為主要有以下三種情況:
1. 在down事件的時候缸托,一般情況呢左敌,view體系的攔截策略是不應(yīng)該在down中設(shè)定的,因為在down事件中攔截的話俐镐,后續(xù)子view請求父容器不要攔截是無效的, 這樣就限定了子view的功能了矫限。但是我們的ViewPager中卻在這攔截了: 如果當(dāng)前還在滾動狀態(tài)并且還沒靠邊,手勢down來了, 那么就要攔截下來佩抹,這個時候就vp就想自己使用后面的move, up事件了叼风,而且子view也不可能在檔次手勢中有機(jī)會使用了。
2. 在move事件時候棍苹, 如果move達(dá)到了滾到閾值,并且橫向滾動值達(dá)到超過了縱向滾動的兩倍,就會將事件攔截下來自己使用了无宿。
3. 如果vp的子view不消耗相應(yīng)滾動, 在vp的onTouchEvent中消耗了滾動事件,并且認(rèn)為是橫向拖拽那么這里就會直接攔截下來枢里,不做多余地判斷;
-
-
onTouchEvent: Viewpager對滾動事件的消耗孽鸡,主要邏輯是處理頁面的滾動蹂午,滾動計算和發(fā)起都在這里面;
//當(dāng)事件由vp消耗有兩種可能,其一是被vp攔截彬碱,其二vp的子view不能消耗對應(yīng)的事件画侣。 public boolean onTouchEvent(MotionEvent ev) { ...... //沒有子pager,那還滾動啥子喲,直接返回false; if (mAdapter == null || mAdapter.getCount() == 0) { // Nothing to present or scroll; nothing to touch. return false; } //構(gòu)建速度拾取器堡妒,通過它可以獲得手勢的速度,坐標(biāo)點等 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: { //手指落下就要終止?jié)L動 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) {//走到這里說明并未發(fā)生攔截,且vp子view不能來識別這個 //down-move事件桑寨。那就上報給我們的vp來處理了伏尼,后面肯定要將mIsBeingDragged設(shè)定 //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); //當(dāng)達(dá)到了我們的閾值尉尾,橫向大于縱向.那么我們就認(rèn)為是拖拽爆阶,這個比攔截的條件松, //畢竟我是第一個處理的沙咏,就不需要等到是縱行的兩倍在認(rèn)為是拖拽; if (xDiff > mTouchSlop && xDiff > yDiff) { if (DEBUG) Log.v(TAG, "Starting drag!"); //vp拖拽狀態(tài)辨图,后面的move就直接來使用 mIsBeingDragged = true; //move已經(jīng)達(dá)到了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); } } } // 少廢話肢藐,這里執(zhí)行拖拽動作; if (mIsBeingDragged) { // Scroll to follow the motion event final int activePointerIndex = MotionEventCompat.findPointerIndex( ev, mActivePointerId); final float x = MotionEventCompat.getX(ev, activePointerIndex); //當(dāng)viewPager滾動page的時候就是通過performDrag來實現(xiàn)滾動到當(dāng)前滑動的位置的; needsInvalidate |= performDrag(x); } break; case MotionEvent.ACTION_UP://這里主要是根據(jù)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顯示區(qū)域最左邊的page final ItemInfo ii = infoForCurrentScrollPosition(); //在viewpager中顯示的最左邊的page final int currentPage = ii.position; //計算當(dāng)前scrollX在當(dāng)前page中的偏移與當(dāng)前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); //設(shè)置滾動到對應(yīng)的頁面; setCurrentItemInternal(nextPage, true, true, initialVelocity); mActivePointerId = INVALID_POINTER; endDrag(); needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease(); } break; .... //viewpager默認(rèn)都返回true;就是說滾動事件只要來到我身上了凑阶,那么我肯定不會拒絕的,來吧衷快! return true; }
-
infoForCurrentScrollPosition: 這個函數(shù)看得有點煩宙橱,在前面手指抬起的時候會計算出當(dāng)前vp最左邊界處出現(xiàn)的page的緩存對象,就是通過這個方法來實現(xiàn)的烦磁。繪個圖吧:
-
上面情形一养匈,向右滑動,計算出來的item是page0, 它是vp左邊界中顯示的頁面都伪。情形二呕乎,向左滑動,計算出來的item是page1.
- determineTargetPage:顧名思義就是計算出手指抬起后陨晶,vp將要外剩靠的頁面; 看下實現(xiàn)吧帝璧。
//currentPage指的是vp最左邊對應(yīng)的頁面哦,不是當(dāng)前mCurItem哦;
private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) {
int targetPage;
//這是快速滑動的判斷湿刽,當(dāng)速度達(dá)到了滑翔條件(view往右滑動速度為負(fù)的烁,向左滑動速度才是正數(shù)。)
if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
//向左快速滑的話,就驼┕耄靠在當(dāng)前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頁面的滾動處理:
-
當(dāng)手指慢慢滑動征唬,頁面需要跟隨手指去滑動捌显,它是由 performDrag 來負(fù)責(zé)的, 來看源代碼吧:
//參數(shù)x是將要滾動到的x坐標(biāo); private boolean performDrag(float x) { boolean needsInvalidate = false; //需要滾動的距離 final float deltaX = mLastMotionX - x; mLastMotionX = x; float oldScrollX = getScrollX(); //計算最終的scrollX, vp的滾動是通過scoll內(nèi)容來實現(xiàn)的哦; float scrollX = oldScrollX + deltaX; final int width = getClientWidth(); //這里的firstoffset并不是指第一個全局的page,而是內(nèi)存中緩存的第一個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來滾動到指定的位置;觸發(fā)之后非驮,系統(tǒng)會不停地調(diào)用我們vp中重寫的computeScroll //方法社露,在該方法中會調(diào)用completeScroll(true)逛钻,他做了一件重要的事情矗钟,就是 //重新計算內(nèi)存中應(yīng)該緩存的page,即populate方法觸發(fā)芋簿。 scrollTo((int) scrollX, getScrollY()); //這里會不停地回調(diào)onPageScrolled,告訴使用者當(dāng)前在滾動的位置是多少..... pageScrolled((int) scrollX); //返回數(shù)值表示是否需要重繪制罢低,即調(diào)用vp自身的onDaw方法贪薪。從前面看到只有到達(dá)了邊緣才需要重繪制媳禁,難道 //我們滾動的時候不需要重新繪制ui嗎,不符合view繪制策略呀画切。實際上vp的ondraw只負(fù)責(zé)marginDrawable //和邊緣滾動效果竣稽,vp自身內(nèi)容的繪制是交給View來做的,所以在邊緣觸發(fā)只是繪制邊緣效果霍弹。其他的繪制會在 //scrollTo中主動觸發(fā)呢毫别。 return needsInvalidate; }
- 總結(jié)一下performDrag方法吧: 當(dāng)在滾動過程中,即onTouch的move中會不停地調(diào)用該方法來實現(xiàn)內(nèi)容的滾動典格,它根據(jù)手勢的位置計算滾動的距離岛宦,然后還會不斷地去計算內(nèi)存中應(yīng)該重新存儲哪些新的page頁面。這就是他的主要目的啦......
-
手動設(shè)置滾動的頁面或者手指抬起要退=桑靠的頁面砾肺,由 setCurrentItemInternal挽霉,setCurrentItem這類方法族來實現(xiàn), 在onTouchEvent中的手指抬起的時候會有這么一段,
//等待計算page內(nèi)存頁 mPopulatePending = true; //計算抬起手指后要滾動到的頁面 int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, totalDelta); //設(shè)置滾動到對應(yīng)的頁面; setCurrentItemInternal(nextPage, true, true, initialVelocity);
來看看setCurrentItemInternal的源碼吧:
//決定是否回調(diào)onPageSelected方法,可以看出只有不等的時候才會回調(diào)变汪,因此 //第一次顯示page時候是不會調(diào)的哦; 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內(nèi)存頁面集合,但是由于前面mPopulatePending=true,up這里其實會跳過內(nèi)部的計算的侠坎。 populate(item); //滾動到特定的頁面,這里會利用到Vp自帶的Scroller去實現(xiàn)平滑滾動效果; scrollToItem(item, smoothScroll, velocity, dispatchSelected); }
繼續(xù)來看看scollToItem怎么來實現(xiàn)滾動頁面的吧:
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手勢走的是這里; //根據(jù)距離和初速度來實現(xiàn)平滑地滾動; 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); //調(diào)用View.scrollTo來實現(xiàn)滾動 scrollTo(destX, 0); pageScrolled(destX); } }
來吧裙盾,接著看smoothScrollTo方法, 看他怎么來實現(xiàn)平滑滾動:
void smoothScrollTo(int x, int y, int velocity) { ....... //如果已經(jīng)滾動結(jié)束了实胸,就設(shè)置SCROLL_STATE_IDLE狀態(tài), 然后使用populate計算內(nèi)存頁 //如果還沒到滾動結(jié)束點呢? if (dx == 0 && dy == 0) { completeScroll(false); populate(); setScrollState(SCROLL_STATE_IDLE); return; } setScrollingCacheEnabled(true); //設(shè)置滾動狀態(tài)SCROLL_STATE_SETTLING番官,表示還在自己滑動 setScrollState(SCROLL_STATE_SETTLING); //下面就是計算慢性滑動的時間童芹,最終的x,y坐標(biāo): 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; //根據(jù)速度來計算時間 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); //調(diào)用輔助來Scoller來計算不同時間的坐標(biāo) mScroller.startScroll(sx, sy, dx, dy, duration); //發(fā)命令給系統(tǒng)做重繪制操作,系統(tǒng)接著會調(diào)用computeScroll方法鲤拿,來根據(jù)滾動位置來滑動內(nèi)容到指定位置; ViewCompat.postInvalidateOnAnimation(this); }
來吧,來看看ViewPager重寫的computeScroll方法;
public void computeScroll() { //當(dāng)是我們的滾動Scroller來負(fù)責(zé)計算署咽,這里如果還沒有滾動結(jié)束 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); } } // 執(zhí)行重新繪制操作近顷,這里保證邊緣效果能有機(jī)會繪制,vp的滾動位置繪制由scrollTo //自己去負(fù)責(zé)的; ViewCompat.postInvalidateOnAnimation(this); return; } // 如果滾動結(jié)束了宁否,那么要干什么呢窒升? completeScroll(true); }
繼續(xù),快結(jié)束了, completeScroll:
private void completeScroll(boolean postEvents) { //如果是還在滾動狀態(tài)慕匠,就要計算page內(nèi)存內(nèi)容啦; 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; } } //這下面兩個饱须,一個是觸發(fā)重繪,一個不是台谊,但是都要執(zhí)行mEndScrollRunnable蓉媳,這個就是 //調(diào)用我們的populate大法了,真不容易锅铅。 if (needPopulate) { if (postEvents) { ViewCompat.postOnAnimation(this, mEndScrollRunnable); } else { mEndScrollRunnable.run(); } } }
看看mEndScrollRunnable實現(xiàn)酪呻,在前面手指抬起的時候,我們其實是沒有計算內(nèi)存中的page頁的盐须,有一個mPopulatePending狀態(tài)跳過了實際計算玩荠,所以在最后頁面滾動結(jié)束的時候來一次最終的計算,就是在這里了贼邓。
private final Runnable mEndScrollRunnable = new Runnable() { public void run() { //設(shè)置SCROLL_STATE_IDLE狀態(tài) setScrollState(SCROLL_STATE_IDLE); //計算內(nèi)存中的page緩存內(nèi)容; populate(); } };
6. 存在的問題
- 當(dāng)同時設(shè)置viewpager的padding和page item之間的margin, page的marginDrawable會繪制在錯誤的地方阶冈,他累計了對應(yīng)的對應(yīng)的padding,這是錯誤的計算;
2. 在ScrollView中直接使用viewager,寬高不生效塑径。原因是ScrollView給子view的測量規(guī)格模式是UNSPECIFIED女坑,而我們的Viewpager測量又是setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), etDefaultSize(0, heightMeasureSpec))
組合。解決也不是很難统舀,只不過要針對不同的模式進(jìn)行自定義測量策略堂飞,后面如果有時間灌旧,綜合寫一下系統(tǒng)控件各種測量存在的問題吧....