Android 視圖繪製流程:

Android應用開發中,可以說肯定會用到View:TextView,ListView.Button等等,他們都是要經過非常科學的繪製流程後才能顯示出來,每一個視圖的繪製過程必須經歷三個最主要的階段:onMeasure(),onLayout(),onDraw()

首先,onMeasure()
measure是測量的意思,所以onMeasure方法實現了測量視圖大小的功能,View系統的繪製流程會從ViewRoot的performTravelsals()方法中開始,然後調用measure方法,其中兩個主要的參數:widthMeasureSpec和HeiMeasureSpec,確定視圖的寬度和高度:
MeasureSpec的值由specSize和specMode共同組成,其中specSize記錄的是大小,specMode記錄的是規格,一共有三種類型:
1,Exactly:表示父視圖希望子視圖的大小是由specSize的值決定的,系統默認會按照這個規則來設置子視圖的大小,開發人員當然也可以按照自己的意願設置成任意的大小。
相當於match_parent
2,AT_Most:表示子視圖最多隻能是specSize中的任意大小,開發人員應該儘可能小的去設置這個視圖。並且不會超過specSize,系統默認會按照這個規則來設置子視圖的大小,開發人員當然可以設置任意的大小。
相當於wrap_content
3,UNSpecified:可以將視圖設置成任意的大小,沒有任何限制。
 widthMeasureSpec和heightMeasureSpec都是由父視圖經過計算後傳遞給子視圖的,說明父視圖會決定子視圖的大小,注:最外層的視圖它的值是Exactly由(ViewRoot源碼中得知)
measure(intwidthMeasureSpec,intheightMeasureSpec)這個方法是final的,無法在子類中重寫這個方法,說明Android不允許我們改變View的measure框架....

需要注意的是,在setMeasuredDimension()方法調用之後,我們才能使用getMeasuredWidth()和getMeasuredHeight()來獲取視圖測量出的寬高,以此之前調用這兩個方法得到的值都會是0。

由此可見,視圖大小的控制是由父視圖、佈局文件、以及視圖本身共同完成的,父視圖會提供給子視圖參考的大小,而開發人員可以在XML文件中指定視圖的大小,然後視圖本身會對最終的大小進行拍板。

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

其次:onLayout()

measure過程結束後,視圖的大小就已經測量好了,接下來就是layout的過程了。正如其名字所描述的一樣,這個方法是用於給視圖進行佈局的,也就是確定視圖的位置。ViewRoot的performTraversals()方法會在measure結束後繼續執行,並調用View的layout()方法來執行此過程,如下所示:

[java] viewplaincopy在CODE上查看代碼片派生到我的代碼片
  1. host.layout(00, host.mMeasuredWidth, host.mMeasuredHeight);  
layout()方法接收四個參數,分別代表着左、上、右、下的座標,當然這個座標是相對於當前視圖的父視圖而言的。可以看到,這裏還把剛纔測量出的寬度和高度傳到了layout()方法中。那麼我們來看下layout()方法中的代碼是什麼樣的吧,如下所示:
[java] viewplaincopy在CODE上查看代碼片派生到我的代碼片
  1. public void layout(int l, int t, int r, int b) {  
  2.     int oldL = mLeft;  
  3.     int oldT = mTop;  
  4.     int oldB = mBottom;  
  5.     int oldR = mRight;  
  6.     boolean changed = setFrame(l, t, r, b);  
  7.     if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {  
  8.         if (ViewDebug.TRACE_HIERARCHY) {  
  9.             ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);  
  10.         }  
  11.         onLayout(changed, l, t, r, b);  
  12.         mPrivateFlags &= ~LAYOUT_REQUIRED;  
  13.         if (mOnLayoutChangeListeners != null) {  
  14.             ArrayList listenersCopy =  
  15.                     (ArrayList) mOnLayoutChangeListeners.clone();  
  16.             int numListeners = listenersCopy.size();  
  17.             for (int i = 0; i < numListeners; ++i) {  
  18.                 listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);  
  19.             }  
  20.         }  
  21.     }  
  22.     mPrivateFlags &= ~FORCE_LAYOUT;  
  23. }  

在layout()方法中,首先會調用setFrame()方法來判斷視圖的大小是否發生過變化,以確定有沒有必要對當前的視圖進行重繪,同時還會在這裏把傳遞過來的四個參數分別賦值給mLeft、mTop、mRight和mBottom這幾個變量。接下來會在第11行調用onLayout()方法,正如onMeasure()方法中的默認行爲一樣,也許你已經迫不及待地想知道onLayout()方法中的默認行爲是什麼樣的了。進入onLayout()方法,咦?怎麼這是個空方法,一行代碼都沒有?!

沒錯,View中的onLayout()方法就是一個空方法,因爲onLayout()過程是爲了確定視圖在佈局中所在的位置,而這個操作應該是由佈局來完成的,即父視圖決定子視圖的顯示位置。既然如此,我們來看下ViewGroup中的onLayout()方法是怎麼寫的吧,代碼如下:[java] viewplaincopy在CODE上查看代碼片派生到我的代碼片

  1. @Override  
  2. protected abstract void onLayout(boolean changed, int l, int t, int r, int b);  
可以看到,ViewGroup中的onLayout()方法竟然是一個抽象方法,這就意味着所有ViewGroup的子類都必須重寫這個方法。沒錯,像LinearLayout、RelativeLayout等佈局,都是重寫了這個方法,然後在內部按照各自的規則對子視圖進行佈局的。由於LinearLayout和RelativeLayout的佈局規則都比較複雜,就不單獨拿出來進行分析了,這裏我們嘗試自定義一個佈局,藉此來更深刻地理解onLayout()的過程。

 

自定義的這個佈局目標很簡單,只要能夠包含一個子視圖,並且讓子視圖正常顯示出來就可以了。那麼就給這個佈局起名叫做SimpleLayout吧,代碼如下所示:

[java] viewplaincopy在CODE上查看代碼片派生到我的代碼片
  1. public class SimpleLayout extends ViewGroup {  
  2.   
  3.     public SimpleLayout(Context context, AttributeSet attrs) {  
  4.         super(context, attrs);  
  5.     }  
  6.   
  7.     @Override  
  8.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  9.         super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
  10.         if (getChildCount() > 0) {  
  11.             View childView = getChildAt(0);  
  12.             measureChild(childView, widthMeasureSpec, heightMeasureSpec);  
  13.         }  
  14.     }  
  15.   
  16.     @Override  
  17.     protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  18.         if (getChildCount() > 0) {  
  19.             View childView = getChildAt(0);  
  20.             childView.layout(00, childView.getMeasuredWidth(), childView.getMeasuredHeight());  
  21.         }  
  22.     }  
  23.   
  24. }  
代碼非常的簡單,我們來看下具體的邏輯吧。你已經知道,onMeasure()方法會在onLayout()方法之前調用,因此這裏在onMeasure()方法中判斷SimpleLayout中是否有包含一個子視圖,如果有的話就調用measureChild()方法來測量出子視圖的大小。

 

接着在onLayout()方法中同樣判斷SimpleLayout是否有包含一個子視圖,然後調用這個子視圖的layout()方法來確定它在SimpleLayout佈局中的位置,這裏傳入的四個參數依次是0、0、childView.getMeasuredWidth()和childView.getMeasuredHeight(),分別代表着子視圖在SimpleLayout中左上右下四個點的座標。其中,調用childView.getMeasuredWidth()和childView.getMeasuredHeight()方法得到的值就是在onMeasure()方法中測量出的寬和高。

這樣就已經把SimpleLayout這個佈局定義好了,下面就是在XML文件中使用它了,如下所示:

[html] viewplaincopy在CODE上查看代碼片派生到我的代碼片
  1. <</span>com.example.viewtest.SimpleLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:layout_width="match_parent"  
  3.     android:layout_height="match_parent" >  
  4.       
  5.     <</span>ImageView   
  6.         android:layout_width="wrap_content"  
  7.         android:layout_height="wrap_content"  
  8.         android:src="@drawable/ic_launcher"  
  9.         />  
  10.       
  11. </</span>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] viewplaincopy在CODE上查看代碼片派生到我的代碼片
  1. @Override  
  2. protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  3.     if (getChildCount() > 0) {  
  4.         View childView = getChildAt(0);  
  5.         childView.layout(00200200);  
  6.     }  
  7. }  
這樣getWidth()方法得到的值就是200- 0 =200,不會再和getMeasuredWidth()的值相同了。當然這種做法充分不尊重measure()過程計算出的結果,通常情況下是不推薦這麼寫的。getHeight()與getMeasureHeight()方法之間的關係同上,就不再重複分析了。

 

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

 

三. onDraw()

measure和layout的過程都結束後,接下來就進入到draw的過程了。同樣,根據名字你就能夠判斷出,在這裏才真正地開始對視圖進行繪製。ViewRoot中的代碼會繼續執行並創建出一個Canvas對象,然後調用View的draw()方法來執行具體的繪製工作。draw()方法內部的繪製過程總共可以分爲六步,其中第二步和第五步在一般情況下很少用到,因此這裏我們只分析簡化後的繪製過程。代碼如下所示:

[java] viewplaincopy在CODE上查看代碼片派生到我的代碼片
  1. public void draw(Canvas canvas) {  
  2.     if (ViewDebug.TRACE_HIERARCHY) {  
  3.         ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);  
  4.     }  
  5.     final int privateFlags = mPrivateFlags;  
  6.     final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&  
  7.             (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);  
  8.     mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;  
  9.     // Step 1, draw the background, if needed  
  10.     int saveCount;  
  11.     if (!dirtyOpaque) {  
  12.         final Drawable background = mBGDrawable;  
  13.         if (background != null) {  
  14.             final int scrollX = mScrollX;  
  15.             final int scrollY = mScrollY;  
  16.             if (mBackgroundSizeChanged) {  
  17.                 background.setBounds(00,  mRight - mLeft, mBottom - mTop);  
  18.                 mBackgroundSizeChanged = false;  
  19.             }  
  20.             if ((scrollX | scrollY) == 0) {  
  21.                 background.draw(canvas);  
  22.             } else {  
  23.                 canvas.translate(scrollX, scrollY);  
  24.                 background.draw(canvas);  
  25.                 canvas.translate(-scrollX, -scrollY);  
  26.             }  
  27.         }  
  28.     }  
  29.     final int viewFlags = mViewFlags;  
  30.     boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;  
  31.     boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;  
  32.     if (!verticalEdges && !horizontalEdges) {  
  33.         // Step 3, draw the content  
  34.         if (!dirtyOpaque) onDraw(canvas);  
  35.         // Step 4, draw the children  
  36.         dispatchDraw(canvas);  
  37.         // Step 6, draw decorations (scrollbars)  
  38.         onDrawScrollBars(canvas);  
  39.         // we're done...  
  40.         return;  
  41.     }  
  42. }  
可以看到,第一步是從第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] viewplaincopy在CODE上查看代碼片派生到我的代碼片
  1. public class MyView extends View {  
  2.   
  3.     private Paint mPaint;  
  4.   
  5.     public MyView(Context context, AttributeSet attrs) {  
  6.         super(context, attrs);  
  7.         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  8.     }  
  9.   
  10.     @Override  
  11.     protected void onDraw(Canvas canvas) {  
  12.         mPaint.setColor(Color.YELLOW);  
  13.         canvas.drawRect(00, getWidth(), getHeight(), mPaint);  
  14.         mPaint.setColor(Color.BLUE);  
  15.         mPaint.setTextSize(20);  
  16.         String text = "Hello View";  
  17.         canvas.drawText(text, 0, getHeight() / 2, mPaint);  
  18.     }  
  19. }  
可以看到,我們創建了一個自定義的MyView繼承自View,並在MyView的構造函數中創建了一個Paint對象。Paint就像是一個畫筆一樣,配合着Canvas就可以進行繪製了。這裏我們的繪製邏輯比較簡單,在onDraw()方法中先是把畫筆設置成黃色,然後調用Canvas的drawRect()方法繪製一個矩形。然後在把畫筆設置成藍色,並調整了一下文字的大小,然後調用drawText()方法繪製了一段文字。

 

就這麼簡單,一個自定義的視圖就已經寫好了,現在可以在XML中加入這個視圖,如下所示:

[html] viewplaincopy在CODE上查看代碼片派生到我的代碼片
  1. <</span>LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:layout_width="match_parent"  
  3.     android:layout_height="match_parent" >  
  4.   
  5.     <</span>com.example.viewtest.MyView   
  6.         android:layout_width="200dp"  
  7.         android:layout_height="100dp"  
  8.         />  
  9.   
  10. </</span>LinearLayout>  
將MyView的寬度設置成200dp,高度設置成100dp,然後運行一下程序,結果如下圖所示:

 

                  

圖中顯示的內容也正是MyView這個視圖的內容部分了。由於我們沒給MyView設置背景,因此這裏看不出來View自動繪製的背景效果。

當然了Canvas的用法還有很多很多,這裏我不可能把Canvas的所有用法都列舉出來,剩下的就要靠大家自行去研究和學習了。

到此爲止,我們把視圖繪製流程的第三階段也分析完了。整個視圖的繪製過程就全部結束了,你現在是不是對View的理解更加深刻了呢?感興趣的朋友可以繼續閱讀 Android視圖狀態及重繪流程分析,帶你一步步深入瞭解View(三) 。

 

第一時間獲得博客更新提醒,以及更多技術信息分享,歡迎關注我的微信公衆號,掃一掃下方二維碼或搜索微信號guolin_blog,即可關注。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章