ViewPager 源碼分析(一) —— setAdapter() 與 populate()
寫在前面
做安卓也有一定時間了,雖然常用控件都已大致掌握,然而隨著 Android N 的發布,不自覺的愈發焦慮起來。說來慚愧,Android L 的 Material Design 庫裏的許多控件都還沒用過,照這樣下去遲早要被新技術所淘汰。那該怎麽辦呢,偶然間我看到一篇博文如此說到:“不要覺得 android 裏邊控件繁雜多樣,官方或第三方新控件層出不窮,其實真正的控件就只有兩個View
和ViewGroup
。一旦有了它們的基礎,不管來什麽新控件,TabLayout
也好,CoordinatorLayout
也罷,花上一下午翻翻源碼基本就掌握了(不僅僅是會用而已)。”
我明白了:新技術的精華還在新技術之外
知識點
之所以選擇 ViewPager 是因為它常常用到,大家對它足夠熟悉。同時它有些難度,卻又是自定義View的官方經典例子,涵蓋了不少知識點:
- PagerAdapter、DataSetObserver 與觀察者模式
- View 的生命周期(measure -> layout -> draw)
- View 的事件分發(滑動沖突的解決)
- View 滑動的工具類 (Scroller、VelocityTracker 等)
- …
閱讀下文需要您已經有 ViewPager 、PagerAdapter 的使用經驗,同時對 View 的繪制和事件分發流程有一定的了解。由於篇幅有限,本文只寫到第一點;後幾點回以續章的形式呈現。
源碼分析
Adapter、DataSetObserver 與觀察者模式
我們使用 ViewPager
,通常需要定義一個PagerAdapter
,然後setAdapter()
,用法上和ListView
很像。如圖:
我們看到,PagerAdapter
持有數據集DataSetObservable
,同時包含一些回調。
setAdapter()
那麽很自然的,我們從ViewPager
的setAdapter
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 兩處,有兩個全局變量mRestoredCurItem
、mFirstLayout
不好理解,而且源碼沒有註釋。。。
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;
}
這基本上是說,初始化為true
,onLayout()
之後變為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()