1. 程式人生 > >ViewPager 源碼分析(一) —— setAdapter() 與 populate()

ViewPager 源碼分析(一) —— setAdapter() 與 populate()

which 比較 scrollto add 基本 tar 。。 除了 安卓

寫在前面


做安卓也有一定時間了,雖然常用控件都已大致掌握,然而隨著 Android N 的發布,不自覺的愈發焦慮起來。說來慚愧,Android L 的 Material Design 庫裏的許多控件都還沒用過,照這樣下去遲早要被新技術所淘汰。那該怎麽辦呢,偶然間我看到一篇博文如此說到:“不要覺得 android 裏邊控件繁雜多樣,官方或第三方新控件層出不窮,其實真正的控件就只有兩個ViewViewGroup。一旦有了它們的基礎,不管來什麽新控件,TabLayout也好,CoordinatorLayout也罷,花上一下午翻翻源碼基本就掌握了(不僅僅是會用而已)。”

我明白了:新技術的精華還在新技術之外

。拋開追尋新技術的浮躁,我決定補一補基礎,這也是我寫這篇文章的初衷。希望它能開一個好頭,勉勵自己沈下心來,read the fucking source code!

知識點


之所以選擇 ViewPager 是因為它常常用到,大家對它足夠熟悉。同時它有些難度,卻又是自定義View的官方經典例子,涵蓋了不少知識點:

  • PagerAdapter、DataSetObserver 與觀察者模式
  • View 的生命周期(measure -> layout -> draw)
  • View 的事件分發(滑動沖突的解決)
  • View 滑動的工具類 (Scroller、VelocityTracker 等)

閱讀下文需要您已經有 ViewPager 、PagerAdapter 的使用經驗,同時對 View 的繪制和事件分發流程有一定的了解。由於篇幅有限,本文只寫到第一點;後幾點回以續章的形式呈現。

源碼分析


Adapter、DataSetObserver 與觀察者模式

我們使用 ViewPager,通常需要定義一個PagerAdapter,然後setAdapter(),用法上和ListView很像。如圖:
技術分享圖片

我們看到,PagerAdapter持有數據集DataSetObservable,同時包含一些回調。

setAdapter()

那麽很自然的,我們從ViewPagersetAdapter

開始分析把。

public void setAdapter(PagerAdapter adapter) {
    if (mAdapter != null) { // 1: 清空舊的 Adapter, 做一些初始化處理
        mAdapter.unregisterDataSetObserver(mObserver);
        mAdapter.startUpdate(this);
        for (int i = 0; i < mItems.size(); i++) {
            final ItemInfo ii = mItems.get(i);
            mAdapter.destroyItem(this, ii.position, ii.object);
        }
        mAdapter.finishUpdate(this);
        mItems.clear();
        removeNonDecorViews();
        mCurItem = 0;
        scrollTo(0, 0);
    }

    // 2: 更新 mAdapter 字段
    final PagerAdapter oldAdapter = mAdapter;
    mAdapter = adapter;
    mExpectedAdapterCount = 0;

    // 3: 給 mAdapter 添加數據 mObserver,恢復狀態
    if (mAdapter != null) {
        if (mObserver == null) {
            mObserver = new PagerObserver();
        }
        // 3.1: 給 mAdapter 添加數據 mObserver
        mAdapter.registerDataSetObserver(mObserver);
        mPopulatePending = false;
        final boolean wasFirstLayout = mFirstLayout;
        mFirstLayout = true;
        mExpectedAdapterCount = mAdapter.getCount();
        if (mRestoredCurItem >= 0) { // 3.2: 之前有狀態保存下來,恢復狀態
            mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
            setCurrentItemInternal(mRestoredCurItem, false, true);
            mRestoredCurItem = -1;
            mRestoredAdapterState = null;
            mRestoredClassLoader = null;
        } else if (!wasFirstLayout) { 
            // 3.3: 沒狀態保存,且不是第一次被 Layout 出來 -> populate() 不知道要幹嘛。。
            populate();
        } else { // 3.4: 沒狀態保存,且是第一次被 Layout 出來 -> 重新布局
            requestLayout();
        }
    }
    // 4: 回調監聽器
    if (mAdapterChangeListener != null && oldAdapter != adapter) {
        mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter);
    }
}

前面都好理解,其中ItemInfo 保存了每一項的信息。然後,mItems其實是頁面的緩存,adapter變更的時候要先清空之前緩存。主要看 3.2 和 3.3 兩處,有兩個全局變量mRestoredCurItemmFirstLayout 不好理解,而且源碼沒有註釋。。。

1. mRestoredCurItem

如代碼所示,在onRestoreInstanceState的時候保存了當前選中狀態。

private int mRestoredCurItem = -1;

@Override
public void onRestoreInstanceState(Parcelable state) {
    ...
    if (mAdapter != null) { ...
    } else {
        mRestoredCurItem = ss.position;
        ...
    }
}

2. mFirstLayout
ctrl + F了一下,發現mFirstLayout在這些地方被賦值。

private boolean mFirstLayout = true;

public void setAdapter(PagerAdapter adapter) {
    ...
    mFirstLayout = true;
}

@Override
protected void onAttachedToWindow() {
    super.onAttachedToWindow();
    mFirstLayout = true;
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    ...
    mFirstLayout = false;
}

這基本上是說,初始化為trueonLayout()之後變為false,使得在setAdapter()裏:
如果已經onLayout()過一次,可以用populate()代替requestLayout()然後又重置了這個mFirstLayout
其實到這裏還是一頭霧水,這個populate()到底要幹嘛,源碼一點註釋都沒有,想要答案還得繼續分析。

populate()

先別急著看源碼,這段比較長,要怎麽分析呢。一個函數200多行,一開始我也懵逼了,多虧這片博客點醒了我:viewpager源碼分析
要關註PagerAdapter!!!是啊,繞來繞去怎麽把這茬忘了,我們就是從setAdapter()入手的,它才是我們的主角啊。這就好辦了,抓住它發現populate()幾乎把mAdapter的生命周期走了個遍。我用註釋 // —— A~F做了標記:

  • startUpdate()
  • getCount()
  • instantiateItem()
  • destroyItem()
  • setPrimaryItem()
  • finishUpdate()

這樣,populate()的職能便呼之欲出了。它主要根據制定的頁面緩存大小(mOffscreenPageLimit),做了頁面的銷毀和重建。除了,A~F這條線,還標註了0~2這條線。其中2部分有一些復雜的計算,主要做了頁面銷毀這項工作。本來還想分析一下calculatePageOffsets(),現在想來沒必要了。我們的主要目標Adapter已經被我們搞定,想必對於PageAdapter中頁面如何創建也有了進一步的認識。

void populate(int newCurrentItem) {
    ...
    mAdapter.startUpdate(this); // ------ A

    // 0: 設置頁數限制,[startPos, endPos]=>[mCurItem - pageLimit, mCurItem + pageLimit]
    // 對應 public void setOffscreenPageLimit(int limit);
    final int pageLimit = mOffscreenPageLimit;
    final int startPos = Math.max(0, mCurItem - pageLimit);
    final int N = mAdapter.getCount(); // ------ B
    final int endPos = Math.min(N-1, mCurItem + pageLimit);

    // 1: Locate the currently focused item or add it if needed.
    int curIndex = -1;
    ItemInfo curItem = null;
    for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
        final ItemInfo ii = mItems.get(curIndex);
        if (ii.position >= mCurItem) { // 1.1: 便利找到第一個大於 mCurItem 的位置
            if (ii.position == mCurItem) curItem = ii;
            break;
        }
    }

    // 1.2: 由於步驟0 處設置了緩存的頁數限制,mItems 中可能會找不到 curItem,
    // 需要 addNewItem 
    if (curItem == null && N > 0) {
        curItem = addNewItem(mCurItem, curIndex); // C: addNewItem()裏邊調用了 mAdapter.instantiateItem()
    }

    // Fill 3x the available width or up to the number of offscreen
    // pages requested to either side, whichever is larger.
    // If we have no current item we have no work to do.
    // 2: (譯)根據 mOffscreenPageLimit 這個參數(默認為1),
    // 決定保留的頁面範圍,即[startPos, endPos]
    if (curItem != null) {
        // 左邊範圍
        float extraWidthLeft = 0.f;
        int itemIndex = curIndex - 1;
        ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
        final int clientWidth = getClientWidth();
        final float leftWidthNeeded = clientWidth <= 0 ? 0 :
                2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
        for (int pos = mCurItem - 1; pos >= 0; pos--) {
            // 2.1: 逆序遍歷左邊,累加 extraWidthLeft,並與 leftWidthNeeded 比較
            // 同時,如果 pos 超出邊界[startPos, endPos], 則銷毀 view
            // 這裏的參數計算比較復雜,只看了個大概。。。
            if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
                if (ii == null) {
                    break;
                }
                if (pos == ii.position && !ii.scrolling) {
                    mItems.remove(itemIndex);
                    // ------ D
                    mAdapter.destroyItem(this, pos, ii.object);  // 2.2: 回調銷毀 view
                    ...
                    itemIndex--;
                    curIndex--;
                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                }
            } else if (ii != null && pos == ii.position) {
                extraWidthLeft += ii.widthFactor; // 2.3: 累加 extraWidthLeft
                itemIndex--;
                ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
            } else {
                ii = addNewItem(pos, itemIndex + 1);
                extraWidthLeft += ii.widthFactor; // 2.4: 累加 extraWidthLeft
                curIndex++;
                ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
            }
        }

        // 右邊情況與左邊完全對偶,不再詳細貼出
        ...

        // 2.6: 計算頁面偏移
        calculatePageOffsets(curItem, curIndex, oldCurInfo);
    }

    mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null); // ------ E
    mAdapter.finishUpdate(this); // ------ F

    // 下面兩部分分別是 LayoutParams 和 Focus 處理,
    // 感覺不太重要,已省略
}

總結


還是小看它了,ViewPager比我想像的要復雜。這一長篇才只分析到PagerAdapter,連DataSetObservable都沒引入。然而我已有些困意,未完待續。。。

ViewPager 源碼分析(一) —— setAdapter() 與 populate()