在上一篇文章中,我帶著大家一起剖析了一下LayoutInflater的工作原理,可以算是對View進行深入了解的第一步吧。那么本篇文章中,我們將繼續對View進行深入探究,看一看它的繪制流程到底是什么樣的。如果你還沒有看過我的上一篇文章,可以先去閱讀 Android LayoutInflater原理分析,帶你一步步深入了解View(一) ?。
相信每個Android程序員都知道,我們每天的開發工作當中都在不停地跟View打交道,Android中的任何一個布局、任何一個控件其實都是直接或間接繼承自View的,如TextView、Button、ImageView、ListView等。這些控件雖然是Android系統本身就提供好的,我們只需要拿過來使用就可以了,但你知道它們是怎樣被繪制到屏幕上的嗎?多知道一些總是沒有壞處的,那么我們趕快進入到本篇文章的正題內容吧。
要知道,任何一個視圖都不可能憑空突然出現在屏幕上,它們都是要經過非常科學的繪制流程后才能顯示出來的。每一個視圖的繪制過程都必須經歷三個最主要的階段,即onMeasure()、onLayout()和onDraw(),下面我們逐個對這三個階段展開進行探討。
一. onMeasure()
measure是測量的意思,那么onMeasure()方法顧名思義就是用于測量視圖的大小的。View系統的繪制流程會從ViewRoot的performTraversals()方法中開始,在其內部調用View的measure()方法。measure()方法接收兩個參數,widthMeasureSpec和heightMeasureSpec,這兩個值分別用于確定視圖的寬度和高度的規格和大小。
MeasureSpec的值由specSize和specMode共同組成的,其中specSize記錄的是大小,specMode記錄的是規格。specMode一共有三種類型,如下所示:
1. EXACTLY
表示父視圖希望子視圖的大小應該是由specSize的值來決定的,系統默認會按照這個規則來設置子視圖的大小,開發人員當然也可以按照自己的意愿設置成任意的大小。
2. AT_MOST
表示子視圖最多只能是specSize中指定的大小,開發人員應該盡可能小得去設置這個視圖,并且保證不會超過specSize。系統默認會按照這個規則來設置子視圖的大小,開發人員當然也可以按照自己的意愿設置成任意的大小。
3. UNSPECIFIED
表示開發人員可以將視圖按照自己的意愿設置成任意的大小,沒有任何限制。這種情況比較少見,不太會用到。
那么你可能會有疑問了,widthMeasureSpec和heightMeasureSpec這兩個值又是從哪里得到的呢?通常情況下,這兩個值都是由父視圖經過計算后傳遞給子視圖的,說明父視圖會在一定程度上決定子視圖的大小。但是最外層的根視圖,它的widthMeasureSpec和heightMeasureSpec又是從哪里得到的呢?這就需要去分析ViewRoot中的源碼了,觀察performTraversals()方法可以發現如下代碼:
[java] view plaincopy
childWidthMeasureSpec?=?getRootMeasureSpec(desiredWindowWidth,?lp.width);??childHeightMeasureSpec?=?getRootMeasureSpec(desiredWindowHeight,?lp.height);??
可以看到,這里調用了getRootMeasureSpec()方法去獲取widthMeasureSpec和heightMeasureSpec的值,注意方法中傳入的參數,其中lp.width和lp.height在創建ViewGroup實例的時候就被賦值了,它們都等于MATCH_PARENT。然后看下getRootMeasureSpec()方法中的代碼,如下所示:
[java] view plaincopy
private?int?getRootMeasureSpec(int?windowSize,?int?rootDimension)?{??????int?measureSpec;??????switch?(rootDimension)?{??????case?ViewGroup.LayoutParams.MATCH_PARENT:??????????measureSpec?=?MeasureSpec.makeMeasureSpec(windowSize,?MeasureSpec.EXACTLY);??????????break;??????case?ViewGroup.LayoutParams.WRAP_CONTENT:??????????measureSpec?=?MeasureSpec.makeMeasureSpec(windowSize,?MeasureSpec.AT_MOST);??????????break;??????default:??????????measureSpec?=?MeasureSpec.makeMeasureSpec(rootDimension,?MeasureSpec.EXACTLY);??????????break;??????}??????return?measureSpec;??}??
可以看到,這里使用了MeasureSpec.makeMeasureSpec()方法來組裝一個MeasureSpec,當rootDimension參數等于MATCH_PARENT的時候,MeasureSpec的specMode就等于EXACTLY,當rootDimension等于WRAP_CONTENT的時候,MeasureSpec的specMode就等于AT_MOST。并且MATCH_PARENT和WRAP_CONTENT時的specSize都是等于windowSize的,也就意味著根視圖總是會充滿全屏的。
介紹了這么多MeasureSpec相關的內容,接下來我們看下View的measure()方法里面的代碼吧,如下所示:
[java] view plaincopy
public?final?void?measure(int?widthMeasureSpec,?int?heightMeasureSpec)?{??????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)?{??????????????throw?new?IllegalStateException("onMeasure()?did?not?set?the"??????????????????????+?"?measured?dimension?by?calling"??????????????????????+?"?setMeasuredDimension()");??????????}??????????mPrivateFlags?|=?LAYOUT_REQUIRED;??????}??????mOldWidthMeasureSpec?=?widthMeasureSpec;??????mOldHeightMeasureSpec?=?heightMeasureSpec;??}??
注意觀察,measure()這個方法是final的,因此我們無法在子類中去重寫這個方法,說明Android是不允許我們改變View的measure框架的。然后在第9行調用了onMeasure()方法,這里才是真正去測量并設置View大小的地方,默認會調用getDefaultSize()方法來獲取視圖的大小,如下所示:
[java] view plaincopy
public?static?int?getDefaultSize(int?size,?int?measureSpec)?{??????int?result?=?size;??????int?specMode?=?MeasureSpec.getMode(measureSpec);??????int?specSize?=?MeasureSpec.getSize(measureSpec);??????switch?(specMode)?{??????case?MeasureSpec.UNSPECIFIED:??????????result?=?size;??????????break;??????case?MeasureSpec.AT_MOST:??????case?MeasureSpec.EXACTLY:??????????result?=?specSize;??????????break;??????}??????return?result;??}??
這里傳入的measureSpec是一直從measure()方法中傳遞過來的。然后調用MeasureSpec.getMode()方法可以解析出specMode,調用MeasureSpec.getSize()方法可以解析出specSize。接下來進行判斷,如果specMode等于AT_MOST或EXACTLY就返回specSize,這也是系統默認的行為。之后會在onMeasure()方法中調用setMeasuredDimension()方法來設定測量出的大小,這樣一次measure過程就結束了。
當然,一個界面的展示可能會涉及到很多次的measure,因為一個布局中一般都會包含多個子視圖,每個視圖都需要經歷一次measure過程。ViewGroup中定義了一個measureChildren()方法來去測量子視圖的大小,如下所示:
[java] view plaincopy
protected?void?measureChildren(int?widthMeasureSpec,?int?heightMeasureSpec)?{??????final?int?size?=?mChildrenCount;??????final?View[]?children?=?mChildren;??????for?(int?i?=?0;?i?<?size;?++i)?{??????????final?View?child?=?children[i];??????????if?((child.mViewFlags?&?VISIBILITY_MASK)?!=?GONE)?{??????????????measureChild(child,?widthMeasureSpec,?heightMeasureSpec);??????????}??????}??}??
這里首先會去遍歷當前布局下的所有子視圖,然后逐個調用measureChild()方法來測量相應子視圖的大小,如下所示:
[java] view plaincopy
protected?void?measureChild(View?child,?int?parentWidthMeasureSpec,??????????int?parentHeightMeasureSpec)?{??????final?LayoutParams?lp?=?child.getLayoutParams();??????final?int?childWidthMeasureSpec?=?getChildMeasureSpec(parentWidthMeasureSpec,??????????????mPaddingLeft?+?mPaddingRight,?lp.width);??????final?int?childHeightMeasureSpec?=?getChildMeasureSpec(parentHeightMeasureSpec,??????????????mPaddingTop?+?mPaddingBottom,?lp.height);??????child.measure(childWidthMeasureSpec,?childHeightMeasureSpec);??}??
可以看到,在第4行和第6行分別調用了getChildMeasureSpec()方法來去計算子視圖的MeasureSpec,計算的依據就是布局文件中定義的MATCH_PARENT、WRAP_CONTENT等值,這個方法的內部細節就不再貼出。然后在第8行調用子視圖的measure()方法,并把計算出的MeasureSpec傳遞進去,之后的流程就和前面所介紹的一樣了。
當然,onMeasure()方法是可以重寫的,也就是說,如果你不想使用系統默認的測量方式,可以按照自己的意愿進行定制,比如:
[java] view plaincopy
public?class?MyView?extends?View?{????????......????????????@Override??????protected?void?onMeasure(int?widthMeasureSpec,?int?heightMeasureSpec)?{??????????setMeasuredDimension(200,?200);??????}????}??
這樣的話就把View默認的測量流程覆蓋掉了,不管在布局文件中定義MyView這個視圖的大小是多少,最終在界面上顯示的大小都將會是200*200。
需要注意的是,在setMeasuredDimension()方法調用之后,我們才能使用getMeasuredWidth()和getMeasuredHeight()來獲取視圖測量出的寬高,以此之前調用這兩個方法得到的值都會是0。
由此可見,視圖大小的控制是由父視圖、布局文件、以及視圖本身共同完成的,父視圖會提供給子視圖參考的大小,而開發人員可以在XML文件中指定視圖的大小,然后視圖本身會對最終的大小進行拍板。
到此為止,我們就把視圖繪制流程的第一階段分析完了。
二. onLayout()
measure過程結束后,視圖的大小就已經測量好了,接下來就是layout的過程了。正如其名字所描述的一樣,這個方法是用于給視圖進行布局的,也就是確定視圖的位置。ViewRoot的performTraversals()方法會在measure結束后繼續執行,并調用View的layout()方法來執行此過程,如下所示:
[java] view plaincopy
host.layout(0,?0,?host.mMeasuredWidth,?host.mMeasuredHeight);??
layout()方法接收四個參數,分別代表著左、上、右、下的坐標,當然這個坐標是相對于當前視圖的父視圖而言的。可以看到,這里還把剛才測量出的寬度和高度傳到了layout()方法中。那么我們來看下layout()方法中的代碼是什么樣的吧,如下所示:
[java] view plaincopy
public?void?layout(int?l,?int?t,?int?r,?int?b)?{??????int?oldL?=?mLeft;??????int?oldT?=?mTop;??????int?oldB?=?mBottom;??????int?oldR?=?mRight;??????boolean?changed?=?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<OnLayoutChangeListener>?listenersCopy?=??????????????????????(ArrayList<OnLayoutChangeListener>)?mOnLayoutChangeListeners.clone();??????????????int?numListeners?=?listenersCopy.size();??????????????for?(int?i?=?0;?i?<?numListeners;?++i)?{??????????????????listenersCopy.get(i).onLayoutChange(this,?l,?t,?r,?b,?oldL,?oldT,?oldR,?oldB);??????????????}??????????}??????}??????mPrivateFlags?&=?~FORCE_LAYOUT;??}??
在layout()方法中,首先會調用setFrame()方法來判斷視圖的大小是否發生過變化,以確定有沒有必要對當前的視圖進行重繪,同時還會在這里把傳遞過來的四個參數分別賦值給mLeft、mTop、mRight和mBottom這幾個變量。接下來會在第11行調用onLayout()方法,正如onMeasure()方法中的默認行為一樣,也許你已經迫不及待地想知道onLayout()方法中的默認行為是什么樣的了。進入onLayout()方法,咦?怎么這是個空方法,一行代碼都沒有?!
沒錯,View中的onLayout()方法就是一個空方法,因為onLayout()過程是為了確定視圖在布局中所在的位置,而這個操作應該是由布局來完成的,即父視圖決定子視圖的顯示位置。既然如此,我們來看下ViewGroup中的onLayout()方法是怎么寫的吧,代碼如下:
[java] view plaincopy
@Override??protected?abstract?void?onLayout(boolean?changed,?int?l,?int?t,?int?r,?int?b);??
可以看到,ViewGroup中的onLayout()方法竟然是一個抽象方法,這就意味著所有ViewGroup的子類都必須重寫這個方法。沒錯,像LinearLayout、RelativeLayout等布局,都是重寫了這個方法,然后在內部按照各自的規則對子視圖進行布局的。由于LinearLayout和RelativeLayout的布局規則都比較復雜,就不單獨拿出來進行分析了,這里我們嘗試自定義一個布局,借此來更深刻地理解onLayout()的過程。
自定義的這個布局目標很簡單,只要能夠包含一個子視圖,并且讓子視圖正常顯示出來就可以了。那么就給這個布局起名叫做SimpleLayout吧,代碼如下所示:
[java] view plaincopy
public?class?SimpleLayout?extends?ViewGroup?{????????public?SimpleLayout(Context?context,?AttributeSet?attrs)?{??????????super(context,?attrs);??????}????????@Override??????protected?void?onMeasure(int?widthMeasureSpec,?int?heightMeasureSpec)?{??????????super.onMeasure(widthMeasureSpec,?heightMeasureSpec);??????????if?(getChildCount()?>?0)?{??????????????View?childView?=?getChildAt(0);??????????????measureChild(childView,?widthMeasureSpec,?heightMeasureSpec);??????????}??????}????????@Override??????protected?void?onLayout(boolean?changed,?int?l,?int?t,?int?r,?int?b)?{??????????if?(getChildCount()?>?0)?{??????????????View?childView?=?getChildAt(0);??????????????childView.layout(0,?0,?childView.getMeasuredWidth(),?childView.getMeasuredHeight());??????????}??????}????}??
代碼非常的簡單,我們來看下具體的邏輯吧。你已經知道,onMeasure()方法會在onLayout()方法之前調用,因此這里在onMeasure()方法中判斷SimpleLayout中是否有包含一個子視圖,如果有的話就調用measureChild()方法來測量出子視圖的大小。
接著在onLayout()方法中同樣判斷SimpleLayout是否有包含一個子視圖,然后調用這個子視圖的layout()方法來確定它在SimpleLayout布局中的位置,這里傳入的四個參數依次是0、0、childView.getMeasuredWidth()和childView.getMeasuredHeight(),分別代表著子視圖在SimpleLayout中左上右下四個點的坐標。其中,調用childView.getMeasuredWidth()和childView.getMeasuredHeight()方法得到的值就是在onMeasure()方法中測量出的寬和高。
這樣就已經把SimpleLayout這個布局定義好了,下面就是在XML文件中使用它了,如下所示:
[html] view plaincopy
<com.example.viewtest.SimpleLayout?xmlns:android="http://schemas.android.com/apk/res/android"??????android:layout_width="match_parent"??????android:layout_height="match_parent"?>????????????<ImageView???????????android:layout_width="wrap_content"??????????android:layout_height="wrap_content"??????????android:src="@drawable/ic_launcher"??????????/>????????</com.example.viewtest.SimpleLayout>??
可以看到,我們能夠像使用普通的布局文件一樣使用SimpleLayout,只是注意它只能包含一個子視圖,多余的子視圖會被舍棄掉。這里SimpleLayout中包含了一個ImageView,并且ImageView的寬高都是wrap_content。現在運行一下程序,結果如下圖所示:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
OK!ImageView成功已經顯示出來了,并且顯示的位置也正是我們所期望的。如果你想改變ImageView顯示的位置,只需要改變childView.layout()方法的四個參數就行了。
在onLayout()過程結束后,我們就可以調用getWidth()方法和getHeight()方法來獲取視圖的寬高了。說到這里,我相信很多朋友長久以來都會有一個疑問,getWidth()方法和getMeasureWidth()方法到底有什么區別呢?它們的值好像永遠都是相同的。其實它們的值之所以會相同基本都是因為布局設計者的編碼習慣非常好,實際上它們之間的差別還是挺大的。
首先getMeasureWidth()方法在measure()過程結束后就可以獲取到了,而getWidth()方法要在layout()過程結束后才能獲取到。另外,getMeasureWidth()方法中的值是通過setMeasuredDimension()方法來進行設置的,而getWidth()方法中的值則是通過視圖右邊的坐標減去左邊的坐標計算出來的。
觀察SimpleLayout中onLayout()方法的代碼,這里給子視圖的layout()方法傳入的四個參數分別是0、0、childView.getMeasuredWidth()和childView.getMeasuredHeight(),因此getWidth()方法得到的值就是childView.getMeasuredWidth() - 0 =?childView.getMeasuredWidth() ,所以此時getWidth()方法和getMeasuredWidth() 得到的值就是相同的,但如果你將onLayout()方法中的代碼進行如下修改:
[java] view plaincopy
@Override??protected?void?onLayout(boolean?changed,?int?l,?int?t,?int?r,?int?b)?{??????if?(getChildCount()?>?0)?{??????????View?childView?=?getChildAt(0);??????????childView.layout(0,?0,?200,?200);??????}??}??
這樣getWidth()方法得到的值就是200 - 0 = 200,不會再和getMeasuredWidth()的值相同了。當然這種做法充分不尊重measure()過程計算出的結果,通常情況下是不推薦這么寫的。getHeight()與getMeasureHeight()方法之間的關系同上,就不再重復分析了。
到此為止,我們把視圖繪制流程的第二階段也分析完了。
三. onDraw()
measure和layout的過程都結束后,接下來就進入到draw的過程了。同樣,根據名字你就能夠判斷出,在這里才真正地開始對視圖進行繪制。ViewRoot中的代碼會繼續執行并創建出一個Canvas對象,然后調用View的draw()方法來執行具體的繪制工作。draw()方法內部的繪制過程總共可以分為六步,其中第二步和第五步在一般情況下很少用到,因此這里我們只分析簡化后的繪制過程。代碼如下所示:
[java] view plaincopy
public?void?draw(Canvas?canvas)?{??????if?(ViewDebug.TRACE_HIERARCHY)?{??????????ViewDebug.trace(this,?ViewDebug.HierarchyTraceType.DRAW);??????}??????final?int?privateFlags?=?mPrivateFlags;??????final?boolean?dirtyOpaque?=?(privateFlags?&?DIRTY_MASK)?==?DIRTY_OPAQUE?&&??????????????(mAttachInfo?==?null?||?!mAttachInfo.mIgnoreDirtyState);??????mPrivateFlags?=?(privateFlags?&?~DIRTY_MASK)?|?DRAWN;????????????int?saveCount;??????if?(!dirtyOpaque)?{??????????final?Drawable?background?=?mBGDrawable;??????????if?(background?!=?null)?{??????????????final?int?scrollX?=?mScrollX;??????????????final?int?scrollY?=?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);??????????????}??????????}??????}??????final?int?viewFlags?=?mViewFlags;??????boolean?horizontalEdges?=?(viewFlags?&?FADING_EDGE_HORIZONTAL)?!=?0;??????boolean?verticalEdges?=?(viewFlags?&?FADING_EDGE_VERTICAL)?!=?0;??????if?(!verticalEdges?&&?!horizontalEdges)?{????????????????????if?(!dirtyOpaque)?onDraw(canvas);????????????????????dispatchDraw(canvas);????????????????????onDrawScrollBars(canvas);????????????????????return;??????}??}??
可以看到,第一步是從第9行代碼開始的,這一步的作用是對視圖的背景進行繪制。這里會先得到一個mBGDrawable對象,然后根據layout過程確定的視圖位置來設置背景的繪制區域,之后再調用Drawable的draw()方法來完成背景的繪制工作。那么這個mBGDrawable對象是從哪里來的呢?其實就是在XML中通過android:background屬性設置的圖片或顏色。當然你也可以在代碼中通過setBackgroundColor()、setBackgroundResource()等方法進行賦值。
接下來的第三步是在第34行執行的,這一步的作用是對視圖的內容進行繪制。可以看到,這里去調用了一下onDraw()方法,那么onDraw()方法里又寫了什么代碼呢?進去一看你會發現,原來又是個空方法啊。其實也可以理解,因為每個視圖的內容部分肯定都是各不相同的,這部分的功能交給子類來去實現也是理所當然的。
第三步完成之后緊接著會執行第四步,這一步的作用是對當前視圖的所有子視圖進行繪制。但如果當前的視圖沒有子視圖,那么也就不需要進行繪制了。因此你會發現View中的dispatchDraw()方法又是一個空方法,而ViewGroup的dispatchDraw()方法中就會有具體的繪制代碼。
以上都執行完后就會進入到第六步,也是最后一步,這一步的作用是對視圖的滾動條進行繪制。那么你可能會奇怪,當前的視圖又不一定是ListView或者ScrollView,為什么要繪制滾動條呢?其實不管是Button也好,TextView也好,任何一個視圖都是有滾動條的,只是一般情況下我們都沒有讓它顯示出來而已。繪制滾動條的代碼邏輯也比較復雜,這里就不再貼出來了,因為我們的重點是第三步過程。
通過以上流程分析,相信大家已經知道,View是不會幫我們繪制內容部分的,因此需要每個視圖根據想要展示的內容來自行繪制。如果你去觀察TextView、ImageView等類的源碼,你會發現它們都有重寫onDraw()這個方法,并且在里面執行了相當不少的繪制邏輯。繪制的方式主要是借助Canvas這個類,它會作為參數傳入到onDraw()方法中,供給每個視圖使用。Canvas這個類的用法非常豐富,基本可以把它當成一塊畫布,在上面繪制任意的東西,那么我們就來嘗試一下吧。
這里簡單起見,我只是創建一個非常簡單的視圖,并且用Canvas隨便繪制了一點東西,代碼如下所示:
[java] view plaincopy
public?class?MyView?extends?View?{????????private?Paint?mPaint;????????public?MyView(Context?context,?AttributeSet?attrs)?{??????????super(context,?attrs);??????????mPaint?=?new?Paint(Paint.ANTI_ALIAS_FLAG);??????}????????@Override??????protected?void?onDraw(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);??????}??}??
可以看到,我們創建了一個自定義的MyView繼承自View,并在MyView的構造函數中創建了一個Paint對象。Paint就像是一個畫筆一樣,配合著Canvas就可以進行繪制了。這里我們的繪制邏輯比較簡單,在onDraw()方法中先是把畫筆設置成黃色,然后調用Canvas的drawRect()方法繪制一個矩形。然后在把畫筆設置成藍色,并調整了一下文字的大小,然后調用drawText()方法繪制了一段文字。
就這么簡單,一個自定義的視圖就已經寫好了,現在可以在XML中加入這個視圖,如下所示:
[html] view plaincopy
<LinearLayout?xmlns:android="http://schemas.android.com/apk/res/android"??????android:layout_width="match_parent"??????android:layout_height="match_parent"?>????????<com.example.viewtest.MyView???????????android:layout_width="200dp"??????????android:layout_height="100dp"??????????/>????</LinearLayout>??
將MyView的寬度設置成200dp,高度設置成100dp,然后運行一下程序,結果如下圖所示:
? ? ? ? ? ? ? ? ? ? ??
圖中顯示的內容也正是MyView這個視圖的內容部分了。由于我們沒給MyView設置背景,因此這里看不出來View自動繪制的背景效果。
當然了Canvas的用法還有很多很多,這里我不可能把Canvas的所有用法都列舉出來,剩下的就要靠大家自行去研究和學習了。
到此為止,我們把視圖繪制流程的第三階段也分析完了。整個視圖的繪制過程就全部結束了,你現在是不是對View的理解更加深刻了呢?
感興趣的朋友可以繼續閱讀
總結
以上是生活随笔為你收集整理的Android视图绘制流程完全解析,带你一步步深入了解View(二)的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。