ViewPager源碼簡析

欣賞一下
聽說廬山的冬季很美捉偏,一直沒機(jī)會去.....我很喜歡冬季里銀裝素裹的大地螟炫,那么的淡雅和純潔
功能點

API 21

  1. 測量根穷,布局柏靶,繪制;
  2. 事件的處理機(jī)制, viewPager的主動消耗物邑,攔截等;
  3. 頁面滾動計算溜哮,手動滾動;
  4. 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)的烦磁。繪個圖吧:

      vp_滑翔滾動圖.png

上面情形一养匈,向右滑動,計算出來的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頁面的滾動處理:
  1. 當(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頁面。這就是他的主要目的啦......
  1. 手動設(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. 存在的問題
  1. 當(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)控件各種測量存在的問題吧....

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市绰筛,隨后出現(xiàn)的幾起案子枢泰,更是在濱河造成了極大的恐慌,老刑警劉巖铝噩,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件衡蚂,死亡現(xiàn)場離奇詭異,居然都是意外死亡骏庸,警方通過查閱死者的電腦和手機(jī)毛甲,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來具被,“玉大人玻募,你說我怎么就攤上這事∫蛔耍” “怎么了七咧?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長叮叹。 經(jīng)常有香客問我艾栋,道長,這世上最難降的妖魔是什么蛉顽? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任蝗砾,我火速辦了婚禮,結(jié)果婚禮上携冤,老公的妹妹穿的比我還像新娘悼粮。我一直安慰自己,他們只是感情好曾棕,可當(dāng)我...
    茶點故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布矮锈。 她就那樣靜靜地躺著,像睡著了一般睁蕾。 火紅的嫁衣襯著肌膚如雪苞笨。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天子眶,我揣著相機(jī)與錄音瀑凝,去河邊找鬼。 笑死臭杰,一個胖子當(dāng)著我的面吹牛粤咪,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播渴杆,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼寥枝,長吁一口氣:“原來是場噩夢啊……” “哼宪塔!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起囊拜,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤某筐,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后冠跷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體南誊,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年蜜托,在試婚紗的時候發(fā)現(xiàn)自己被綠了抄囚。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,926評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡橄务,死狀恐怖幔托,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蜂挪,我是刑警寧澤重挑,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站锅劝,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏蟆湖。R本人自食惡果不足惜故爵,卻給世界環(huán)境...
    茶點故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望隅津。 院中可真熱鬧诬垂,春花似錦、人聲如沸伦仍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽充蓝。三九已至隧枫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間谓苟,已是汗流浹背官脓。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留涝焙,地道東北人卑笨。 一個月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像仑撞,于是被迫代替她去往敵國和親赤兴。 傳聞我的和親對象是個殘疾皇子妖滔,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,871評論 2 354