Android UI測量、布局、繪制過程探究


在上一篇博客《Android中Activity啟動過程探究》中,已經從ActivityThread.main()開始,一路摸索到ViewRootImpl.performTraversals()了。本篇就來探究UI的繪制過程。

 

performTraversals()方法非常長,其中關鍵性的三個步驟是依次調用了performMeasure(), performLayout(), performDraw()。分別來看這三個步驟吧!

 

Measure過程(測量過程)

直接來看performMeasure()方法。

該方法非常直接了當的調用了mViewmeasure()方法。mView是一個android.view.View對象,在ViewRootImpl類中的mView是整個UI的根節點,實際上也就是PhoneWindow中的mDecor對象,再說具體點,就是一個Activity所對應的一個屏幕(不包括頂部的系統狀態條)中的視圖,包括可能存在也可能不存在的ActionBar。

 

接着來看View.measure()方法。請注意紅色標記的部分。

該方法中調用了View.onMeasure()方法。View.onMeasure()非常的簡單,就是直接調用了一個setMeasureDimension()的方法。

setMeasureDimension()方法中最關鍵的步驟是對View的兩個成員變量進行一次賦值,如下圖所示,請注意紅色標記的部分:

顯然,View的onMeasure()方法似乎有點過於簡單了,都沒有明白所謂的measure步驟到底做了什么事情。經過筆者這兩天的研究,已經大致明白了measure步驟是干了什么,這里我先將結論拿出來說:(以下內容非常關鍵,請仔細閱讀(⊙o⊙)

View.onMeasure()方法的作用是:設置自己所需要的大小

對於單一的簡單一個View對象來說,就是這么簡單,比如說一個TextView,要顯示一段文字,那么當調用onMeasure()方法,這個方法的目的就是要給這個TextView設置好它所需要的寬度和高度,在程序代碼中的體現,實際上就是mMeasuredWidthmMeasuredHeight兩個成員變量,調用setMeasuredDimension()為這兩個變量賦值。

這時問題來了,挖掘機技術到底哪家強?\(^o^)/~

這時問題來了,TextView的高度和寬度是多少呢?我們回想到我們使用TextView的時候有兩個很重要的參數,在xml中叫layout_width, layout_height。在代碼中是LayoutParams對象的兩個屬性,也分別表示寬度和高度。

而這個數值有如下幾種情況:

1.match_parent:填充父控件,與父控件一樣大

2.wrap_content:包裹內容,僅僅將自己的內容包裹起來那么大就足夠了

3.指定的具體數值,30dp,55px之類的。

接下來,TextView需要給自己設置大小,

如果是match_parent的情況,那么就需要和父控件一樣大。

如果是wrap_content的情況,那么我需要知道自己顯示出的文字的高度和寬度是多少。

如果是具體的數值,我就直接顯示好了,當然,不能超過父控件的大小

 

很顯然,TextView想要准確的計算出自己所需要的大小需要得知兩個信息,一個是我的寬度高度的屬性是什么,另一個是父控件有多大,或者說是我能夠使用多大的空間

而這一兩組數據在寬度和高度兩個值中都需要得到體現,常理來說我們需要四個參數。但是Android很巧妙的用兩個參數就解決了問題。

請注意onMeasure()的兩個整型參數。widthMeasureSpec, heightMeasureSpec

這兩個數值都是int型的,在Java中,int型的變量是32位的,而Android的設計者將這個整型的最高兩位當做type來用,低30位當做數值來用。

高2位總共可以組成三種情況,分別為:AT_MOST、UNSPECIFIED、EXACTLY。

低30位所表示的整型數值,表示着,當前控件可用的最大寬度與高度。再詳細解釋下所謂可用的最大寬度和高度:對於ViewRoot來說,那么它的可用寬度和可用高度就是屏幕的尺寸,假如它設置了一個padding = 10,上下左右都為10px。那么內部的東西就不能放置在padding中,所以挨個調用子控件的onMeasure()方法的時候,給它的可用尺寸就是(屏幕寬度 - 20,屏幕高度 - 20)(左右兩邊都有padding嘛~)。就是這么回事。

 

這兩個特殊的整型參數不需要我們手動的去做位移操作來取有意義的數值,可以使用MeasureSpec.getMode()MeasureSpec.getSize()來獲取。

當onMeasure()執行完畢后,就可以調用該View的getMeasuredWidth()和getMeasuredHeight()來獲取這個控件的高度與寬度了。

 

所以總結下onMeasure()的作用

1.onMeasure()方法是measure()調用的。

2.onMeasure()方法的作用是要計算出當前控件自身所需要的大小是多少,計算的根據是在xml或者代碼中設置的寬度和高度的參數,參數指明了要求你是填充父控件(match_parent)還是包裹內容(wrap_content)還是精確的一個大小,但最終你的大小不應該超過父控件給你提供的空間。

3.onMeasure()方法結束之前必須調用setMeasuredDimension()來設置View.mMeasuredWidth和View.mMeasuredHeight兩個參數。這個方法的兩個整型是單純的表示寬度與高度,整個32位都是用來表示數值。

4.onMeasure()方法執行完畢后,該View的尺寸已經得到確認,需要使用的話,調用View.getMeasuredWidth()和View.getMeasuredHeight()來獲取。

 

以上就是measure過程的內容。接下來measure過程一路遞歸調用子類的onMeasure(),一路退棧返回,終於把所有的measure全部執行完了,所有的控件都已經知道了自己的大小,就開始調用ViewRootImpl.performLayout()方法了。

 

Layout過程(布局過程)

在ViewRootImpl.performLayout()方法中,調用了根視圖的layout()方法,也就是View.layout()方法。

 

接下來看View.layout()方法

layout()方法有四個參數,分別是left, top, right, bottom,它們是相對於父控件的位移距離。哎,我還是畫個圖吧~

如上圖所示,left指的是該View的左邊到其父控件左邊的距離,top也是類似的意思,而right是left加上該控件的寬度,總結起來的話:就是該控件四條邊到父控件左上角頂點的距離。

再回到代碼,注意紅色標記的部分,先調用了setFrame()方法。

setFrame()方法是個很重要的方法!

注意上面用紅色標記的部分,其中newWidth並不是mMeasuredWidth,而是用right - left。難道我(View)的寬度值都不算數嗎?要通過所謂的右邊減去左邊來確定我的寬度?

沒錯就是這樣,setFrame()的這個Frame,可以理解為一個View真正渲染到屏幕上的矩形區域。而四個參數left, top, right, bottom就是指定了這個矩形區域的四個頂點。

可以想象一下這樣的情況,父控件的寬度是500,padding值為0,那么其子控件可用的寬度自然就是500了,假如有一個控件已經占滿了300px的寬度,而另一個控件同樣需要300px的寬度,而父控件只剩下了200px的寬度,Android是怎么處理這件事情的呢?

首先父控件調用onMeasure()方法,遍歷子控件,調用子控件的onMeasure()方法,這樣一來大家都知道了自己有多大了。

然后父控件調用了onLayout()方法,onLayout()方法實際上是給自己的子控件布局,假如一個控件是View的話,它就沒有子控件了,onLayout實際上就沒什么作用。回到這個情景,onLayout()遍歷的調用子控件的layout()方法,指定子控件的上下左右的位置,layout()方法中調用setFrame()方法,根據參數值設置自己實際渲染的區域。那么當第一個控件占了300px的寬度,這個時候父控件已經知道了剩下的可用寬度只有200px了,那么它就會根據這個值來進行調整,將計算好,根據剩下的空間把第二個子控件的上下左右四個參數交給它的layout方法,讓他去設置自己的frame。也就是說,第二個空間的Frame的寬度只有200px了,並不會超出這個范圍。

 

這里得出一個事實:measure出來的寬度與高度,是該控件期望得到的尺寸,但是真正顯示到屏幕上的位置與大小是由layout()方法來決定的。left, top決定位置,right,bottom決定frame渲染尺寸。

 

回到源代碼,以上的代碼是View的,所以onLayout()中是空的。在具體的ViewGroup中有更加具體的實現。

接下來是對layout步驟的總結。(以下內容非常重要哦

1.要設置一個View的位置與實際渲染的大小需要調用View.layout()方法。

2.layout()方法中的setFrame()方法是設置該控件的位置與實際渲染的大小。這是layout過程中最關鍵,最重要的步驟。

3.接下來就是遞歸,遍歷子控件,並調用他們的onLayout()方法。

也就是說你需要實現一個ViewGroup的話,你只需要在onLayout()方法中遍歷的調用子控件的onLayout()方法就行了,需要做的事情就是把left, top, right, bottom這四個值算好。

 

以上就是Layout過程。

 

Draw過程(繪制過程)

繪制過程沒有研究的太詳細,實際上它就是調用Canvas的接口了,然后Canvas又是和OPENGLES什么的相關,具體的繪圖方法都是native的,沒有看到,但onDraw()方法在我的了解下是這樣的。就以TextView來解釋。

TextView里面有很多屬性,比如文字大小,文字顏色等等,根據這些屬性設置Paint對象,然后在onDraw()方法里用這個paint對象把text的內容都畫出來。當我們每次調用一個public的方法的時候,比如setText(),它就會先更改自身的屬性,然后要求重新繪制一次,於是乎,onDraw()就又調用了一次。也就是說,onDraw()是已經將所有的屬性都考慮了進來,並不是我們改變什么它就繪制什么,而是從頭到尾都繪制一遍。

這就是我對Draw過程的理解。

 

最后再貼一個自己實現的LinearLayout,再來鞏固下上面的measure和layout步驟,大家體會一下。只實現了垂直布局的部分。

  1 /**
  2  * 自己實現的LinearLayout
  3  * @author kross(krossford@foxmail.com)
  4  * @update 2014-10-16 19:42:47 第一次編寫,實現垂直布局
  5  * */
  6 public class KLinearLayout extends ViewGroup {
  7     
  8     private static final String TAG = "KLinearLayout";
  9     
 10     /** 垂直布局 */
 11     public static final byte ORITENTATION_VERTICAL = 0x1;
 12     /** 水平布局 */
 13     public static final byte ORITENTATION_HORIZONTAL = 0x0;
 14     
 15     /** 線性布局的方向,默認值為水平
 16      * @see #ORITENTATION_HORIZONTAL
 17      * @see #ORITENTATION_VERTICAL */
 18     private int mOritentation = ORITENTATION_HORIZONTAL;
 19     
 20     /** 最終的寬度 */
 21     private int mWidth;
 22     /** 最終的高度 */
 23     private int mHeight;
 24     
 25     public KLinearLayout(Context context) {
 26         super(context);
 27         mOritentation = ORITENTATION_HORIZONTAL;
 28     }
 29     
 30     /**
 31      * 設置線性布局的方向:垂直或水平
 32      * @param oritentation
 33      * @see #ORITENTATION_HORIZONTAL
 34      * @see #ORITENTATION_VERTICAL
 35      * */
 36     public void setOritentation(byte oritentation) {
 37         mOritentation = oritentation;
 38     }
 39     
 40     @Override
 41     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 42         Log.i(TAG, "onMeasure");
 43         if (mOritentation == ORITENTATION_HORIZONTAL) {
 44             measureHorizontal(widthMeasureSpec, heightMeasureSpec);
 45         } else {
 46             measureVertical(widthMeasureSpec, heightMeasureSpec);
 47         }
 48     }
 49 
 50     @Override
 51     protected void onLayout(boolean changed, int l, int t, int r, int b) {
 52         Log.i(TAG, "onLayout l:" + l + " t:" + t + " r:" + r + " b:" + b);
 53         
 54         if (mOritentation == ORITENTATION_HORIZONTAL) {
 55             layoutHorizontal(l, t, r, b);
 56         } else {
 57             layoutVertical(l, t, r, b);
 58         }
 59     }
 60     
 61     /**
 62      * 垂直測量
 63      * */
 64     private void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
 65         Log.i(TAG, "measureVertical");
 66         
 67         int widthMode = MeasureSpec.getMode(widthMeasureSpec);
 68         int widthSize = MeasureSpec.getSize(widthMeasureSpec);
 69         
 70         int heightMode = MeasureSpec.getMode(heightMeasureSpec);
 71         int heightSize = MeasureSpec.getSize(heightMeasureSpec);
 72         
 73         /** 
 74          * 已經使用了的高度,容器是空的,已經使用的高度為0,如果已經存在一個高度為x的子控件,這個值為x。
 75          * 這個值也表示,所有的子控件所需要的高度總值。
 76          */
 77         int heightUsed = 0;    
 78         View childTemp = null;
 79         for (int index = 0; index < getChildCount(); index++) {    //遍歷子控件
 80             childTemp = getChildAt(index);
 81             measureChildWithMargins(childTemp, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);    //獲取子控件並測量它的大小
 82             LinearLayout.LayoutParams childLp = (LinearLayout.LayoutParams)childTemp.getLayoutParams();
 83             
 84             //子控件的高度,包括子控件的上下外邊距一起累加到heightUsed值中
 85             heightUsed = heightUsed + childTemp.getMeasuredHeight() + childLp.topMargin + childLp.bottomMargin;    
 86             //因為是垂直布局,所以寬度直選最大的一個
 87             mWidth = Math.max(mWidth, childTemp.getMeasuredWidth() + childLp.leftMargin + childLp.rightMargin);    
 88         }
 89         
 90         mWidth = mWidth + getPaddingLeft() + getPaddingRight();    //加上左右內邊距
 91         
 92         switch (widthMode) {
 93         case MeasureSpec.AT_MOST:    //wrap_parent
 94             mWidth = Math.min(widthSize, mWidth);    //因為是包裹內容,所以寬度應該是盡可能的小
 95             break;
 96         case MeasureSpec.EXACTLY:    //match_parent
 97             mWidth = widthSize;    //與父控件一樣大,那么寬度應該是父控件給的,也就是參數所給的
 98             break;
 99         case MeasureSpec.UNSPECIFIED:
100             break;
101         }
102         
103         mHeight = heightUsed + getPaddingTop() + getPaddingBottom();    //所有子控件的高度和 + 上下內邊距
104         
105         switch (heightMode) {
106         case MeasureSpec.AT_MOST:    //wrap_parent
107             mHeight = Math.min(heightSize, mHeight);
108             break;
109         case MeasureSpec.EXACTLY:    //match_parent
110             mHeight = heightSize;
111             break;
112         case MeasureSpec.UNSPECIFIED:
113             break;
114         }
115         
116         setMeasuredDimension(mWidth, mHeight);
117     }
118     
119     /**
120      * 水平測量
121      * */
122     private void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) {
123         Log.i(TAG, "measureHorizontal");
124         setMeasuredDimension(100, 100);
125     }
126     
127     /**
128      * 垂直布局
129      * */
130     private void layoutVertical(int l, int t, int r, int b) {
131         int avaliableLeft = getPaddingLeft();
132         int avaliableTop = getPaddingTop();
133         
134         View childTemp = null;
135         for (int i = 0; i < getChildCount(); i++) {
136             childTemp = getChildAt(i);
137             LinearLayout.LayoutParams childLp = (LinearLayout.LayoutParams)childTemp.getLayoutParams();
138             //layout()方法會確切的限制View的顯示大小,真正顯示到屏幕上的矩形區域,是由layout的四個參數所決定的。
139             childTemp.layout(avaliableLeft + childLp.leftMargin, 
140                     avaliableTop + childLp.topMargin, 
141                     childTemp.getMeasuredWidth() + avaliableLeft + childLp.rightMargin, 
142                     childTemp.getMeasuredHeight() + avaliableTop + childLp.bottomMargin);
143             avaliableTop = avaliableTop + childTemp.getMeasuredHeight() + childLp.topMargin + childLp.bottomMargin;
144         }
145     }
146     
147     /**
148      * 水平布局
149      * */
150     private void layoutHorizontal(int l, int t, int r, int b) {
151         
152     }
153 }

然后再貼上一個使用它的代碼:

 1 public class MainActivity extends Activity {
 2     
 3     
 4     @SuppressLint("ServiceCast") @Override
 5     protected void onCreate(Bundle savedInstanceState) {
 6         super.onCreate(savedInstanceState);
 7         LinearLayout root = (LinearLayout)LayoutInflater.from(this).inflate(R.layout.activity_main, null);
 8         setContentView(root);
 9         
10         KLinearLayout myLinearLayout = new KLinearLayout(this);
11         myLinearLayout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
12         myLinearLayout.setPadding(10, 20, 30, 40);
13         myLinearLayout.setOritentation(KLinearLayout.ORITENTATION_VERTICAL);
14         
15         root.addView(myLinearLayout);
16         
17         TextView tv3 = new TextView(this);
18         tv3.setText("abcd哈哈你好");
19         tv3.setTextSize(50);
20         LayoutParams tv3lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
21         tv3lp.setMargins(10, 10, 10, 10);
22         tv3.setLayoutParams(tv3lp);
23         
24         myLinearLayout.addView(tv3);
25         
26         TextView tv1 = new TextView(this);
27         tv1.setText("adbcdsaf");
28         tv1.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
29         
30         myLinearLayout.addView(tv1);
31         
32         
33         TextView tv2 = new TextView(this);
34         tv2.setText("abcd哈哈你好");
35         tv2.setTextSize(100);
36         tv2.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
37         
38         myLinearLayout.addView(tv2);
39         
40         TextView tv4 = new TextView(this);
41         tv4.setText("大號大號大號大號");
42         tv4.setTextSize(100);
43         tv4.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
44         
45         myLinearLayout.addView(tv4); 
46     }
47 }

最后,效果如圖所示:

 

以上。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM