在上一篇文章中,我?guī)е蠹乙黄鹌饰隽艘幌翷ayoutInflater的工作原理,可以算是對(duì)View進(jìn)行深入了解的第一步吧。那么本篇文章中,我們將繼續(xù)對(duì)View進(jìn)行深入探究,看一看它的繪制流程到底是什么樣的。如果你還沒(méi)有看過(guò)我的上一篇文章,可以先去閱讀Android LayoutInflater原理分析,帶你一步步深入了解View(一)。
相信每個(gè)Android程序員都知道,我們每天的開(kāi)發(fā)工作當(dāng)中都在不停地跟View打交道,Android中的任何一個(gè)布局、任何一個(gè)控件其實(shí)都是直接或間接繼承自View的,如TextView、Button、ImageView、ListView等。這些控件雖然是Android系統(tǒng)本身就提供好的,我們只需要拿過(guò)來(lái)使用就可以了,但你知道它們是怎樣被繪制到屏幕上的嗎?多知道一些總是沒(méi)有壞處的,那么我們趕快進(jìn)入到本篇文章的正題內(nèi)容吧。
要知道,任何一個(gè)視圖都不可能憑空突然出現(xiàn)在屏幕上,它們都是要經(jīng)過(guò)非常科學(xué)的繪制流程后才能顯示出來(lái)的。每一個(gè)視圖的繪制過(guò)程都必須經(jīng)歷三個(gè)最主要的階段,即onMeasure()、onLayout()和onDraw(),下面我們逐個(gè)對(duì)這三個(gè)階段展開(kāi)進(jìn)行探討。
measure是測(cè)量的意思,那么onMeasure()方法顧名思義就是用于測(cè)量視圖的大小的。View系統(tǒng)的繪制流程會(huì)從ViewRoot的performTraversals()方法中開(kāi)始,在其內(nèi)部調(diào)用View的measure()方法。measure()方法接收兩個(gè)參數(shù),widthMeasureSpec和heightMeasureSpec,這兩個(gè)值分別用于確定視圖的寬度和高度的規(guī)格和大小。
MeasureSpec的值由specSize和specMode共同組成的,其中specSize記錄的是大小,specMode記錄的是規(guī)格。specMode一共有三種類型,如下所示:
1. EXACTLY
表示父視圖希望子視圖的大小應(yīng)該是由specSize的值來(lái)決定的,系統(tǒng)默認(rèn)會(huì)按照這個(gè)規(guī)則來(lái)設(shè)置子視圖的大小,開(kāi)發(fā)人員當(dāng)然也可以按照自己的意愿設(shè)置成任意的大小。
2. AT_MOST
表示子視圖最多只能是specSize中指定的大小,開(kāi)發(fā)人員應(yīng)該盡可能小得去設(shè)置這個(gè)視圖,并且保證不會(huì)超過(guò)specSize。系統(tǒng)默認(rèn)會(huì)按照這個(gè)規(guī)則來(lái)設(shè)置子視圖的大小,開(kāi)發(fā)人員當(dāng)然也可以按照自己的意愿設(shè)置成任意的大小。
3. UNSPECIFIED
表示開(kāi)發(fā)人員可以將視圖按照自己的意愿設(shè)置成任意的大小,沒(méi)有任何限制。這種情況比較少見(jiàn),不太會(huì)用到。
那么你可能會(huì)有疑問(wèn)了,widthMeasureSpec和heightMeasureSpec這兩個(gè)值又是從哪里得到的呢?通常情況下,這兩個(gè)值都是由父視圖經(jīng)過(guò)計(jì)算后傳遞給子視圖的,說(shuō)明父視圖會(huì)在一定程度上決定子視圖的大小。但是最外層的根視圖,它的widthMeasureSpec和heightMeasureSpec又是從哪里得到的呢?這就需要去分析ViewRoot中的源碼了,觀察performTraversals()方法可以發(fā)現(xiàn)如下代碼:
[java]view plaincopy
childWidthMeasureSpec?=?getRootMeasureSpec(desiredWindowWidth,?lp.width);
childHeightMeasureSpec?=?getRootMeasureSpec(desiredWindowHeight,?lp.height);
可以看到,這里調(diào)用了getRootMeasureSpec()方法去獲取widthMeasureSpec和heightMeasureSpec的值,注意方法中傳入的參數(shù),其中l(wèi)p.width和lp.height在創(chuàng)建ViewGroup實(shí)例的時(shí)候就被賦值了,它們都等于MATCH_PARENT。然后看下getRootMeasureSpec()方法中的代碼,如下所示:
[java]view plaincopy
privateintgetRootMeasureSpec(intwindowSize,introotDimension)?{
intmeasureSpec;
switch(rootDimension)?{
caseViewGroup.LayoutParams.MATCH_PARENT:
measureSpec?=?MeasureSpec.makeMeasureSpec(windowSize,?MeasureSpec.EXACTLY);
break;
caseViewGroup.LayoutParams.WRAP_CONTENT:
measureSpec?=?MeasureSpec.makeMeasureSpec(windowSize,?MeasureSpec.AT_MOST);
break;
default:
measureSpec?=?MeasureSpec.makeMeasureSpec(rootDimension,?MeasureSpec.EXACTLY);
break;
}
returnmeasureSpec;
}
可以看到,這里使用了MeasureSpec.makeMeasureSpec()方法來(lái)組裝一個(gè)MeasureSpec,當(dāng)rootDimension參數(shù)等于MATCH_PARENT的時(shí)候,MeasureSpec的specMode就等于EXACTLY,當(dāng)rootDimension等于WRAP_CONTENT的時(shí)候,MeasureSpec的specMode就等于AT_MOST。并且MATCH_PARENT和WRAP_CONTENT時(shí)的specSize都是等于windowSize的,也就意味著根視圖總是會(huì)充滿全屏的。
介紹了這么多MeasureSpec相關(guān)的內(nèi)容,接下來(lái)我們看下View的measure()方法里面的代碼吧,如下所示:
[java]view plaincopy
publicfinalvoidmeasure(intwidthMeasureSpec,intheightMeasureSpec)?{
if((mPrivateFlags?&?FORCE_LAYOUT)?==?FORCE_LAYOUT?||
widthMeasureSpec?!=?mOldWidthMeasureSpec?||
heightMeasureSpec?!=?mOldHeightMeasureSpec)?{
mPrivateFlags?&=?~MEASURED_DIMENSION_SET;
if(ViewDebug.TRACE_HIERARCHY)?{
ViewDebug.trace(this,?ViewDebug.HierarchyTraceType.ON_MEASURE);
}
onMeasure(widthMeasureSpec,?heightMeasureSpec);
if((mPrivateFlags?&?MEASURED_DIMENSION_SET)?!=?MEASURED_DIMENSION_SET)?{
thrownewIllegalStateException("onMeasure()?did?not?set?the"
+"?measured?dimension?by?calling"
+"?setMeasuredDimension()");
}
mPrivateFlags?|=?LAYOUT_REQUIRED;
}
mOldWidthMeasureSpec?=?widthMeasureSpec;
mOldHeightMeasureSpec?=?heightMeasureSpec;
}
注意觀察,measure()這個(gè)方法是final的,因此我們無(wú)法在子類中去重寫(xiě)這個(gè)方法,說(shuō)明Android是不允許我們改變View的measure框架的。然后在第9行調(diào)用了onMeasure()方法,這里才是真正去測(cè)量并設(shè)置View大小的地方,默認(rèn)會(huì)調(diào)用getDefaultSize()方法來(lái)獲取視圖的大小,如下所示:
[java]view plaincopy
publicstaticintgetDefaultSize(intsize,intmeasureSpec)?{
intresult?=?size;
intspecMode?=?MeasureSpec.getMode(measureSpec);
intspecSize?=?MeasureSpec.getSize(measureSpec);
switch(specMode)?{
caseMeasureSpec.UNSPECIFIED:
result?=?size;
break;
caseMeasureSpec.AT_MOST:
caseMeasureSpec.EXACTLY:
result?=?specSize;
break;
}
returnresult;
}
這里傳入的measureSpec是一直從measure()方法中傳遞過(guò)來(lái)的。然后調(diào)用MeasureSpec.getMode()方法可以解析出specMode,調(diào)用MeasureSpec.getSize()方法可以解析出specSize。接下來(lái)進(jìn)行判斷,如果specMode等于AT_MOST或EXACTLY就返回specSize,這也是系統(tǒng)默認(rèn)的行為。之后會(huì)在onMeasure()方法中調(diào)用setMeasuredDimension()方法來(lái)設(shè)定測(cè)量出的大小,這樣一次measure過(guò)程就結(jié)束了。
當(dāng)然,一個(gè)界面的展示可能會(huì)涉及到很多次的measure,因?yàn)橐粋€(gè)布局中一般都會(huì)包含多個(gè)子視圖,每個(gè)視圖都需要經(jīng)歷一次measure過(guò)程。ViewGroup中定義了一個(gè)measureChildren()方法來(lái)去測(cè)量子視圖的大小,如下所示:
[java]view plaincopy
protectedvoidmeasureChildren(intwidthMeasureSpec,intheightMeasureSpec)?{
finalintsize?=?mChildrenCount;
finalView[]?children?=?mChildren;
for(inti?=0;?i?<?size;?++i)?{
finalView?child?=?children[i];
if((child.mViewFlags?&?VISIBILITY_MASK)?!=?GONE)?{
measureChild(child,?widthMeasureSpec,?heightMeasureSpec);
}
}
}
這里首先會(huì)去遍歷當(dāng)前布局下的所有子視圖,然后逐個(gè)調(diào)用measureChild()方法來(lái)測(cè)量相應(yīng)子視圖的大小,如下所示:
[java]view plaincopy
protectedvoidmeasureChild(View?child,intparentWidthMeasureSpec,
intparentHeightMeasureSpec)?{
finalLayoutParams?lp?=?child.getLayoutParams();
finalintchildWidthMeasureSpec?=?getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft?+?mPaddingRight,?lp.width);
finalintchildHeightMeasureSpec?=?getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop?+?mPaddingBottom,?lp.height);
child.measure(childWidthMeasureSpec,?childHeightMeasureSpec);
}
可以看到,在第4行和第6行分別調(diào)用了getChildMeasureSpec()方法來(lái)去計(jì)算子視圖的MeasureSpec,計(jì)算的依據(jù)就是布局文件中定義的MATCH_PARENT、WRAP_CONTENT等值,這個(gè)方法的內(nèi)部細(xì)節(jié)就不再貼出。然后在第8行調(diào)用子視圖的measure()方法,并把計(jì)算出的MeasureSpec傳遞進(jìn)去,之后的流程就和前面所介紹的一樣了。
當(dāng)然,onMeasure()方法是可以重寫(xiě)的,也就是說(shuō),如果你不想使用系統(tǒng)默認(rèn)的測(cè)量方式,可以按照自己的意愿進(jìn)行定制,比如:
[java]view plaincopy
publicclassMyViewextendsView?{
......
@Override
protectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec)?{
setMeasuredDimension(200,200);
}
}
這樣的話就把View默認(rèn)的測(cè)量流程覆蓋掉了,不管在布局文件中定義MyView這個(gè)視圖的大小是多少,最終在界面上顯示的大小都將會(huì)是200*200。
需要注意的是,在setMeasuredDimension()方法調(diào)用之后,我們才能使用getMeasuredWidth()和getMeasuredHeight()來(lái)獲取視圖測(cè)量出的寬高,以此之前調(diào)用這兩個(gè)方法得到的值都會(huì)是0。
由此可見(jiàn),視圖大小的控制是由父視圖、布局文件、以及視圖本身共同完成的,父視圖會(huì)提供給子視圖參考的大小,而開(kāi)發(fā)人員可以在XML文件中指定視圖的大小,然后視圖本身會(huì)對(duì)最終的大小進(jìn)行拍板。
到此為止,我們就把視圖繪制流程的第一階段分析完了。
measure過(guò)程結(jié)束后,視圖的大小就已經(jīng)測(cè)量好了,接下來(lái)就是layout的過(guò)程了。正如其名字所描述的一樣,這個(gè)方法是用于給視圖進(jìn)行布局的,也就是確定視圖的位置。ViewRoot的performTraversals()方法會(huì)在measure結(jié)束后繼續(xù)執(zhí)行,并調(diào)用View的layout()方法來(lái)執(zhí)行此過(guò)程,如下所示:
[java]view plaincopy
host.layout(0,0,?host.mMeasuredWidth,?host.mMeasuredHeight);
layout()方法接收四個(gè)參數(shù),分別代表著左、上、右、下的坐標(biāo),當(dāng)然這個(gè)坐標(biāo)是相對(duì)于當(dāng)前視圖的父視圖而言的。可以看到,這里還把剛才測(cè)量出的寬度和高度傳到了layout()方法中。那么我們來(lái)看下layout()方法中的代碼是什么樣的吧,如下所示:
[java]view plaincopy
publicvoidlayout(intl,intt,intr,intb)?{
intoldL?=?mLeft;
intoldT?=?mTop;
intoldB?=?mBottom;
intoldR?=?mRight;
booleanchanged?=?setFrame(l,?t,?r,?b);
if(changed?||?(mPrivateFlags?&?LAYOUT_REQUIRED)?==?LAYOUT_REQUIRED)?{
if(ViewDebug.TRACE_HIERARCHY)?{
ViewDebug.trace(this,?ViewDebug.HierarchyTraceType.ON_LAYOUT);
}
onLayout(changed,?l,?t,?r,?b);
mPrivateFlags?&=?~LAYOUT_REQUIRED;
if(mOnLayoutChangeListeners?!=null)?{
ArrayList?listenersCopy?=
(ArrayList)?mOnLayoutChangeListeners.clone();
intnumListeners?=?listenersCopy.size();
for(inti?=0;?i?<?numListeners;?++i)?{
listenersCopy.get(i).onLayoutChange(this,?l,?t,?r,?b,?oldL,?oldT,?oldR,?oldB);
}
}
}
mPrivateFlags?&=?~FORCE_LAYOUT;
}
在layout()方法中,首先會(huì)調(diào)用setFrame()方法來(lái)判斷視圖的大小是否發(fā)生過(guò)變化,以確定有沒(méi)有必要對(duì)當(dāng)前的視圖進(jìn)行重繪,同時(shí)還會(huì)在這里把傳遞過(guò)來(lái)的四個(gè)參數(shù)分別賦值給mLeft、mTop、mRight和mBottom這幾個(gè)變量。接下來(lái)會(huì)在第11行調(diào)用onLayout()方法,正如onMeasure()方法中的默認(rèn)行為一樣,也許你已經(jīng)迫不及待地想知道onLayout()方法中的默認(rèn)行為是什么樣的了。進(jìn)入onLayout()方法,咦?怎么這是個(gè)空方法,一行代碼都沒(méi)有?!
沒(méi)錯(cuò),View中的onLayout()方法就是一個(gè)空方法,因?yàn)閛nLayout()過(guò)程是為了確定視圖在布局中所在的位置,而這個(gè)操作應(yīng)該是由布局來(lái)完成的,即父視圖決定子視圖的顯示位置。既然如此,我們來(lái)看下ViewGroup中的onLayout()方法是怎么寫(xiě)的吧,代碼如下:
[java]view plaincopy
@Override
protectedabstractvoidonLayout(booleanchanged,intl,intt,intr,intb);
可以看到,ViewGroup中的onLayout()方法竟然是一個(gè)抽象方法,這就意味著所有ViewGroup的子類都必須重寫(xiě)這個(gè)方法。沒(méi)錯(cuò),像LinearLayout、RelativeLayout等布局,都是重寫(xiě)了這個(gè)方法,然后在內(nèi)部按照各自的規(guī)則對(duì)子視圖進(jìn)行布局的。由于LinearLayout和RelativeLayout的布局規(guī)則都比較復(fù)雜,就不單獨(dú)拿出來(lái)進(jìn)行分析了,這里我們嘗試自定義一個(gè)布局,借此來(lái)更深刻地理解onLayout()的過(guò)程。
自定義的這個(gè)布局目標(biāo)很簡(jiǎn)單,只要能夠包含一個(gè)子視圖,并且讓子視圖正常顯示出來(lái)就可以了。那么就給這個(gè)布局起名叫做SimpleLayout吧,代碼如下所示:
[java]view plaincopy
publicclassSimpleLayoutextendsViewGroup?{
publicSimpleLayout(Context?context,?AttributeSet?attrs)?{
super(context,?attrs);
}
@Override
protectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec)?{
super.onMeasure(widthMeasureSpec,?heightMeasureSpec);
if(getChildCount()?>0)?{
View?childView?=?getChildAt(0);
measureChild(childView,?widthMeasureSpec,?heightMeasureSpec);
}
}
@Override
protectedvoidonLayout(booleanchanged,intl,intt,intr,intb)?{
if(getChildCount()?>0)?{
View?childView?=?getChildAt(0);
childView.layout(0,0,?childView.getMeasuredWidth(),?childView.getMeasuredHeight());
}
}
}
代碼非常的簡(jiǎn)單,我們來(lái)看下具體的邏輯吧。你已經(jīng)知道,onMeasure()方法會(huì)在onLayout()方法之前調(diào)用,因此這里在onMeasure()方法中判斷SimpleLayout中是否有包含一個(gè)子視圖,如果有的話就調(diào)用measureChild()方法來(lái)測(cè)量出子視圖的大小。
接著在onLayout()方法中同樣判斷SimpleLayout是否有包含一個(gè)子視圖,然后調(diào)用這個(gè)子視圖的layout()方法來(lái)確定它在SimpleLayout布局中的位置,這里傳入的四個(gè)參數(shù)依次是0、0、childView.getMeasuredWidth()和childView.getMeasuredHeight(),分別代表著子視圖在SimpleLayout中左上右下四個(gè)點(diǎn)的坐標(biāo)。其中,調(diào)用childView.getMeasuredWidth()和childView.getMeasuredHeight()方法得到的值就是在onMeasure()方法中測(cè)量出的寬和高。
這樣就已經(jīng)把SimpleLayout這個(gè)布局定義好了,下面就是在XML文件中使用它了,如下所示:
[html]view plaincopy
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_launcher"
/>
可以看到,我們能夠像使用普通的布局文件一樣使用SimpleLayout,只是注意它只能包含一個(gè)子視圖,多余的子視圖會(huì)被舍棄掉。這里SimpleLayout中包含了一個(gè)ImageView,并且ImageView的寬高都是wrap_content。現(xiàn)在運(yùn)行一下程序,結(jié)果如下圖所示:
OK!ImageView成功已經(jīng)顯示出來(lái)了,并且顯示的位置也正是我們所期望的。如果你想改變ImageView顯示的位置,只需要改變childView.layout()方法的四個(gè)參數(shù)就行了。
在onLayout()過(guò)程結(jié)束后,我們就可以調(diào)用getWidth()方法和getHeight()方法來(lái)獲取視圖的寬高了。說(shuō)到這里,我相信很多朋友長(zhǎng)久以來(lái)都會(huì)有一個(gè)疑問(wèn),getWidth()方法和getMeasureWidth()方法到底有什么區(qū)別呢?它們的值好像永遠(yuǎn)都是相同的。其實(shí)它們的值之所以會(huì)相同基本都是因?yàn)椴季衷O(shè)計(jì)者的編碼習(xí)慣非常好,實(shí)際上它們之間的差別還是挺大的。
首先getMeasureWidth()方法在measure()過(guò)程結(jié)束后就可以獲取到了,而getWidth()方法要在layout()過(guò)程結(jié)束后才能獲取到。另外,getMeasureWidth()方法中的值是通過(guò)setMeasuredDimension()方法來(lái)進(jìn)行設(shè)置的,而getWidth()方法中的值則是通過(guò)視圖右邊的坐標(biāo)減去左邊的坐標(biāo)計(jì)算出來(lái)的。
觀察SimpleLayout中onLayout()方法的代碼,這里給子視圖的layout()方法傳入的四個(gè)參數(shù)分別是0、0、childView.getMeasuredWidth()和childView.getMeasuredHeight(),因此getWidth()方法得到的值就是childView.getMeasuredWidth() - 0 =?childView.getMeasuredWidth() ,所以此時(shí)getWidth()方法和getMeasuredWidth() 得到的值就是相同的,但如果你將onLayout()方法中的代碼進(jìn)行如下修改:
[java]view plaincopy
@Override
protectedvoidonLayout(booleanchanged,intl,intt,intr,intb)?{
if(getChildCount()?>0)?{
View?childView?=?getChildAt(0);
childView.layout(0,0,200,200);
}
}
這樣getWidth()方法得到的值就是200 - 0 = 200,不會(huì)再和getMeasuredWidth()的值相同了。當(dāng)然這種做法充分不尊重measure()過(guò)程計(jì)算出的結(jié)果,通常情況下是不推薦這么寫(xiě)的。getHeight()與getMeasureHeight()方法之間的關(guān)系同上,就不再重復(fù)分析了。
到此為止,我們把視圖繪制流程的第二階段也分析完了。
measure和layout的過(guò)程都結(jié)束后,接下來(lái)就進(jìn)入到draw的過(guò)程了。同樣,根據(jù)名字你就能夠判斷出,在這里才真正地開(kāi)始對(duì)視圖進(jìn)行繪制。ViewRoot中的代碼會(huì)繼續(xù)執(zhí)行并創(chuàng)建出一個(gè)Canvas對(duì)象,然后調(diào)用View的draw()方法來(lái)執(zhí)行具體的繪制工作。draw()方法內(nèi)部的繪制過(guò)程總共可以分為六步,其中第二步和第五步在一般情況下很少用到,因此這里我們只分析簡(jiǎn)化后的繪制過(guò)程。代碼如下所示:
[java]view plaincopy
publicvoiddraw(Canvas?canvas)?{
if(ViewDebug.TRACE_HIERARCHY)?{
ViewDebug.trace(this,?ViewDebug.HierarchyTraceType.DRAW);
}
finalintprivateFlags?=?mPrivateFlags;
finalbooleandirtyOpaque?=?(privateFlags?&?DIRTY_MASK)?==?DIRTY_OPAQUE?&&
(mAttachInfo?==null||?!mAttachInfo.mIgnoreDirtyState);
mPrivateFlags?=?(privateFlags?&?~DIRTY_MASK)?|?DRAWN;
//?Step?1,?draw?the?background,?if?needed
intsaveCount;
if(!dirtyOpaque)?{
finalDrawable?background?=?mBGDrawable;
if(background?!=null)?{
finalintscrollX?=?mScrollX;
finalintscrollY?=?mScrollY;
if(mBackgroundSizeChanged)?{
background.setBounds(0,0,??mRight?-?mLeft,?mBottom?-?mTop);
mBackgroundSizeChanged?=false;
}
if((scrollX?|?scrollY)?==0)?{
background.draw(canvas);
}else{
canvas.translate(scrollX,?scrollY);
background.draw(canvas);
canvas.translate(-scrollX,?-scrollY);
}
}
}
finalintviewFlags?=?mViewFlags;
booleanhorizontalEdges?=?(viewFlags?&?FADING_EDGE_HORIZONTAL)?!=0;
booleanverticalEdges?=?(viewFlags?&?FADING_EDGE_VERTICAL)?!=0;
if(!verticalEdges?&&?!horizontalEdges)?{
//?Step?3,?draw?the?content
if(!dirtyOpaque)?onDraw(canvas);
//?Step?4,?draw?the?children
dispatchDraw(canvas);
//?Step?6,?draw?decorations?(scrollbars)
onDrawScrollBars(canvas);
//?we're?done...
return;
}
}
可以看到,第一步是從第9行代碼開(kāi)始的,這一步的作用是對(duì)視圖的背景進(jìn)行繪制。這里會(huì)先得到一個(gè)mBGDrawable對(duì)象,然后根據(jù)layout過(guò)程確定的視圖位置來(lái)設(shè)置背景的繪制區(qū)域,之后再調(diào)用Drawable的draw()方法來(lái)完成背景的繪制工作。那么這個(gè)mBGDrawable對(duì)象是從哪里來(lái)的呢?其實(shí)就是在XML中通過(guò)android:background屬性設(shè)置的圖片或顏色。當(dāng)然你也可以在代碼中通過(guò)setBackgroundColor()、setBackgroundResource()等方法進(jìn)行賦值。
接下來(lái)的第三步是在第34行執(zhí)行的,這一步的作用是對(duì)視圖的內(nèi)容進(jìn)行繪制。可以看到,這里去調(diào)用了一下onDraw()方法,那么onDraw()方法里又寫(xiě)了什么代碼呢?進(jìn)去一看你會(huì)發(fā)現(xiàn),原來(lái)又是個(gè)空方法啊。其實(shí)也可以理解,因?yàn)槊總€(gè)視圖的內(nèi)容部分肯定都是各不相同的,這部分的功能交給子類來(lái)去實(shí)現(xiàn)也是理所當(dāng)然的。
第三步完成之后緊接著會(huì)執(zhí)行第四步,這一步的作用是對(duì)當(dāng)前視圖的所有子視圖進(jìn)行繪制。但如果當(dāng)前的視圖沒(méi)有子視圖,那么也就不需要進(jìn)行繪制了。因此你會(huì)發(fā)現(xiàn)View中的dispatchDraw()方法又是一個(gè)空方法,而ViewGroup的dispatchDraw()方法中就會(huì)有具體的繪制代碼。
以上都執(zhí)行完后就會(huì)進(jìn)入到第六步,也是最后一步,這一步的作用是對(duì)視圖的滾動(dòng)條進(jìn)行繪制。那么你可能會(huì)奇怪,當(dāng)前的視圖又不一定是ListView或者ScrollView,為什么要繪制滾動(dòng)條呢?其實(shí)不管是Button也好,TextView也好,任何一個(gè)視圖都是有滾動(dòng)條的,只是一般情況下我們都沒(méi)有讓它顯示出來(lái)而已。繪制滾動(dòng)條的代碼邏輯也比較復(fù)雜,這里就不再貼出來(lái)了,因?yàn)槲覀兊闹攸c(diǎn)是第三步過(guò)程。
通過(guò)以上流程分析,相信大家已經(jīng)知道,View是不會(huì)幫我們繪制內(nèi)容部分的,因此需要每個(gè)視圖根據(jù)想要展示的內(nèi)容來(lái)自行繪制。如果你去觀察TextView、ImageView等類的源碼,你會(huì)發(fā)現(xiàn)它們都有重寫(xiě)onDraw()這個(gè)方法,并且在里面執(zhí)行了相當(dāng)不少的繪制邏輯。繪制的方式主要是借助Canvas這個(gè)類,它會(huì)作為參數(shù)傳入到onDraw()方法中,供給每個(gè)視圖使用。Canvas這個(gè)類的用法非常豐富,基本可以把它當(dāng)成一塊畫(huà)布,在上面繪制任意的東西,那么我們就來(lái)嘗試一下吧。
這里簡(jiǎn)單起見(jiàn),我只是創(chuàng)建一個(gè)非常簡(jiǎn)單的視圖,并且用Canvas隨便繪制了一點(diǎn)東西,代碼如下所示:
[java]view plaincopy
publicclassMyViewextendsView?{
privatePaint?mPaint;
publicMyView(Context?context,?AttributeSet?attrs)?{
super(context,?attrs);
mPaint?=newPaint(Paint.ANTI_ALIAS_FLAG);
}
@Override
protectedvoidonDraw(Canvas?canvas)?{
mPaint.setColor(Color.YELLOW);
canvas.drawRect(0,0,?getWidth(),?getHeight(),?mPaint);
mPaint.setColor(Color.BLUE);
mPaint.setTextSize(20);
String?text?="Hello?View";
canvas.drawText(text,0,?getHeight()?/2,?mPaint);
}
}
可以看到,我們創(chuàng)建了一個(gè)自定義的MyView繼承自View,并在MyView的構(gòu)造函數(shù)中創(chuàng)建了一個(gè)Paint對(duì)象。Paint就像是一個(gè)畫(huà)筆一樣,配合著Canvas就可以進(jìn)行繪制了。這里我們的繪制邏輯比較簡(jiǎn)單,在onDraw()方法中先是把畫(huà)筆設(shè)置成黃色,然后調(diào)用Canvas的drawRect()方法繪制一個(gè)矩形。然后在把畫(huà)筆設(shè)置成藍(lán)色,并調(diào)整了一下文字的大小,然后調(diào)用drawText()方法繪制了一段文字。
就這么簡(jiǎn)單,一個(gè)自定義的視圖就已經(jīng)寫(xiě)好了,現(xiàn)在可以在XML中加入這個(gè)視圖,如下所示:
[html]view plaincopy
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_width="200dp"
android:layout_height="100dp"
/>
將MyView的寬度設(shè)置成200dp,高度設(shè)置成100dp,然后運(yùn)行一下程序,結(jié)果如下圖所示:
圖中顯示的內(nèi)容也正是MyView這個(gè)視圖的內(nèi)容部分了。由于我們沒(méi)給MyView設(shè)置背景,因此這里看不出來(lái)View自動(dòng)繪制的背景效果。
當(dāng)然了Canvas的用法還有很多很多,這里我不可能把Canvas的所有用法都列舉出來(lái),剩下的就要靠大家自行去研究和學(xué)習(xí)了。
到此為止,我們把視圖繪制流程的第三階段也分析完了。整個(gè)視圖的繪制過(guò)程就全部結(jié)束了,你現(xiàn)在是不是對(duì)View的理解更加深刻了呢?感興趣的朋友可以繼續(xù)閱讀Android視圖狀態(tài)及重繪流程分析,帶你一步步深入了解View(三)。