最近想擼一個(gè)垂直方向的VerticalViewPager济炎,如果想要把它做到屌川抡,那自然是要參考下現(xiàn)有我們的ViewPager實(shí)現(xiàn)。
該篇從ViewPager的measure與layout著手须尚,解讀ViewPager如何來實(shí)現(xiàn)自身已經(jīng)childView的測(cè)量與布局崖堤。
onMeasure():
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//直接設(shè)置ViewPager自身的大小,這里可以看出ViewPager設(shè)置wrap_content時(shí)不起作用
setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
getDefaultSize(0, heightMeasureSpec));
//mGutterSize是在事件分發(fā)時(shí)用來判斷是否拖拽的一個(gè)閾值耐床,影響是否攔截事件密幔。
final int measuredWidth = getMeasuredWidth();
final int maxGutterSize = measuredWidth / 10;
mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize);
// 設(shè)置完寬高后計(jì)算出ViewPager實(shí)際可用顯示的寬高
int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight();
int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
/**
* 以下為測(cè)量DecorView的過程
* 這里的 DecorView是ViewPager內(nèi)部定義的注解
* ViewPager還可以通過xml布局添加item
* <ViewPager>
* <DecorView/>
* </ViewPager>
* 所以DecorView是用來表示在xml布局中添加的子view,在xml布局中添加子view時(shí)需要添加@DecorView注解
*/
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) {
//這里省略DecorView的具體測(cè)量過程
//…………
//ViewPager實(shí)際可用顯示的寬高減去DecorView已占用的寬高
if (consumeVertical) {
childHeightSize -= child.getMeasuredHeight();
} else if (consumeHorizontal) {
childWidthSize -= child.getMeasuredWidth();
}
}
}
}
//計(jì)算去除decorView占位后的寬高測(cè)量模式(用于adapter中添加的ChildView)
mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);
/**
* populate()方法是ViewPager的一個(gè)核心方法了撩轰,它里面有矯正當(dāng)前ViewPager上需要加載的ItemInfo胯甩,
* 下面會(huì)詳細(xì)分析【ItemInfo】^①和【populate()】^③
*/
mInLayout = true;
populate();
mInLayout = false;
/* *
* 測(cè)量非DecorView的子View(通過Adapter添加的ChildView)
* 上面通過populate()已經(jīng)矯正過需要加載的Item,
* 所以下面的循環(huán)的getChildCount并不會(huì)等于DecorView的數(shù)量+Adapter.getCount()
* (實(shí)際情況是: getChildCount() = DecorView數(shù)量+mItems.size()堪嫂,
* mItem中存放當(dāng)前ViewPager需要加載的ItemInfo偎箫,populate()后會(huì)得到最新的mItems)
*/
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);
}
//這里的LayoutParams為【ViewPager.LayoutParams】^②,下面會(huì)詳細(xì)分析
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//這個(gè)lp == null嚴(yán)重懷疑是個(gè)筆誤,應(yīng)該是lp != null
//同時(shí)插一點(diǎn)lp.widthFactor皆串,可以看到該值會(huì)直接決定ViewPager中ChildView的顯示寬度淹办。
//可以通過復(fù)寫Adapter.getPageWidth()方法返回值改變?cè)撝怠? if (lp == null || !lp.isDecor) {
final int widthSpec = MeasureSpec.makeMeasureSpec(
(int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);
child.measure(widthSpec, mChildHeightMeasureSpec);
}
}
}
}
總結(jié)一下onMeasure()的整體流程:
1、設(shè)置ViewPager自身的大杏拚健娇唯;
2、算出可用大小后為每個(gè)子DecorView測(cè)量寂玲;
3塔插、通過populate()方法調(diào)整ViewPager當(dāng)前需要加載的itemInfo
4、為當(dāng)前ViewPager加載的所有非DecorView測(cè)量拓哟。
接下來分析onMeasure預(yù)留的3個(gè)重要對(duì)象或方法:
①:ItemInfo
ItemInfo用來保存當(dāng)前ViewPager需要加載的子View的相關(guān)信息想许。
static class ItemInfo {
Object object;//childView,該值為Adapter.instantiateItem()的返回值
int position;//childView在viewPager中的位置
boolean scrolling;//是否在滾動(dòng)
float widthFactor;//寬度的占比断序,可以通過復(fù)寫Adapter.getPageWidth()方法返回值改變?cè)撝盗魑疲J(rèn)返回1。
float offset;//頁面的偏移违诗,用來決定layout時(shí)childView的位置
}
②:ViewPager.LayoutParams
ViewPager.LayoutParams中主要關(guān)心以下幾個(gè)重要字段漱凝。
//是否為DecorView
public boolean isDecor;
// childView寬度與ViewPager寬度的比值(實(shí)際是可以超過1的)
float widthFactor = 0.f;
// 與Adapter中position相對(duì)應(yīng),DecorView不用關(guān)心該字段
int position;
// childView在mItems中的位置
int childIndex;
③:populate()
populate()方法較長(zhǎng)诸迟,邏輯代碼較多茸炒,不過耐心點(diǎn)閱讀難度也不高愕乎。
void populate(int newCurrentItem) {
/**
*省略部分代碼
*……
*/
/**
* 這里mOffscreenPageLimit決定緩存數(shù)量,
* 可以通過setOffscreenPageLimit()方法來該表該值
* mItems最多只會(huì)緩存1(mCurItem)+2*mOffscreenPageLimit個(gè)item,
* mCurItem左右不足mOffscreenPageLimit個(gè)item時(shí)則達(dá)不到最大數(shù)量壁公。
* 以下starPos與endPos分別表示mItems中緩存的起始與結(jié)束position
*/
final int pageLimit = mOffscreenPageLimit;
final int startPos = Math.max(0, mCurItem - pageLimit);
final int N = mAdapter.getCount();
final int endPos = Math.min(N - 1, mCurItem + pageLimit);
if (N != mExpectedAdapterCount) {
String resName;
try {
/**
* 知識(shí)點(diǎn)感论,系統(tǒng)提供了根據(jù)id獲取id名字的方法(之前傻傻反射去獲取)
*/
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());
}
/**
* 在mItems中找到當(dāng)前的ItemInfo
* 這里需要區(qū)分mCurItem和curIndex
* mCurItem表示當(dāng)前item對(duì)應(yīng)所有item的position
* curIndex表示當(dāng)前itemInfo對(duì)應(yīng)items中的位置
*/
int curIndex = -1;
ItemInfo curItem = null;
for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
final ItemInfo ii = mItems.get(curIndex);
if (ii.position >= mCurItem) {
if (ii.position == mCurItem) curItem = ii;
break;
}
}
/**
* 如果當(dāng)前未找到對(duì)應(yīng)的ItemInfo,
* addNewItem()方法會(huì)調(diào)用Adapter.instantiateItem()方法往ViewPager中添加對(duì)應(yīng)mCurItem的childView,
* 同時(shí)根據(jù)Adapter.instantiateItem()的返回值創(chuàng)建對(duì)應(yīng)的ItemInfo并添加到mItems
*/
if (curItem == null && N > 0) {
curItem = addNewItem(mCurItem, curIndex);
}
// 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.
if (curItem != null) {
float extraWidthLeft = 0.f;
int itemIndex = curIndex - 1;
//獲取當(dāng)前ItemInfo的左邊的ItemInfo li紊册,下面循環(huán)從這里開始
ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
//獲取到viewPager真實(shí)可用寬度
final int clientWidth = getClientWidth();
/**
* leftWidthNeeded表示左邊打到回收條件的寬度比例的閾值比肄,
* 以下處理三種場(chǎng)景
* 場(chǎng)景1:當(dāng)左邊ItemInfo的extraWidthLeft超過leftWidthNeeded,
* 且對(duì)應(yīng)的位置不在上面計(jì)算的startPos-endPos范圍內(nèi)時(shí)將從mItems中移除(如果存在)
* 同時(shí)調(diào)用Adapter.destroyItem()通知Adapter回收itemInfo對(duì)應(yīng)的view
* 場(chǎng)景2:在顯示范圍內(nèi)且存在囊陡,繼續(xù)下一次循環(huán)
* 場(chǎng)景3:在顯示范圍內(nèi)且不存在芳绩,則通過addNewItem()方法調(diào)用Adapter.instantiateItem()方法
* 往ViewPager中添加對(duì)應(yīng)mCurItem的childView,
* 同時(shí)根據(jù)Adapter.instantiateItem()的返回值創(chuàng)建對(duì)應(yīng)的ItemInfo并添加到mItems
*/
final float leftWidthNeeded = clientWidth <= 0 ? 0 :
2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
/**
* 遍歷mCurItem左邊的ItemInfo
*/
for (int pos = mCurItem - 1; pos >= 0; pos--) {
//場(chǎng)景1
if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
if (ii == null) {
break;
}
if (pos == ii.position && !ii.scrolling) {
mItems.remove(itemIndex);
//移除緩存時(shí)回調(diào) mAdapter.destroyItem
mAdapter.destroyItem(this, pos, ii.object);
if (DEBUG) {
Log.i(TAG, "populate() - destroyItem() with pos: " + pos
+ " view: " + ((View) ii.object));
}
itemIndex--;
curIndex--;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
}
//場(chǎng)景2
} else if (ii != null && pos == ii.position) {
extraWidthLeft += ii.widthFactor;
itemIndex--;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
//場(chǎng)景3
} else {
ii = addNewItem(pos, itemIndex + 1);
extraWidthLeft += ii.widthFactor;
curIndex++;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
}
}
/**
* 以下為右遍歷,邏輯同以上左遍歷
*/
float extraWidthRight = curItem.widthFactor;
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;
}
}
}
/**
* 這里在整理好緩存items后撞反,計(jì)算每個(gè)items里的{@link ItemInfo#offset}偏移量
*/
calculatePageOffsets(curItem, curIndex, oldCurInfo);
}
/**
* 這里是知識(shí)點(diǎn)示括,adapter會(huì)講當(dāng)前的{@link ItemInfo#object}通過setPrimaryItem回調(diào)給adapter,
* 這個(gè)curItem.object就是Adapter中instantiateItem(ViewGroup container, int position)方法的返回值痢畜,一般都會(huì)返回我們添加的chilView
* 所以在我們需要獲取ViewPager當(dāng)前的childView時(shí)垛膝,我們可以在Adapter中可以復(fù)寫setPrimaryItem方法將curItem.object保存起來,
*/
mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);
mAdapter.finishUpdate(this);
/**
* 將以上更新的ItemInfo中的內(nèi)容更新到對(duì)應(yīng)childView的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;
}
}
}
sortChildDrawingOrder();
//焦點(diǎn)傳遞
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(View.FOCUS_FORWARD)) {
break;
}
}
}
}
}
}
populate()代碼稍微有些長(zhǎng)丁稀,但是核心邏輯也不難理解吼拥,它實(shí)現(xiàn)了ViewPager的添加childView和刪除childView的功能,大致流程為:
1线衫、根據(jù)mOffscreenPageLimit計(jì)算mItems將要儲(chǔ)存ViewPager中startPos~endPos對(duì)應(yīng)的ItemInfo凿可;
2、從mItems中獲取當(dāng)前mCurItem對(duì)應(yīng)的ItemInfo授账,若沒有則通過addNewItem()方法調(diào)用Adapter.instantiateItem()為ViewPager添加mCurItem位置的childView枯跑。同時(shí)創(chuàng)建該位置對(duì)應(yīng)的ItemInfo并添加到mItems中;
3白热、循環(huán)遍歷當(dāng)前item左邊的所有item敛助,若不在leftWidthNeeded與startPos-endPos決定范圍內(nèi),則從mItems中刪除屋确,同時(shí)調(diào)用Adapter.destroyItem()方法來通知Adapter回收itemInfo對(duì)應(yīng)的view纳击。若在該范圍內(nèi)且不存在時(shí)則通過addNewItem()方法調(diào)用Adapter.instantiateItem()方法 往ViewPager中添加對(duì)應(yīng)mCurItem的childView,同時(shí)根據(jù)Adapter.instantiateItem()的返回值創(chuàng)建對(duì)應(yīng)的ItemInfo并添加到mItems;
4攻臀、循環(huán)遍歷當(dāng)前item右邊的所有item焕数,邏輯與左循環(huán)基本一致;
5刨啸、通過以上流程整理好需要加載的mItems后堡赔,計(jì)算mItems里的每個(gè)ItemInfo的偏移量,用來決定layout時(shí)的位置
6设联、最后將以上更新的ItemInfo中的內(nèi)容更新到對(duì)應(yīng)childView的LayoutParams中
populate()方法決定了ViewPager添加childView以及刪除childView善已,在整個(gè)ViewPager源碼中多個(gè)方法中調(diào)用存璃,有:onMeasure()、setAdapter()雕拼、onInterceptTouchEvent()、setAdapter()粘招、setPageTransformer()啥寇、setOffscreenPageLimit()、smoothScrollTo()等多個(gè)涉及到頁面發(fā)生變化的方法中調(diào)用洒扎,由此可見它在ViewPager中時(shí)非常重要的一個(gè)方法辑甜,所以好好理解它還是非常有必要的。
onLayout():
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//……略略略……
// 首先對(duì)子DecorView進(jìn)行l(wèi)ayout
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) {
//……略略略……
childLeft += scrollX;
child.layout(childLeft, childTop,
childLeft + child.getMeasuredWidth(),
childTop + child.getMeasuredHeight());
decorCount++;
}
}
}
//對(duì)Adapter中添加的childView進(jìn)行l(wèi)ayout
final int childWidth = width - paddingLeft - paddingRight;
// Page views. Do this once we have the right padding offsets from above.
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
ItemInfo ii;
if (!lp.isDecor && (ii = infoForChild(child)) != null) {
//……略略略……
child.layout(childLeft, childTop,
childLeft + child.getMeasuredWidth(),
childTop + child.getMeasuredHeight());
}
}
}
mTopPageBounds = paddingTop;
mBottomPageBounds = height - paddingBottom;
mDecorChildCount = decorCount;
//首次layout需要將viewPager滾動(dòng)到對(duì)應(yīng)的mCurItem位置
if (mFirstLayout) {
scrollToItem(mCurItem, false, 0, false);
}
mFirstLayout = false;
}
可以看到onLayout()方法還是比較簡(jiǎn)單的袍冷,通過循環(huán)DecorView與非DecorView(Adapter中添加的childView)結(jié)合他們對(duì)應(yīng)的LayoutParams磷醋,對(duì)所有childView進(jìn)行l(wèi)ayout。
到這里ViewPager的onMeasure與onLayout就分析完了胡诗,重要的事情最后在提醒一次整個(gè)流程的重點(diǎn)就在populate()方法的流程邓线,populate()也是整個(gè)ViewPager的一個(gè)核心方法,還是要耐心將它讀完的煌恢。
到這里ViewPager的onMeasure骇陈、onLayout、populate就已經(jīng)分析完了瑰抵,下一篇《ViewPager源碼解析(二):setAdapter,notifyDataSetChanged》
會(huì)進(jìn)一步分析ViewPager的數(shù)據(jù)綁定與刷新你雌。