ViewPager源碼簡析

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

API 21

  1. 測量,布局,繪制;
  2. 事件的處理機制, viewPager的主動消耗,攔截等;
  3. 頁面滾動計算,手動滾動;
  4. viewPager設計帶來的問題;
0. 核心變量和標記
- mItems: 已經緩存過的page, 按照page的position從小到大來排列。    
- mCurItem: 當前顯示的page的position, 這是全局的。全局是針對mItems來說的.假如有5個page,
mItems存儲的可能是最后的三個頁面,那他緩存的第一個頁面并不是系統中的第一個page,而是全局的第三個page.
- mAdapter: 動態加載子page。
- ItemInfo: page控件構建的對象,里面的position即為全局page的position。
- mOffscreenPageLimit: 離屏限制數量,默認是1,也就是除了當前page左右緩存各一個,總數是3;如果是2,那么就左右各緩存兩個,總數是5。
- Scroller: 一個平滑滾動效果的計算工具類,類似的有Overscroller.他是根據起始坐標,終點坐標,以及時間這幾個變量來計算不同時間的view的x, y坐標的哦,從而實現滾動計算。
1. 測量:
  • ViewPager我們一般是不會在它的內部主動添加子view的,而是通過Adapter的形式去動態注入。其實除此之外,他還可以在xml添加他的DecorView, 這種特殊的view和adapter中添加的view的測量,布局都是不一樣,他一般是固定在viewPager的頁面中的不像page view一樣隨著手勢滾動,比如ViewPager的indicator這種就是DecorView。

  • onMeasure: 測量

     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         //簡單的一行代碼告訴了我,這viewPager的大小在這里就已經確定了。
         //如果viewpager是wrap和match的結果都一樣就是父容器剩下的寬高,如果是設定了dimense那
         //就是他自己的dimense寬高了。viewPager的這種設定就和通常的控件測量不一樣了,他完全忽略了自己的
         //pageview自己設定的寬與高了,這種設計存在這一些缺陷.
         //比如不要輕易地將viewPager放到ScrollView中,你會發現viewpager沒有高度。
         setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
                              getDefaultSize(0, heightMeasureSpec));
    
         final int measuredWidth = getMeasuredWidth();
         final int maxGutterSize = measuredWidth / 10;
         mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize);
    
         //這是viewPager測量之后,得到的剩余可用的寬與高
         int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight();
         int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
    
         //先測量裝飾的Decor,viewager的pageview的空間是扣除Decor之后的空間的哦;
         int size = getChildCount();
         for (int i = 0; i < size; ++i) {
             final View child = getChildAt(i);
             if (child.getVisibility() != GONE) {
                 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                 if (lp != null && lp.isDecor) {//如果是Decor
                     final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
                     final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
                     //Decor的測量規則大概是這樣的,如果是top/bottom就是橫向填充,寬的規格mode是
                     //EXACTLY, 規格size根據match/wrap,dimense來定。也就是如果size是
                     //match,wrap, 他們最后的規格尺寸都是一樣的即viewpager的可用寬度。dimense就是
                     //設定的寬。高的規格mode則要根據layoutParams來定,如果不是wrap,那么就是
                     //EXACTLY, 是就是AT_MOST,size在match/wrap的狀態下都一樣的。所以呢,他這個測量
                     //原則和標準的測量行為是保持一致的。一個方向的規格在wrap/match情況下size都是相同
                     //的,只有在dimense情形下不同。
                     int widthMode = MeasureSpec.AT_MOST;
                     int heightMode = MeasureSpec.AT_MOST;
                     boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM;
                     boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT;
    
                     if (consumeVertical) {
                         widthMode = MeasureSpec.EXACTLY;
                     } else if (consumeHorizontal) {
                         heightMode = MeasureSpec.EXACTLY;
                     }
    
                     int widthSize = childWidthSize;
                     int heightSize = childHeightSize;
                     if (lp.width != LayoutParams.WRAP_CONTENT) {
                         widthMode = MeasureSpec.EXACTLY;
                         if (lp.width != LayoutParams.FILL_PARENT) {
                             widthSize = lp.width;
                         }
                     }
                     if (lp.height != LayoutParams.WRAP_CONTENT) {
                         heightMode = MeasureSpec.EXACTLY;
                         if (lp.height != LayoutParams.FILL_PARENT) {
                             heightSize = lp.height;
                         }
                     }
                     final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode);
                     final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode);
                     //得出了Decor的測量規格之后,就可以對Decor進行測量啦;
                     child.measure(widthSpec, heightSpec);
    
                     if (consumeVertical) {
                         //剩余的高就是page view的高度
                         childHeightSize -= child.getMeasuredHeight();
                     } else if (consumeHorizontal) {
                         //剩余的寬就是page view的寬
                         childWidthSize -= child.getMeasuredWidth();
                     }
                 }
             }
         }
    
         //看到了,這就是page view的測量規格,這里已經確定了,他和具體的page view所設定的尺寸
         //沒有半毛錢的關系,全靠viewpager除去Decor之后剩余寬,高決定。
         mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
         mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);
    
         // Make sure we have created all fragments that we need to have shown.
         mInLayout = true;
         //這里是再次確定下要創建page view;
         populate();
         mInLayout = false;
    
         // Page views next.
         size = getChildCount();
         for (int i = 0; i < size; ++i) {
             final View child = getChildAt(i);
             if (child.getVisibility() != GONE) {
                 if (DEBUG) Log.v(TAG, "Measuring #" + i + " " + child
                                  + ": " + mChildWidthMeasureSpec);
    
               final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                 if (lp == null || !lp.isDecor) {
                     //對于pageView的測量啊,我們要計算一下頁面的寬度因子,這個是0-1.0之間,1是全部的
                     //寬,0.5是一半這樣子....
                     final int widthSpec = MeasureSpec.makeMeasureSpec(
                         (int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);
                     //測量子page啦;
                     child.measure(widthSpec, mChildHeightMeasureSpec);
                 }
             }
         }
     }
    
    
    • 總結一下, 在測量的時候,一開始是沒有子page view的,所以需要調用populate來創建和加載子page view, 然后才能測量子page。所以測量的功能大體分為三步驟:一先測量ViewPager大小, 二加載子page, 三測量子page。
  • populate:創建和銷毀page view的核心方法

    void populate(int newCurrentItem) {
        ItemInfo oldCurInfo = null;
        int focusDirection = View.FOCUS_FORWARD;
        //新的page的position和老的不同,那么將新賦值給mCurItem
        if (mCurItem != newCurrentItem) {
            focusDirection = mCurItem < newCurrentItem ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
            //記錄老的position對應的page, 這些都緩存在了mItems中呢。
            oldCurInfo = infoForPosition(mCurItem);
            mCurItem = newCurrentItem;
        }
    
        if (mAdapter == null) {//如果沒設置adapter, 那么就沒法創建和加載子page啦,簡單地排下Decor
            //順序,然后跳過啦.
            sortChildDrawingOrder();
            return;
        }
    
        //這個是在當用戶抬起的手指的時候,page還在計算滾動,我們不去創建和更改子page,為了安全起見。跳過啦。
        if (mPopulatePending) {
            if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
            sortChildDrawingOrder();
            return;
        }
    
        // Also, don't populate until we are attached to a window.  This is to
        // avoid trying to populate before we have restored our view hierarchy
        // state and conflicting with what is restored.
        if (getWindowToken() == null) {
            return;
        }
      
        //這是adapter的一個回調,用來告訴外界,已經開始加載子page了。
        mAdapter.startUpdate(this);
    
        
        final int pageLimit = mOffscreenPageLimit;
        //計算出最左端的全局position,最小肯定是0啦;
        final int startPos = Math.max(0, mCurItem - pageLimit);
        final int N = mAdapter.getCount();
        //計算出最右邊的全局positon, 最大肯定是N-1;
        final int endPos = Math.min(N-1, mCurItem + pageLimit);
      //這是保證當更新了adapter的數據之后,你要手動地去notifyDataSetChanged,否則數據不會更新;
        if (N != mExpectedAdapterCount) {
            String resName;
            try {
                resName = getResources().getResourceName(getId());
            } catch (Resources.NotFoundException e) {
                resName = Integer.toHexString(getId());
            }
            throw new IllegalStateException("The application's PagerAdapter changed the adapter's" +
                                            " contents without calling PagerAdapter#notifyDataSetChanged!" +
                                            " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N +
                                            " Pager id: " + resName +
                                            " Pager class: " + getClass() +
                                            " Problematic adapter: " + mAdapter.getClass());
        }
    
        // curIndex不是page對應的position, 而是items集合中存儲的位置,這個要和mCurItem區分開來哦
        int curIndex = -1;
        //curItem,當前要display的page.
        ItemInfo curItem = null;
        //在mItems尋找當前display的page
        for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
            final ItemInfo ii = mItems.get(curIndex);
            if (ii.position >= mCurItem) {//如果我寫的話肯定就是直接判等,但是沒這樣好,這樣大于的
                //時候會立即終止遍歷沒必要啦。如果都小于mCurItem就在最后一個位置加上新的item.
                //算是效率上的一次小優化吧,值得學習
                if (ii.position == mCurItem) curItem = ii;
                break;
            }
        }
    
        //只要當前curItem為null, page view設置了數量,創建新的item,添加到mItems集合中相應的位置中去呢;
        //這種一般是在第一次創建的時候才有的,后面就不會走的了....
        if (curItem == null && N > 0) {
            curItem = addNewItem(mCurItem, curIndex);
        }
    
        //接下來就是根據當前的page來左右去增刪page了,這里也是vp的核心思路啦
        if (curItem != null) {
            //這個是用來累計左邊的item的所處的寬度;
            float extraWidthLeft = 0.f;
            int itemIndex = curIndex - 1;
            ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
            final int clientWidth = getClientWidth();
            //這是一個左邊的限定因子,用來決定最左邊可以使用的寬度是多少,以決定怎么緩存page
            //如果是widthFactor是1,那么左邊因子就是1(忽略padding),也就是左邊至少能緩存一個page,
            //如果widthFactor是0.5,那么左邊因子就是1.5, 也就是左邊至少可以緩存3個page
            final float leftWidthNeeded = clientWidth <= 0 ? 0 :
            2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
            //從當前的page前一個開始往左遍歷,在全局的position為0停下來,這樣當當前page是0時候,就不會再
            //浪費時間往前去排查了。
            for (int pos = mCurItem - 1; pos >= 0; pos--) {
                //除了限定因子,還有一個我們設置的mOffscreenPageLimit哦
                if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
                    //當滿足了限定,并且位置是小于了最左邊的position,就需要destory了;
                    //如果查到前面有null了,說明這個位置已經destory過了。就不需要去destory了,可以停下
                    //了。
                    if (ii == null) {
                        break;
                    }
                    //
                    if (pos == ii.position && !ii.scrolling) {
                        //緩存中清除
                        mItems.remove(itemIndex);
                        //從viewPager中清除子page
                        mAdapter.destroyItem(this, pos, ii.object);
                        if (DEBUG) {
                            Log.i(TAG, "populate() - destroyItem() with pos: " + pos +
                                  " view: " + ((View) ii.object));
                        }
                        //因為要往前遍歷去destory啦!保證找到一個為null的page.
                        itemIndex--;
                        curIndex--;
                        ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                    }
                    //如果緩存中的該page正好是需要的page
                } else if (ii != null && pos == ii.position) {
                    //累計一個widthFactor
                    extraWidthLeft += ii.widthFactor;
                    //在緩存中向前查page,
                    itemIndex--;
                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                } else {//如果緩存中沒有需要的page,那么就要創建了哦
                    //沒有需要的page是一般因為itemIndex為-1,當前緩存的最左邊的就是當前page,所以需要
                    //在0位置上再添加一個page.也還有可能是不符合的page,那么也要添加一個page,因此要在
                    //itemIndex偏移一個位置。
                    ii = addNewItem(pos, itemIndex + 1);
                    extraWidthLeft += ii.widthFactor;
                    //當前page在mItems位置增加1
                    curIndex++;
                    //取出當前不符合的page遍歷,下一次他可能就需要destory了。
                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                }
            }
    
            float extraWidthRight = curItem.widthFactor;
            //右邊的一個緩存page
            itemIndex = curIndex + 1;
            
            //下面的處理和左邊幾乎就是一模一樣啦。
            if (extraWidthRight < 2.f) {
                ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                final float rightWidthNeeded = clientWidth <= 0 ? 0 :
                (float) getPaddingRight() / (float) clientWidth + 2.f;
                for (int pos = mCurItem + 1; pos < N; pos++) {
                    if (extraWidthRight >= rightWidthNeeded && pos > endPos) {
                        if (ii == null) {
                            break;
                        }
                        if (pos == ii.position && !ii.scrolling) {
                            mItems.remove(itemIndex);
                            mAdapter.destroyItem(this, pos, ii.object);
                            if (DEBUG) {
                                Log.i(TAG, "populate() - destroyItem() with pos: " + pos +
                                      " view: " + ((View) ii.object));
                            }
                            ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                        }
                    } else if (ii != null && pos == ii.position) {
                        extraWidthRight += ii.widthFactor;
                        itemIndex++;
                        ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                    } else {
                        //左邊要偏移一個位置,這里是不需要的
                        ii = addNewItem(pos, itemIndex);
                        itemIndex++;
                        extraWidthRight += ii.widthFactor;
                        ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                    }
                }
            }
            //這是最后一個難度計算了;快結束了.....
            calculatePageOffsets(curItem, curIndex, oldCurInfo);
        }
    
      ......略
            
        //告訴外面當前display的page是誰。
        mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);
    
        //到這里,添加頁面刪除頁面的動作就結束了啦。后面的東西和添加刪除page關系不是太大;
        mAdapter.finishUpdate(this);
    
        // 這里是用來設定child的布局參數的,因為child的布局參數是源自于pageadpter的設定的;所以在讀取了
        //adapter內容之后,這里要把他的widthFactor和position給到LayoutParams中去,以便后續使用
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            lp.childIndex = i;
            if (!lp.isDecor && lp.widthFactor == 0.f) {
                // 0 means requery the adapter for this, it doesn't have a valid width.
                final ItemInfo ii = infoForChild(child);
              if (ii != null) {
                    lp.widthFactor = ii.widthFactor;
                    lp.position = ii.position;
                }
            }
        }
        //這里將緩存的所有view排好繪制順序,Decor裝飾元素是最后繪的.
        sortChildDrawingOrder();
      //如果viewPager有焦點,必須將焦點view放在當前顯示的page的結構樹上;
        if (hasFocus()) {
            View currentFocused = findFocus();
            ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null;
            if (ii == null || ii.position != mCurItem) {
                for (int i=0; i<getChildCount(); i++) {
                    View child = getChildAt(i);
                    ii = infoForChild(child);
                    if (ii != null && ii.position == mCurItem) {
                        if (child.requestFocus(focusDirection)) {
                            break;
                        }
                    }
                }
            }
        }
    }
    
    
    • 總結一下下:populate的實現比較繁瑣略帶復雜,但是他的目的是很單純的,就是在初次加載page或者滑動viewpager的時候在布局容器中加載對應的子page, 同時刪除超過限定位置的page,以達到內存的優化啦。比如我們限定的mOffscreenPageLimit是1, 那么內存中緩存的就是3個page, 我們會計算出當前page的左右兩個緩存起來的,其他的頁面刪除掉。隨著頁面的滾動,動態更新緩存內容page.
  • addNewItem: 添加子Item元素

    //創建新的item到mItems集合中去;position是page在所有的page中對應的位置,全局。index是在mItems中緩存的位置。
    ItemInfo addNewItem(int position, int index) {
        ItemInfo ii = new ItemInfo();
        ii.position = position;
        //看到沒,這里是調用我們復寫adapter的instantiateItem來創建子page的哦;
        ii.object = mAdapter.instantiateItem(this, position);
        //也是調用我們的adapter來加載寬度因子
        ii.widthFactor = mAdapter.getPageWidth(position);
        if (index < 0 || index >= mItems.size()) {
            mItems.add(ii);
        } else {
            mItems.add(index, ii);
        }
        return ii;
    }
    
    • calculatePageOffsets:計算各個page的offset的偏移量。
     
    //curItem-當前display的page, curIndex-他在mItems中的位置, oldInfo上一次display的page
    
    private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) {
         final int N = mAdapter.getCount();
         final int width = getClientWidth();
         final float marginOffset = width > 0 ? (float) mPageMargin / width : 0;
         // Fix up offsets for later layout.
         if (oldCurInfo != null) {
             final int oldCurPosition = oldCurInfo.position;
             //其實下面的邏輯是計算當前的page和原來顯示的page之間的page的offset偏移量
             // 如果當前是向左側滑動
             if (oldCurPosition < curItem.position) {
                 int itemIndex = 0;
                 ItemInfo ii = null;
                 //當前page的offset是左邊一個page的offset+他的寬度因子+margin因子
                 float offset = oldCurInfo.offset + oldCurInfo.widthFactor + marginOffset;
                 for (int pos = oldCurPosition + 1;
                      pos <= curItem.position && itemIndex < mItems.size(); pos++) {
                     ii = mItems.get(itemIndex);
                     while (pos > ii.position && itemIndex < mItems.size() - 1) {
                         itemIndex++;
                         ii = mItems.get(itemIndex);
                     }
                     while (pos < ii.position) {
                         // We don't have an item populated for this,
                         // ask the adapter for an offset.
                         offset += mAdapter.getPageWidth(pos) + marginOffset;
                         pos++;
                     }
                     
                     ii.offset = offset;
                     //下一個page的offset同樣累加上當前page的寬度因子和margin因子
                     offset += ii.widthFactor + marginOffset;
                 }
                 //如果是向右邊滑動
             } else if (oldCurPosition > curItem.position) {
                 int itemIndex = mItems.size() - 1;
                 ItemInfo ii = null;
                 float offset = oldCurInfo.offset;
                 for (int pos = oldCurPosition - 1;
                      pos >= curItem.position && itemIndex >= 0; pos--) {
                     ii = mItems.get(itemIndex);
                     while (pos < ii.position && itemIndex > 0) {
                         itemIndex--;
                         ii = mItems.get(itemIndex);
                     }
                     while (pos > ii.position) {
                         // We don't have an item populated for this,
                         // ask the adapter for an offset.
                         offset -= mAdapter.getPageWidth(pos) + marginOffset;
                         pos--;
                     }
                     //當前page的offset = 后一個page的offset - 當前page的width因子- margin因子
                     offset -= ii.widthFactor + marginOffset;
                     ii.offset = offset;
                 }
             }
         }
    
         //接下來計算所有的緩存的page的偏移因子;根據前面的原則,難度也不大。
        // 除此之外,還計算了第一個緩存的page的偏移因子,最后一個page的偏移因子。
         final int itemCount = mItems.size();
        
         float offset = curItem.offset;
         int pos = curItem.position - 1;
         mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE;
         mLastOffset = curItem.position == N - 1 ?
             curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE;
         // Previous pages
         for (int i = curIndex - 1; i >= 0; i--, pos--) {
             final ItemInfo ii = mItems.get(i);
             while (pos > ii.position) {
                 offset -= mAdapter.getPageWidth(pos--) + marginOffset;
             }
             offset -= ii.widthFactor + marginOffset;
             ii.offset = offset;
             if (ii.position == 0) mFirstOffset = offset;
         }
         offset = curItem.offset + curItem.widthFactor + marginOffset;
         pos = curItem.position + 1;
         // Next pages
         for (int i = curIndex + 1; i < itemCount; i++, pos++) {
             final ItemInfo ii = mItems.get(i);
             while (pos < ii.position) {
                 offset += mAdapter.getPageWidth(pos++) + marginOffset;
             }
             if (ii.position == N - 1) {
                 mLastOffset = offset + ii.widthFactor - 1;
             }
             ii.offset = offset;
             offset += ii.widthFactor + marginOffset;
         }
    
         mNeedCalculatePageOffsets = false;
     }
    
    • 簡單總結一下, page的offset計算也是挺繁瑣的,這個玩意是來干嘛的, 有什么用呢? 它其實是用來布局子page元素的,定位每個page的位置,每個page的定位都和前面的page息息相關,這里用每個page的offset來標識。接下來看viewPager是如何給子page來布局, 就會明白這個offset的實際用途呢。
2. ViewPager的布局過程:
  • onLayout: 布局ViewPager的子page以及裝飾的Decor.如title

    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int count = getChildCount();
        int width = r - l;
        int height = b - t;
        int paddingLeft = getPaddingLeft();
        int paddingTop = getPaddingTop();
        int paddingRight = getPaddingRight();
        int paddingBottom = getPaddingBottom();
        final int scrollX = getScrollX();
    
        int decorCount = 0;
    
        //先計算Decor這類的view的位置,這種view一般都是固定的,不會隨之viewPager去移動的,
        //這種使用的不多,略吧....
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                int childLeft = 0;
                int childTop = 0;
                if (lp.isDecor) {//如果是Decor
                    final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
                    final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
                    switch (hgrav) {//如果是垂直填充
                        default:
                            childLeft = paddingLeft;
                            break;
                        case Gravity.LEFT:
                            childLeft = paddingLeft;
                         
                            paddingLeft += child.getMeasuredWidth();
                            break;
                        case Gravity.CENTER_HORIZONTAL:
                            childLeft = Math.max((width - child.getMeasuredWidth()) / 2,
                                                 paddingLeft);
                            break;
                        case Gravity.RIGHT:
                            childLeft = width - paddingRight - child.getMeasuredWidth();
                            paddingRight += child.getMeasuredWidth();
                            break;
                    }
                    switch (vgrav) {//如果是水平填充,
                        default:
                            childTop = paddingTop;
                            break;
                        case Gravity.TOP:
                            childTop = paddingTop;
                            paddingTop += child.getMeasuredHeight();
                            break;
                        case Gravity.CENTER_VERTICAL:
                            childTop = Math.max((height - child.getMeasuredHeight()) / 2,
                                                paddingTop);
                            break;
                        case Gravity.BOTTOM:
                            childTop = height - paddingBottom - child.getMeasuredHeight();
                            paddingBottom += child.getMeasuredHeight();
                            break;
                    }
                    //累計scrollx,可以看到,隨著scroll移動,childLeft的位置是會跟著移動,以達到
                    //Decor保持在屏幕原來的位置;
                    childLeft += scrollX;
                    child.layout(childLeft, childTop,
                                 childLeft + child.getMeasuredWidth(),
                                 childTop + child.getMeasuredHeight());
                    decorCount++;
                }
            }
        }
    
       
        //如果沒有Decor就是除去viewpager自己的左右padding,這個寬度就是child的寬度啦。
        final int childWidth = width - paddingLeft - paddingRight;
       
         //真正開始布局我們的page了;
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                ItemInfo ii;
                //從緩存中找到對應的view.因為有offset的數值呀,child中沒有哦;
                if (!lp.isDecor && (ii = infoForChild(child)) != null) {
                    //看到了嗎,offset乘以childWidth,來計算當前page的偏移量。
                    int loff = (int) (childWidth * ii.offset);
                    //每個page的left等于paddingleft + 自己的偏移量
                    int childLeft = paddingLeft + loff;
                    int childTop = paddingTop;
                    //當在第一次Populate時候,添加的子page,那個時候創建的page添加進去的page
                    //的needsMeasure是true.
                    if (lp.needsMeasure) {
                        // This was added during layout and needs measurement.
                        // Do it now that we know what we're working with.
                        lp.needsMeasure = false;
                        //這個其實在onMeasure中已經測量過了,這里沒有必要重測
                        final int widthSpec = MeasureSpec.makeMeasureSpec(
                            (int) (childWidth * lp.widthFactor),
                            MeasureSpec.EXACTLY);
                        //高要測量一次,
                        final int heightSpec = MeasureSpec.makeMeasureSpec(
                            (int) (height - paddingTop - paddingBottom),
                            MeasureSpec.EXACTLY);
                        child.measure(widthSpec, heightSpec);
                    }
                    if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object
                                     + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth()
                                     + "x" + child.getMeasuredHeight());
                    //根據他的left, top,然后測量的寬高就可以給page布局啦
                    child.layout(childLeft, childTop,
                                 childLeft + child.getMeasuredWidth(),
                                 childTop + child.getMeasuredHeight());
                }
            }
        }
        
        mTopPageBounds = paddingTop;
        mBottomPageBounds = height - paddingBottom;
        mDecorChildCount = decorCount;
      //第一次布局會在這里滾動到指定位置;
        if (mFirstLayout) {
            scrollToItem(mCurItem, false, 0, false);
        }
        mFirstLayout = false;
    }
    
    
    
    • 總結一下: 布局其實就是利用前面計算的page偏移量來和page的測量寬度來布局子page的位置哦。這里說下偏移量,比如第一個page的偏移量是0, 那么第二個page的偏移量就是第一個page的width + margiin , 后面的page就這樣累計疊加。布局里面雖然有測量,但我認為這只是一個安全措施,第一次應該已經實現了對子page的測量了。這里的測量結果和前面應該是一致的。
3. 繪制的地方
  • onDraw:主要是繪制view本身,因為Viewpager本身并沒有什么東西,他的子view由子child本身繪制。但是當我們設置了marginDrawable的時候,這個drawable就要由我們的ViewPager來繪制啦,我們看看他的實現。
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    // 存在著margin,并且設定了drawable.
    if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 
        && mAdapter != null) {
        final int scrollX = getScrollX();
        final int width = getWidth();
        // 計算margin與viewager的寬度的比例
        final float marginOffset = (float) mPageMargin / width;
        int itemIndex = 0;
        // 取出緩存的第一個子View.
        ItemInfo ii = mItems.get(0);
        float offset = ii.offset;
        final int itemCount = mItems.size();
        final int firstPos = ii.position;
        final int lastPos = mItems.get(itemCount - 1).position;
        //遍歷緩存中所有的view.
        for (int pos = firstPos; pos < lastPos; pos++) {
            // 這個寫法其實有點不好看,意思就是不停地從mItems緩存中取出新的View.
            while (pos > ii.position && itemIndex < itemCount) {
                ii = mItems.get(++itemIndex);
            }

            float drawAt;
            //通過view的寬度因子和左邊的便宜來計算marginDrawable繪制的開始位置;
            if (pos == ii.position) {
                drawAt = (ii.offset + ii.widthFactor) * width;
                offset = ii.offset + ii.widthFactor + marginOffset;
            } else {
                float widthFactor = mAdapter.getPageWidth(pos);
                drawAt = (offset + widthFactor) * width;
                offset += widthFactor + marginOffset;
            }

            if (drawAt + mPageMargin > scrollX) {
                mMarginDrawable.setBounds((int) drawAt, mTopPageBounds,
                                          (int) (drawAt + mPageMargin + 0.5f), mBottomPageBounds);
                mMarginDrawable.draw(canvas);
            }
            //其實前面已經繪制過了,這個忽略的繪制本意卻沒有達到
            if (drawAt > scrollX + width) {
                break; // No more visible, no sense in continuing
            }
        }
    }
}


說完了基本的測量、布局、繪制,就要來看看viewPager的內容滾動吧,畢竟這不只是一個靜態的容器.

4. 事件的攔截與觸摸消耗
  • onInterceptTouchEvent: 表示在什么情況下的用戶操作,會將手勢操作攔截下來給到我們viewpager來用的意思。 在一次手勢中如果攔截成功后面就不會再觸發該方法,如果沒有攔截成功會不停地調用該方法來檢測攔截策略.

    public boolean onInterceptTouchEvent(MotionEvent ev) {
      
        final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
        //4.1 up,cancel不攔截;
      //在View的攔截機制中啊, 如果發生了攔截,那么當次手勢是不會再觸發onInterceptTouchEvent啦
        //來到這里,說明down,move事件都沒有發生過攔截,這里cacel,up自然不要攔截啦,
        //其次這里主要做了一些viewpager任務清理工作.
        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
            // Release the drag.
            if (DEBUG) Log.v(TAG, "Intercept done!");
            //清理工作;
            mIsBeingDragged = false;
            mIsUnableToDrag = false;
            mActivePointerId = INVALID_POINTER;
            if (mVelocityTracker != null) {
                mVelocityTracker.recycle();
                mVelocityTracker = null;
            }
            //down, move沒有攔截,這里自然不會攔截。
            return false;
        }
      
        //4.2, 如果是move事件,
        if (action != MotionEvent.ACTION_DOWN) {
                  //雖然沒攔截,但是vp如果在ontouch中被認為是拖拽了。這里就攔截下來了。畢竟也不一定攔截
              //才能消耗的,如果vp沒有子view或者子view不消耗,那么vp就有機會消耗啦呀。
        
                if (mIsBeingDragged) {
                    if (DEBUG) Log.v(TAG, "Intercept returning true!");
                    return true;
                }
                //如果之前是縱行滾動,當次手勢是不會被Viewpager去攔截的;
                if (mIsUnableToDrag) {
                    if (DEBUG) Log.v(TAG, "Intercept returning false!");
                    return false;
                }
          }
    
        //下面看看,如果第一次來到vp中,什么時候會主動攔截
        switch (action) {
            case MotionEvent.ACTION_MOVE: {
       
                final int activePointerId = mActivePointerId;
                if (activePointerId == INVALID_POINTER) {
                    // If we don't have a valid id, the touch down wasn't on content.
                    break;
                }
    
                final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
                final float x = MotionEventCompat.getX(ev, pointerIndex);
                final float dx = x - mLastMotionX;
                final float xDiff = Math.abs(dx);
                final float y = MotionEventCompat.getY(ev, pointerIndex);
                final float yDiff = Math.abs(y - mInitialMotionY);
                if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
    
                
                if (dx != 0 && !isGutterDrag(mLastMotionX, dx) &&
                    canScroll(this, false, (int) dx, (int) x, (int) y)) {
                    // Nested view has scrollable area under this point. Let it be handled there.
                    mLastMotionX = x;
                    mLastMotionY = y;
                    mIsUnableToDrag = true;
                    return false;
                }
                //在這里檢測到攔截了,條件是橫向的move達到了滾到閾值,
                //并且橫向滾動值達到超過了縱向滾動的兩倍;
                if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {
                    if (DEBUG) Log.v(TAG, "Starting drag!");
                    mIsBeingDragged = true;
                    //申請父容器不要攔截vp
                    requestParentDisallowInterceptTouchEvent(true);
                    setScrollState(SCROLL_STATE_DRAGGING);
                    mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop :
                    mInitialMotionX - mTouchSlop;
                    mLastMotionY = y;
                    setScrollingCacheEnabled(true);
                } else if (yDiff > mTouchSlop) {
                    //縱向滾動了,該次手勢后面就不會讓viewpager嘗試攔截哦;
                    //所以識別到了縱行滾動,該次就不會嘗試viewPager攔截事件了;
                    if (DEBUG) Log.v(TAG, "Starting unable to drag!");
                    //看這里;
                    mIsUnableToDrag = true;
                }
                if (mIsBeingDragged) {
                    // Scroll to follow the motion event
                    if (performDrag(x)) {
                        ViewCompat.postInvalidateOnAnimation(this);
                    }
                }
                break;
            }
    
            case MotionEvent.ACTION_DOWN: {//down手勢會攔截嗎?會的啊
                /*
                     * Remember location of down touch.
                     * ACTION_DOWN always refers to pointer index 0.
                     */
                mLastMotionX = mInitialMotionX = ev.getX();
                mLastMotionY = mInitialMotionY = ev.getY();
                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
                //down說明是一次新的手勢啦,要清除掉請面的縱向滾動標記;
                mIsUnableToDrag = false;
    
                mScroller.computeScrollOffset();
                //當前viewPager還在滾動沒停下來,還沒靠邊,down手勢下來了,就要攔截呢。
                if (mScrollState == SCROLL_STATE_SETTLING &&
                    Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough){
                    // 終止滾動
                    mScroller.abortAnimation();
                    mPopulatePending = false;
                    //計算頁面
                    populate();
                    //當前要攔截了
                    mIsBeingDragged = true;
                    //請求父容器不要攔截。這里可以看出vp后面來消耗滾動事件,因此就讓他的父容器不要
                    //攔截后續的move事件,讓他們能順利地來到vp中.
                    requestParentDisallowInterceptTouchEvent(true);
                    //裝態為dragging
                    setScrollState(SCROLL_STATE_DRAGGING);
                } else {//非上述情況就不攔截了,也是默認處理。
                    completeScroll(false);
                    mIsBeingDragged = false;
                }
    
                if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY
                                 + " mIsBeingDragged=" + mIsBeingDragged
                                 + "mIsUnableToDrag=" + mIsUnableToDrag);
                break;
            }
          
              
            case MotionEventCompat.ACTION_POINTER_UP:
                 //多手指更換。很簡單,第二個手指放下,跟蹤第二個手指的滑動,放棄跟蹤第一個手指動作.
                onSecondaryPointerUp(ev);
                break;
        }
    
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(ev);
    
        //vp認為是否是拖拽,是否要攔截下來
        return mIsBeingDragged;
    }
    
    
    
    • 總結一下,啥情況下會攔截呢, 我認為主要有以下三種情況:

      1. 在down事件的時候,一般情況呢,view體系的攔截策略是不應該在down中設定的,因為在down事件中攔截的話,后續子view請求父容器不要攔截是無效的, 這樣就限定了子view的功能了。但是我們的ViewPager中卻在這攔截了: 如果當前還在滾動狀態并且還沒靠邊,手勢down來了, 那么就要攔截下來,這個時候就vp就想自己使用后面的move, up事件了,而且子view也不可能在檔次手勢中有機會使用了。

      2. 在move事件時候, 如果move達到了滾到閾值,并且橫向滾動值達到超過了縱向滾動的兩倍,就會將事件攔截下來自己使用了。
      3. 如果vp的子view不消耗相應滾動, 在vp的onTouchEvent中消耗了滾動事件,并且認為是橫向拖拽那么這里就會直接攔截下來,不做多余地判斷;

  • onTouchEvent: Viewpager對滾動事件的消耗,主要邏輯是處理頁面的滾動,滾動計算和發起都在這里面;

    //當事件由vp消耗有兩種可能,其一是被vp攔截,其二vp的子view不能消耗對應的事件。
    public boolean onTouchEvent(MotionEvent ev) {
        
        ......
    
         //沒有子pager,那還滾動啥子喲,直接返回false;
        if (mAdapter == null || mAdapter.getCount() == 0) {
            // Nothing to present or scroll; nothing to touch.
            return false;
        }
    
        //構建速度拾取器,通過它可以獲得手勢的速度,坐標點等
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        //跟蹤手勢,為了計算速度;
        mVelocityTracker.addMovement(ev);
    
        final int action = ev.getAction();
        boolean needsInvalidate = false;
    
        switch (action & MotionEventCompat.ACTION_MASK) {
                
            case MotionEvent.ACTION_DOWN: {
                //手指落下就要終止滾動
                mScroller.abortAnimation();
                mPopulatePending = false;
                //重新計算頁面量
                populate();
                // 記錄down位置的x, y位置;
                mLastMotionX = mInitialMotionX = ev.getX();
                mLastMotionY = mInitialMotionY = ev.getY();
                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
                break;
            }
            case MotionEvent.ACTION_MOVE://
                if (!mIsBeingDragged) {//走到這里說明并未發生攔截,且vp子view不能來識別這個
                    //down-move事件。那就上報給我們的vp來處理了,后面肯定要將mIsBeingDragged設定
                    //true表示vp自己來處理滾動事件。
                    final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
                    final float x = MotionEventCompat.getX(ev, pointerIndex);
                    final float xDiff = Math.abs(x - mLastMotionX);
                    final float y = MotionEventCompat.getY(ev, pointerIndex);
                    final float yDiff = Math.abs(y - mLastMotionY);
                    if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
                    //當達到了我們的閾值,橫向大于縱向.那么我們就認為是拖拽,這個比攔截的條件松,
                    //畢竟我是第一個處理的,就不需要等到是縱行的兩倍在認為是拖拽;
                    if (xDiff > mTouchSlop && xDiff > yDiff) {
                        if (DEBUG) Log.v(TAG, "Starting drag!");
                       //vp拖拽狀態,后面的move就直接來使用
                        mIsBeingDragged = true;
                        //move已經達到了vp可以識別的滾動了,那么就告訴父容器后面的滾動事件就不能攔截了
                        requestParentDisallowInterceptTouchEvent(true);
                        mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop :
                        mInitialMotionX - mTouchSlop;
                        mLastMotionY = y;
                        setScrollState(SCROLL_STATE_DRAGGING);
                        setScrollingCacheEnabled(true);
    
                        // Disallow Parent Intercept, just in case
                        ViewParent parent = getParent();
                        if (parent != null) {
                            parent.requestDisallowInterceptTouchEvent(true);
                        }
                    }
                }
                // 少廢話,這里執行拖拽動作;
                if (mIsBeingDragged) {
                    // Scroll to follow the motion event
                    final int activePointerIndex = MotionEventCompat.findPointerIndex(
                        ev, mActivePointerId);
                    final float x = MotionEventCompat.getX(ev, activePointerIndex);
                    //當viewPager滾動page的時候就是通過performDrag來實現滾動到當前滑動的位置的;
                    needsInvalidate |= performDrag(x);
                }
                break;
            case MotionEvent.ACTION_UP://這里主要是根據vp滑動的位置來計算最后要滾到vp的哪個子page
                if (mIsBeingDragged) {//只有是vp識別的拖拽,才會計算vp最后停靠的頁面。
                    final VelocityTracker velocityTracker = mVelocityTracker;
                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                    int initialVelocity = (int) VelocityTrackerCompat.getXVelocity(
                        velocityTracker, mActivePointerId);
                    mPopulatePending = true;
                    final int width = getClientWidth();
                    final int scrollX = getScrollX();
                    //計算出的這個item是vp顯示區域最左邊的page
                    final ItemInfo ii = infoForCurrentScrollPosition();
                    //在viewpager中顯示的最左邊的page
                    final int currentPage = ii.position;
                    //計算當前scrollX在當前page中的偏移與當前page的with的比例,來看看后面該滾動到哪一
                    //頁。
                    final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor;
                    final int activePointerIndex =
                        MotionEventCompat.findPointerIndex(ev, mActivePointerId);
                    final float x = MotionEventCompat.getX(ev, activePointerIndex);
                    //從down-up過程中移動的距離
                    final int totalDelta = (int) (x - mInitialMotionX);
                    //判斷即將停靠的頁面
                    int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
                                                       totalDelta);
                    //設置滾動到對應的頁面;
                    setCurrentItemInternal(nextPage, true, true, initialVelocity);
    
                    mActivePointerId = INVALID_POINTER;
                    endDrag();
                    needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease();
                }
                break;
           
                .... 
                    
         //viewpager默認都返回true;就是說滾動事件只要來到我身上了,那么我肯定不會拒絕的,來吧!
      return true;
    }
    
    
    • infoForCurrentScrollPosition: 這個函數看得有點煩,在前面手指抬起的時候會計算出當前vp最左邊界處出現的page的緩存對象,就是通過這個方法來實現的。繪個圖吧:

      vp_滑翔滾動圖.png

上面情形一,向右滑動,計算出來的item是page0, 它是vp左邊界中顯示的頁面。情形二,向左滑動,計算出來的item是page1.

  • determineTargetPage:顧名思義就是計算出手指抬起后,vp將要停靠的頁面; 看下實現吧。
//currentPage指的是vp最左邊對應的頁面哦,不是當前mCurItem哦;
private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) {
    int targetPage;
    //這是快速滑動的判斷,當速度達到了滑翔條件(view往右滑動速度為負,向左滑動速度才是正數。)
    if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
        //向左快速滑的話,就停靠在當前vp左邊界的page位置。向右快速滑,就停靠在下一個頁面上。
        //參照上圖,向右快速滑停靠的頁面是page0,向左快速滑動停靠的頁面是page2
        targetPage = velocity > 0 ? currentPage : currentPage + 1;
    } else {
        //從這里看到,如果往右邊滑動,truncator = 0.4f,要想選中下一個page,必須要劃過下一個page
        //0.6的寬度因子哦;如果往左邊滑動currentPage會小于mCurItem,那么必須也要劃出來0.6因子
        //那么余下的pageOffset會小于0.4,這樣家起來小于1,會跳到前面的頁面;
        final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f;
        targetPage = (int) (currentPage + pageOffset + truncator);
    }

    if (mItems.size() > 0) {//這里是確保page都是我們緩存中的page.
        final ItemInfo firstItem = mItems.get(0);
        final ItemInfo lastItem = mItems.get(mItems.size() - 1);

        // Only let the user target pages we have items for
        targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position));
    }

    return targetPage;
}
5. page頁面的滾動處理:
  1. 當手指慢慢滑動,頁面需要跟隨手指去滑動,它是由 performDrag 來負責的, 來看源代碼吧:

    //參數x是將要滾動到的x坐標;
    private boolean performDrag(float x) {
        boolean needsInvalidate = false;
     //需要滾動的距離
        final float deltaX = mLastMotionX - x;
        mLastMotionX = x;
    
        float oldScrollX = getScrollX();
        //計算最終的scrollX, vp的滾動是通過scoll內容來實現的哦;
        float scrollX = oldScrollX + deltaX;
        final int width = getClientWidth();
     //這里的firstoffset并不是指第一個全局的page,而是內存中緩存的第一個page,mLastOffset同理如此;
        float leftBound = width * mFirstOffset;
        float rightBound = width * mLastOffset;
        boolean leftAbsolute = true;
        boolean rightAbsolute = true;
    
        final ItemInfo firstItem = mItems.get(0);
        final ItemInfo lastItem = mItems.get(mItems.size() - 1);
        if (firstItem.position != 0) {//如果不是第一個全局page.
            leftAbsolute = false;//就不會繪制邊緣拖拽效果
            leftBound = firstItem.offset * width;
        }
        if (lastItem.position != mAdapter.getCount() - 1) {//如果不是最后一個全局page.
            rightAbsolute = false;//就不會繪制邊緣拖拽效果
            rightBound = lastItem.offset * width;
        }
    
        if (scrollX < leftBound) {
            if (leftAbsolute) {//如果到了第一個的頂邊了,就要繪制拖拽邊緣效果
                float over = leftBound - scrollX;
                needsInvalidate = mLeftEdge.onPull(Math.abs(over) / width);
            }
            scrollX = leftBound;
        } else if (scrollX > rightBound) {
            if (rightAbsolute) {//如果到了最后一個的頂邊了,就要繪制拖拽邊緣效果
                float over = scrollX - rightBound;
                needsInvalidate = mRightEdge.onPull(Math.abs(over) / width);
            }
            scrollX = rightBound;
        }
        
        mLastMotionX += scrollX - (int) scrollX;
        //通過View.scollTo來滾動到指定的位置;觸發之后,系統會不停地調用我們vp中重寫的computeScroll
        //方法,在該方法中會調用completeScroll(true),他做了一件重要的事情,就是
        //重新計算內存中應該緩存的page,即populate方法觸發。
        scrollTo((int) scrollX, getScrollY());
        //這里會不停地回調onPageScrolled,告訴使用者當前在滾動的位置是多少.....
        pageScrolled((int) scrollX);
     
        //返回數值表示是否需要重繪制,即調用vp自身的onDaw方法。從前面看到只有到達了邊緣才需要重繪制,難道
        //我們滾動的時候不需要重新繪制ui嗎,不符合view繪制策略呀。實際上vp的ondraw只負責marginDrawable
        //和邊緣滾動效果,vp自身內容的繪制是交給View來做的,所以在邊緣觸發只是繪制邊緣效果。其他的繪制會在
        //scrollTo中主動觸發呢。
        return needsInvalidate;
    }
    
    
    • 總結一下performDrag方法吧: 當在滾動過程中,即onTouch的move中會不停地調用該方法來實現內容的滾動,它根據手勢的位置計算滾動的距離,然后還會不斷地去計算內存中應該重新存儲哪些新的page頁面。這就是他的主要目的啦......
  1. 手動設置滾動的頁面或者手指抬起要停靠的頁面,由 setCurrentItemInternal,setCurrentItem這類方法族來實現, 在onTouchEvent中的手指抬起的時候會有這么一段,

    //等待計算page內存頁
    mPopulatePending = true;
    
    //計算抬起手指后要滾動到的頁面
    int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
                                                       totalDelta);
    //設置滾動到對應的頁面;
    setCurrentItemInternal(nextPage, true, true, initialVelocity);
    
    

    來看看setCurrentItemInternal的源碼吧:

    //決定是否回調onPageSelected方法,可以看出只有不等的時候才會回調,因此
    //第一次顯示page時候是不會調的哦;
    final boolean dispatchSelected = mCurItem != item;
    if (mFirstLayout) {
        mCurItem = item;
        if (dispatchSelected && mOnPageChangeListener != null) {
            mOnPageChangeListener.onPageSelected(item);
        }
        if (dispatchSelected && mInternalPageChangeListener != null) {
            mInternalPageChangeListener.onPageSelected(item);
        }
        requestLayout();
    } else {
        //重新計算page內存頁面集合,但是由于前面mPopulatePending=true,up這里其實會跳過內部的計算的。
        populate(item);
        //滾動到特定的頁面,這里會利用到Vp自帶的Scroller去實現平滑滾動效果;
        scrollToItem(item, smoothScroll, velocity, dispatchSelected);
    }
    

    繼續來看看scollToItem怎么來實現滾動頁面的吧:

    private void scrollToItem(int item, boolean smoothScroll, int velocity,
                              boolean dispatchSelected) {
        final ItemInfo curInfo = infoForPosition(item);
        int destX = 0;
        if (curInfo != null) {
            final int width = getClientWidth();
            destX = (int) (width * Math.max(mFirstOffset,
                                            Math.min(curInfo.offset, mLastOffset)));
        }
        if (smoothScroll) {//up手勢走的是這里;
            //根據距離和初速度來實現平滑地滾動;
            smoothScrollTo(destX, 0, velocity);
            if (dispatchSelected && mOnPageChangeListener != null) {
                //告訴使用者我們的變化到了哪個頁面;
                mOnPageChangeListener.onPageSelected(item);
            }
            if (dispatchSelected && mInternalPageChangeListener != null) {
                mInternalPageChangeListener.onPageSelected(item);
            }
        } else {//非平滑滾動
            if (dispatchSelected && mOnPageChangeListener != null) {
                mOnPageChangeListener.onPageSelected(item);
            }
            if (dispatchSelected && mInternalPageChangeListener != null) {
                mInternalPageChangeListener.onPageSelected(item);
            }
         
            completeScroll(false);
             //調用View.scrollTo來實現滾動
            scrollTo(destX, 0);
            pageScrolled(destX);
        }
    }
    
    

    來吧,接著看smoothScrollTo方法, 看他怎么來實現平滑滾動:

    void smoothScrollTo(int x, int y, int velocity) {
         .......
            //如果已經滾動結束了,就設置SCROLL_STATE_IDLE狀態, 然后使用populate計算內存頁
            //如果還沒到滾動結束點呢?
        if (dx == 0 && dy == 0) {
            completeScroll(false);
            populate();
            setScrollState(SCROLL_STATE_IDLE);
            return;
        }
     
        setScrollingCacheEnabled(true);
        //設置滾動狀態SCROLL_STATE_SETTLING,表示還在自己滑動
        setScrollState(SCROLL_STATE_SETTLING);
    
        //下面就是計算慢性滑動的時間,最終的x,y坐標:
        final int width = getClientWidth();
        final int halfWidth = width / 2;
        final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width);
        final float distance = halfWidth + halfWidth *
            distanceInfluenceForSnapDuration(distanceRatio);
    
        int duration = 0;
        //根據速度來計算時間
        velocity = Math.abs(velocity);
        if (velocity > 0) {
            duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
        } else {
            final float pageWidth = width * mAdapter.getPageWidth(mCurItem);
            final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin);
            duration = (int) ((pageDelta + 1) * 100);
        }
        duration = Math.min(duration, MAX_SETTLE_DURATION);
     //調用輔助來Scoller來計算不同時間的坐標
        mScroller.startScroll(sx, sy, dx, dy, duration);
        //發命令給系統做重繪制操作,系統接著會調用computeScroll方法,來根據滾動位置來滑動內容到指定位置;
        ViewCompat.postInvalidateOnAnimation(this);
    }
    
    

    來吧,來看看ViewPager重寫的computeScroll方法;

        public void computeScroll() {
            //當是我們的滾動Scroller來負責計算,這里如果還沒有滾動結束
            if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
                int oldX = getScrollX();
                int oldY = getScrollY();
                int x = mScroller.getCurrX();
                int y = mScroller.getCurrY();
             //滾動到指定的位置
                if (oldX != x || oldY != y) {
                    scrollTo(x, y);
                    if (!pageScrolled(x)) {
                        mScroller.abortAnimation();
                        scrollTo(0, y);
                    }
                }
    
                // 執行重新繪制操作,這里保證邊緣效果能有機會繪制,vp的滾動位置繪制由scrollTo
                //自己去負責的;
                ViewCompat.postInvalidateOnAnimation(this);
                return;
            }
    
            // 如果滾動結束了,那么要干什么呢?
            completeScroll(true);
        }
    

    繼續,快結束了, completeScroll:

    private void completeScroll(boolean postEvents) {
        
        //如果是還在滾動狀態,就要計算page內存內容啦;
        boolean needPopulate = mScrollState == SCROLL_STATE_SETTLING;
    
        .......
        
        mPopulatePending = false;
        for (int i=0; i<mItems.size(); i++) {
            ItemInfo ii = mItems.get(i);
            if (ii.scrolling) {
                needPopulate = true;
                ii.scrolling = false;
            }
        }
        //這下面兩個,一個是觸發重繪,一個不是,但是都要執行mEndScrollRunnable,這個就是
        //調用我們的populate大法了,真不容易。
        if (needPopulate) {
            if (postEvents) {
                ViewCompat.postOnAnimation(this, mEndScrollRunnable);
            } else {
                mEndScrollRunnable.run();
            }
        }
    }
    
    

    看看mEndScrollRunnable實現,在前面手指抬起的時候,我們其實是沒有計算內存中的page頁的,有一個mPopulatePending狀態跳過了實際計算,所以在最后頁面滾動結束的時候來一次最終的計算,就是在這里了。

    private final Runnable mEndScrollRunnable = new Runnable() {
        public void run() {
            //設置SCROLL_STATE_IDLE狀態
            setScrollState(SCROLL_STATE_IDLE);
            //計算內存中的page緩存內容;
            populate();
        }
    };
    
    
6. 存在的問題
  1. 當同時設置viewpager的padding和page item之間的margin, page的marginDrawable會繪制在錯誤的地方,他累計了對應的對應的padding,這是錯誤的計算;

2. 在ScrollView中直接使用viewager,寬高不生效。原因是ScrollView給子view的測量規格模式是UNSPECIFIED,而我們的Viewpager測量又是setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), etDefaultSize(0, heightMeasureSpec))組合。解決也不是很難,只不過要針對不同的模式進行自定義測量策略,后面如果有時間,綜合寫一下系統控件各種測量存在的問題吧....

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。