Android ViewPager源碼詳細分析
1.問題
由于Android Framework源碼很龐大,所以讀源碼必須帶著問題來讀!沒有問題,創(chuàng)造問題再來讀!否則很容易迷失在無數(shù)的方法與屬性之中,最后無功而返。
那么,關(guān)于ViewPager有什么問題呢?
1). setOffsreenPageLimit()方法是如何實現(xiàn)頁面緩存的?
2). 在布局文件中,ViewPager布局內(nèi)部能否添加其他View?
3). 為什么ViewPager初始化時,顯示了一個頁面卻不會觸發(fā)onPageSelected回調(diào)?
問題肯定不止這三個,但是有這三個問題基本可以找到本次分析的重點了。讀者朋友也可以自己先提出一些問題,再看下面的分析,看看是否可以從分析過程中找到答案。
2.從onMeasure()下手
ViewPager繼承自ViewGroup,是Android Framework提供的一個控件,而Android系統(tǒng)顯示控件的流程就是: Activity加載布局實例化所有控件 —> rootView遍歷所以控件 —> 對需要重繪的控件執(zhí)行測量,布局,繪制的操作。
而轉(zhuǎn)化到某個控件來說,它的流程就是:構(gòu)造方法 —> onMeasure —> onLayout —> onDraw
由于ViewPager的構(gòu)造方法中只是初始化了一些與本文主題無關(guān)的屬性就略過不講,那么自然而然onMeasure方法就來到了我們眼前。
那么在onMeasure中ViewPager做了些什么呢?先把源碼擺出來,我進行了一些刪減。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//測量ViewPager自身大小
setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
getDefaultSize(0, heightMeasureSpec));
final int measuredWidth = getMeasuredWidth();
// child的寬高,占滿父控件
int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight();
int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
//1.測量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進行測量
//省略若干代碼,主要負責對Decor控件的測量
...
}
}
}
mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);
// 2.從Adapter中獲取childView
mInLayout = true;
populate();
mInLayout = false;
// 3.測量非Decor的childView
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) {
final int widthSpec = MeasureSpec.makeMeasureSpec(
(int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);
child.measure(widthSpec, mChildHeightMeasureSpec);
}
}
}
}
簡單總結(jié)就是三件事情。
2.1 測量Decor控件
可能很多人有些懵x了,Decor是個啥?
其實Decor是一個接口,在ViewPager內(nèi)部定義的,并且該接口是沒有定義任何內(nèi)容的。唯一的作用就是如果你的控件實現(xiàn)了Decor接口,那么你的控件就屬于DecorView了。
我們知道ViewPager的數(shù)據(jù)是通過Adapter管理的,但其實還有一種方式給ViewPager添加childView.
#layout.xml <ViewPager> <DecorView /> </ViewPager>
上面這種直接在ViewPager布局內(nèi)部添加控件也是可以的,但是要求DecorView必須實現(xiàn)Decor接口,否則將不予顯示。
在ViewPager的addView方法中會對childView進行判斷,也看一下代碼吧!
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
if (!checkLayoutParams(params)) {
params = generateLayoutParams(params);
}
final LayoutParams lp = (LayoutParams) params;
lp.isDecor |= child instanceof Decor; //在此處給isDecor賦值
//省略無關(guān)代碼
...
}
至于addView()方法是如何調(diào)用,可以參考本人博客 ViewGroup如何加載布局中的View?
而上面的代碼我們要注意的是lp.isDecor,這是ViewPager為它的childView準備的LayoutParams,在onMeasure的第一步中就是根據(jù)lp.isDecor來挑選出Decor控件來測量的。
至于Decor的測量過程與本文主題無關(guān),在此就不詳述了,有興趣的可以自己去查看源碼。
2.2 從Adapter中創(chuàng)建ChildView(populate方法)
ViewPager也是采用Observable模式來設(shè)計的,數(shù)據(jù)通過PagerAdapter來管理,并且childView也是通過PagerAdapter來創(chuàng)建的,ViewPager主要負責界面交互相關(guān)的工作。
對PagerAdapter并不會做太詳細的介紹,直接給一個示例代碼吧。
public class AutoScrollAdapter extends PagerAdapter {
//省略構(gòu)造方法代碼
...
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
}
@Override
public int getCount() {
return mData.size();
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
View itemView = new TextView(mContext); //通過各種方法新建一個childView
container.addView(itemView);//將childView添加到ViewPager中
return itemView;
}
}
這四個方法是必須要重寫的,方法的含義根據(jù)方法名就能看出來。這里主要要講一下最后這個方法instantiateItem()。它負責向ViewPager提供childView,這里調(diào)用的addView方法是被ViewPager重寫過的,所以會對lp.isDecor賦值,并且我們可以知道,這里的isDecor=false。
有些人可能要問,這一步的主角不應(yīng)該是populate()方法嗎?的確應(yīng)該是populate方法,但是由于這個方法比較復雜,為了閱讀的連貫性考慮,博主決定單獨提出來,一會兒再講它。
在這里主要告訴大家,populate()方法內(nèi)部會調(diào)用Adapter.instantiateItem()方法,也就是將Adapter中的childView添加到ViewPager中來,為下一步做準備。
2.3 測量ChildView
有了上面的分析,這一步的內(nèi)容就很好理解了。
簡單來說就是,遍歷所有的childView,挑選出lp.isDecor==false的childView,然后調(diào)用view.measure()方法讓childView自己去完成測量。
還有一點需要注意,就是childView的寬度 width= childWidthSize * lp.widthFactor。
childWidthSize就是ViewPager的寬度,lp.widthFactor代表這個childView占幾個頁面。
lp.widthFactor默認情況下是1.0,可以重寫PagerAdapter.getPageWidth(pos)方法來修改這個值。
到此,ViewPager的測量過程就完成了。
3.populate()方法
可以說這是ViewPager最核心的一個方法,所以單獨作為一個小節(jié)來分析。
在分析源碼之前,必須先介紹一個類——ItemInfo
3.1 ItemInfo是什么?
static class ItemInfo {
Object object; //childView
int position; //childView在Adapter中的位置
boolean scrolling; //是否在滾動
float widthFactor; //寬度的倍數(shù),默認情況下是1
float offset; //頁面的偏移參數(shù),粗暴的理解就是第幾個頁面
}
這是ViewPager內(nèi)部定義的一個靜態(tài)類,將childView相關(guān)的屬性進行了包裝,主要是為了方便對childView的管理。
并且在ViewPager內(nèi)部還維護了一個ArrayList,由ItemInfo對象組成,屬性名是mItems。
這個list的長度就是由mOffscreenPageLimit來決定的,這個在后面的代碼分析中會看到。
好了,了解了基本對象之后,就可以開始分析populate方法了。
注意:由于代碼比較長,為了方便閱讀博主打算將populate()方法的代碼分段講解,如過代碼中沒有方法聲明,則表示該段代碼屬于populate()方法。
3.2 獲取當前的ItemInfo對象
從這里開始,對populate()方法的源碼進行分析,分析內(nèi)容主要在代碼的注釋中編寫。
void populate(int newCurrentItem) {
ItemInfo oldCurInfo = null;
int focusDirection = View.FOCUS_FORWARD;
if (mCurItem != newCurrentItem) {
focusDirection = mCurItem < newCurrentItem ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
oldCurInfo = infoForPosition(mCurItem); //獲取舊的ItemInfo對象
mCurItem = newCurrentItem; //更新mCurItem的值,就是在Adapter中的position
}
//省略無關(guān)代碼
...
//mOffscreenPageLimit就是setOffscreenPageLimit方法設(shè)置的值
final int pageLimit = mOffscreenPageLimit;
//根據(jù)下面三行代碼可知:mItems的長度就是 2 * pageLimit + 1
//這里聲明的startPos和endPos在后面會起作用,大家注意一下
final int startPos = Math.max(0, mCurItem - pageLimit);
final int N = mAdapter.getCount();
final int endPos = Math.min(N-1, mCurItem + pageLimit);
// 遍歷mItems列表,找出mCurItem對應(yīng)的ItemInfo對象,是根據(jù)position來判斷的
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;
}
}
// 如果mItems中還未保存該ItemInfo,則創(chuàng)建一個IntemInfo對象
if (curItem == null && N > 0) {
curItem = addNewItem(mCurItem, curIndex);
}
...
這里要注意的一點是,在新建ItemInfo對象時,我們是調(diào)用的addNewItem方法,它的代碼如下所示。
ItemInfo addNewItem(int position, int index) {
ItemInfo ii = new ItemInfo(); //新建一個ItemInfo對象
ii.position = position;
ii.object = mAdapter.instantiateItem(this, position);//用Adapter創(chuàng)建一個childView
ii.widthFactor = mAdapter.getPageWidth(position);//默認返回1.0f
if (index < 0 || index >= mItems.size()) { //添加到mItems中
mItems.add(ii);
} else {
mItems.add(index, ii);
}
return ii;
}
不管是從mItems中提取還是新建一個ItemInfo對象,總之我們已經(jīng)得到了curItem,即當前的IntemInfo對象。
3.3 管理mItems中的其余對象
因為我們的mItems長度是有限的,并且與pageLimit有關(guān),所以很可能出現(xiàn)頁面總數(shù)大于mItems長度的情況。當顯示的頁面改變時,我們必須將一些ItemInfo添加進來,將另一些ItemInfo移除。
以保證我們的mItems中的ItemInfo.position是這樣的:
[ startPos … mCurItem … endPos ]
其中:
mCurItem = curItem.position
startPos = mCurItem - pagLimit
endPos = mCurItem + pagLimit
具體如何操作,我們來看代碼
if (curItem != null) {
//1.調(diào)整curItem左邊的對象
float extraWidthLeft = 0.f;
// curIndex是curItem在mItems中的索引
// itemIndex就是curItem左邊的ItemInfo的索引
int itemIndex = curIndex - 1;
//獲取左邊的ItemInfo對象
ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
final int clientWidth = getClientWidth();
//curItem左邊需要的寬度,默認情況下為1.0f
final float leftWidthNeeded = clientWidth <= 0 ? 0 :
2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
//遍歷mItems左半部分,即curIndex左邊的對象
//只有在pos < startPos時才能退出循環(huán),否則會一直遍歷到pos=0
for (int pos = mCurItem - 1; pos >= 0; pos--) {
// 建議大家先從下面的else if開始看,因為這里的邏輯是準備退出循環(huán)了
if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
//當pos < startPos,說明mItems左邊部分已經(jīng)調(diào)整完畢了
//此時的ii代表的是,startPos左邊的對象了
if (ii == null) {
break;
}
//如果startPos左邊還有對象,需要從mItems中移除
if (pos == ii.position && !ii.scrolling) {
mItems.remove(itemIndex);
mAdapter.destroyItem(this, pos, ii.object);
itemIndex--;
curIndex--;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
}
//如果curIndex左邊的ItemInfo對象不為null
} else if (ii != null && pos == ii.position) {
extraWidthLeft += ii.widthFactor; //累加curItem左邊需要的寬度
itemIndex--; //再往curIndex左邊移一個位置
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; //取出ItemInfo對象
//如果curIndex左邊的ItemInfo為null
} else {
//新建一個ItemInfo對象,添加到itemIndex的右邊
ii = addNewItem(pos, itemIndex + 1);
extraWidthLeft += ii.widthFactor; //累加左邊寬度
curIndex++; //由于往mItems中插入了一個對象,故curIndex需要加1
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; //去除ItemInfo
}
}
//2.調(diào)整curItem右邊的對象,邏輯與上面類似
//代碼省略
...
// 3.計算mItems中的偏移參數(shù)
calculatePageOffsets(curItem, curIndex, oldCurInfo);
}
代碼主要是一些邏輯,需要大家靜下心來讀,也不知道講清除了沒有。(發(fā)現(xiàn)要把代碼翻譯成文字真是累,一句代碼要用一大段文字來說明)
對于calculatePageOffsets方法,就不貼源碼分析了,主要說一下它做了哪些事情吧
根據(jù)oldItem.position與curItem.position的大小關(guān)系,來確定curItem的offset值
再分別對curItem的左邊和右邊的Item寫入offset值
mPageMargin是頁面之間的間隔, marginOffset = mPageMargin / childWidth
每個頁面的offset = mAdapter.getPageWidth(pos) + marginOffset
參照上面的四點提示,大家去讀源碼應(yīng)該也沒啥難度的,關(guān)鍵是都是一些邏輯處理很難文字化說明。
3.4 一些收尾工作
// 將ItemInfo的內(nèi)容更新到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) {
final ItemInfo ii = infoForChild(child);
if (ii != null) {
lp.widthFactor = ii.widthFactor;
lp.position = ii.position;
}
}
}
//根據(jù)lp.position的大小對所有childView進行排序,另外DecorView是排在其他child之前的
sortChildDrawingOrder();
OK,populate方法分析到此就結(jié)束了。
4. onLayout
布局也是先布局Decor,再布局Adapter創(chuàng)建的childView,直接上源碼吧。
@Override
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;
//1.布局Decor,根據(jù)lp.isDecor來篩選DecorView
//代碼略
...
final int childWidth = width - paddingLeft - paddingRight;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
ItemInfo ii;
//此處將DecorView過濾掉,并且根據(jù)view從mItems中查找ItemInfo對象
//如果ViewPager布局中添加了未實現(xiàn)Decor接口的控件,將不會被布局
//因為無法從mItems中查找到ItemInfo對象
if (!lp.isDecor && (ii = infoForChild(child)) != null) {
//計算當前page的左邊界偏移值,此處的offset會隨著頁面增加而增加
int loff = (int) (childWidth * ii.offset);
int childLeft = paddingLeft + loff;
int childTop = paddingTop;
if (lp.needsMeasure) {//如果需要重新測量,則重新測量之
lp.needsMeasure = false;
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);
}
//child調(diào)用自己的layout方法來布局自己
child.layout(childLeft, childTop,
childLeft + child.getMeasuredWidth(),
childTop + child.getMeasuredHeight());
}
}
}
mTopPageBounds = paddingTop;
mBottomPageBounds = height - paddingBottom;
mDecorChildCount = decorCount;
//如果是首次布局,則會調(diào)用scrollToItem方法
if (mFirstLayout) {
scrollToItem(mCurItem, false, 0, false);
}
mFirstLayout = false;
}
布局這一塊的代碼相對來說要簡單一些,就是根據(jù)offset偏移量來計算出left,right, top, bottom值,然后直接調(diào)用View.layout方法進行布局。
但是,這里需要插一句,在用ViewPager實現(xiàn)輪播控件時,有一種方法是將Adapter.getCount返回Integer.MAX_VALUE,已達到偽循環(huán)播放的目的。從上面的代碼可以看到,此時這個offset值會不斷的變大,那么
int loff = (int) (childWidth * ii.offset);
這個loff很可能會超出int的最大值邊界。
所以,以后大家實現(xiàn)輪播控件時,還是不要采用這種方法了。
然后,回過頭來再說下scrollToItem方法
注意上面調(diào)用scrollToItem時,最后一個參數(shù)傳遞的是false,而這個參數(shù)就是決定是否調(diào)用onPageSelected回調(diào)函數(shù)的。
看代碼:
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) {
smoothScrollTo(destX, 0, velocity);
if (dispatchSelected) {
dispatchOnPageSelected(item);
}
} else {
if (dispatchSelected) { //是否需要分發(fā)OnPageSelected回調(diào)
dispatchOnPageSelected(item);
}
completeScroll(false);
scrollTo(destX, 0);
pageScrolled(destX);
}
}
也就是說,第一次布局ViewPager時雖然會顯示一個頁面,卻不會調(diào)用onPageSelected方法。
onLayout的分析也到此結(jié)束了,至于onDraw方法ViewPager并沒有做什么,只是編寫了繪制Page之間間隔的代碼,就不做分析了。
當然,ViewPager的代碼還不止這些,此文分析的僅僅是它的骨架,還有許多其他處理如onInterceptTouchEvent方法,pageScrolled方法等等,這些就留給讀者自己去分析吧。
理解了這篇文章之后,對ViewPager的工作原理也有一定程度的了解了,相信再去讀那些代碼難度不會很大。
至于篇頭提到的三個問題,相信各位也已經(jīng)有了答案。
- Android利用ViewPager實現(xiàn)滑動廣告板實例源碼
- Android ViewPager相冊橫向移動的實現(xiàn)方法
- android教程viewpager自動循環(huán)和手動循環(huán)
- Android 使用viewpager實現(xiàn)無限循環(huán)(定時+手動)
- Android 開發(fā)之BottomBar+ViewPager+Fragment實現(xiàn)炫酷的底部導航效果
- android配合viewpager實現(xiàn)可滑動的標簽欄示例分享
- Android App中ViewPager與Fragment結(jié)合的一些問題解決
- Android App中使用ViewPager+Fragment實現(xiàn)滑動切換效果
- Android 使用ViewPager自動滾動循環(huán)輪播效果
- Android viewpager中動態(tài)添加view并實現(xiàn)偽無限循環(huán)的方法
相關(guān)文章
Android 系統(tǒng)net和wap接入點的區(qū)別
這篇文章主要介紹了Android 系統(tǒng)net和wap接入點的區(qū)別的相關(guān)資料,需要的朋友可以參考下2016-09-09
AndroidHttpClient詳解及調(diào)用示例
本文給大家介紹AndroidHttpClient結(jié)構(gòu)、使用方式及調(diào)用示例詳解,需要的朋友可以參考下2015-10-10
Android自定義控件實現(xiàn)簡單滑動開關(guān)效果
這篇文章主要為大家詳細介紹了Android自定義控件實現(xiàn)簡單滑動開關(guān)效果,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-02-02
Activity isFinishing()判斷Activity的狀態(tài)實例
下面小編就為大家分享一篇Activity isFinishing()判斷Activity的狀態(tài)實例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-03-03

