Android視圖繪制流程完全解析,帶你一步步深入了解View(二)

在上一篇文章中,我?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)行探討。

一. onMeasure()

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)行拍板。

到此為止,我們就把視圖繪制流程的第一階段分析完了。

二. onLayout()

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ù)分析了。

到此為止,我們把視圖繪制流程的第二階段也分析完了。

三. onDraw()

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(三)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,071評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 175,409評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,569評(píng)論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,360評(píng)論 6 404
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 54,895評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,123評(píng)論 0 286
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,643評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,559評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,742評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評(píng)論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,981評(píng)論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,363評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,622評(píng)論 1 280
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,354評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,707評(píng)論 2 370

推薦閱讀更多精彩內(nèi)容