Android的onMeasure和onLayout And MeasureSpec揭祕

Android中自定義ViewGroup最重要的就是onMeasure和onLayout方法,都需要重寫這兩個方法,ViewGroup繪製 的過程是這樣的:onMeasure → onLayout → DispatchDraw

[java] view plaincopy
  1.   

其實我覺得官方文檔解釋有大大的問題,剛開始一直很疑惑onMeasure和onLayout是什麼意思,看了很多資料後豁然開朗,總結如下

首先要知道ViewGroup是繼承View的,後面的解釋跟View有關。ViewGourp可以包含很多個View,View就是它的孩子,比如LinearLayout佈局是一個ViewGroup,在佈局內可以放TextEdit、ImageView等等常用的控件,這些叫子View,當然不限於這個固定的控件。

onMeasure → onLayout → DispatchDraw:onMeasure負責測量這個ViewGroup和子View的大小,onLayout負責設置子View的佈局,DispatchDraw就是真正畫上去了。

onMeasure

官方解釋:

protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec)

Measure the view and its content to determine the measured width and the measured height. 即 測量View和它的內容決定寬度和高度。
說實在的,官方文檔說測量我剛開始很疑惑,onMeasure翻譯過來是測量,根本不知道它的意圖,其實它有兩方面作用:①獲得ViewGroup和子View的寬和高 ②設置子ViewGroup的寬和高,注意,只是寬和高。其實,追蹤onMeasure方法會發現,它繼承自View。
典型的onMeasure的一個實現
[java] view plaincopy
  1. @Override    
  2. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    
  3.     
  4.     int width = MeasureSpec.getSize(widthMeasureSpec);   //獲取ViewGroup寬度    
  5.     int height = MeasureSpec.getSize(heightMeasureSpec);  //獲取ViewGroup高度    
  6.     setMeasuredDimension(width, height);    //設置ViewGroup的寬高    
  7.     
  8.     int childCount = getChildCount();   //獲得子View的個數,下面遍歷這些子View設置寬高    
  9.     for (int i = 0; i < childCount; i++) {    
  10.         View child = getChildAt(i);    
  11.             child.measure(viewWidth, viewHeight);  //設置子View寬高    
  12.         }    
  13.  }  

很明顯,先獲取到了寬高再設置。順序是先設置ViewGroup的,再設置子View。
其中,設置ViewGroup寬高的方法是 setMeasureDimension(),查看這個方法的源代碼,它在view.class下
[java] view plaincopy
  1. protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {    
  2.         boolean optical = isLayoutModeOptical(this);    
  3.         if (optical != isLayoutModeOptical(mParent)) {    
  4.             Insets insets = getOpticalInsets();    
  5.             int opticalWidth  = insets.left + insets.right;    
  6.             int opticalHeight = insets.top  + insets.bottom;    
  7.     
  8.             measuredWidth  += optical ? opticalWidth  : -opticalWidth;    
  9.             measuredHeight += optical ? opticalHeight : -opticalHeight;    
  10.         }    
  11.         mMeasuredWidth = measuredWidth;  //這就是保存到類變量  
  12.         mMeasuredHeight = measuredHeight;    
  13.     
  14.         mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;    
  15.  }  

setMeasureDimension方法必須由onMeasure調用,上上的代碼剛好是在onMeasure中調用,所以才符合要求。那設置的這個寬高保存在哪裏呢?源代碼中也可以看出,它保存在ViewGroup中:mMeasuredWidth,mMeasuredHeight是View這個類中的變量。
接下來是設置子View的寬高,每個子View都會分別設置,這個寬高當然是自己定義的。child.measure(viewWidth, viewHeight);調用的是measure方法,注意這個方法是屬於子View的方法,那設置的高度保存在哪裏呢?對了,就是每個子View中,而不是ViewGroup中,這點要分清楚。
再來看看measure的實現
[java] view plaincopy
  1. public final void measure(int widthMeasureSpec, int heightMeasureSpec) {  
  2. <span style="white-space:pre">  </span>.........    
  3. <span style="white-space:pre">  </span>// measure ourselves, this should set the measured dimension flag back    
  4.         onMeasure(widthMeasureSpec, heightMeasureSpec);    
  5. <span style="white-space:pre">  </span>..........    
  6. }  

其實它又調用了View類中的onMeasure方法,在看View.class的onMeasure方法
[java] view plaincopy
  1. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    
  2.         setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),    
  3.                 getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));    
  4. }  

很奇怪吧,又繞回了原來的setMeasureDimension方法,說到底,真正設置ViewGroup和子View寬高的都是setMeasureDimension方法,但是爲什麼上面child.measure(viewWidth, viewHeight);不直接調用child.setMeasureDimension(viewWidth,viewHeight)呢,多方便啊。因爲setMeasureDimension()只能由onMeasure()方法調用。
所以onMeasure沒什麼神奇之處,就是測量(Measure)和設置(determine)寬高,現在終於理解API文檔所解釋的。

onLayout
官方解釋 
protected abstract void onLayout (boolean changed, int l, int t, int r, int b)
Called from layout when this view should assign a size and position to each of its children. 
它纔是設置子View的大小和位置。onMeasure只是獲得寬高並且存儲在它各自的View中,這時ViewGroup根本就不知道子View的大小,onLayout告訴ViewGroup,子View在它裏面中的大小和應該放在哪裏。注意兩個的區別,我當時也被搞得一頭霧水。
參數int l, int t, int r, int b不用多說,就是ViewGroup在屏幕的位置。
[java] view plaincopy
  1. @Override      
  2. protected void onLayout(boolean changed, int left, int top, int right, int bottom) {      
  3.     int mTotalHeight = 0;      
  4.     // 當然,也是遍歷子View,每個都要告訴ViewGroup      
  5.     int childCount = getChildCount();      
  6.     for (int i = 0; i < childCount; i++) {      
  7.         View childView = getChildAt(i);      
  8.         // 獲取在onMeasure中計算的視圖尺寸      
  9.         int measureHeight = childView.getMeasuredHeight();      
  10.         int measuredWidth = childView.getMeasuredWidth();      
  11.         childView.layout(left, mTotalHeight, measuredWidth, mTotalHeight + measureHeight);          
  12.         mTotalHeight += measureHeight;      
  13.     }      
  14. }  


接下來就是DispatchDraw。。。
好了,現在的理解只能是這樣

ADD:關於MeasureSpec
MeasureSpec是View中的一個內部類,A MeasureSpec encapsulates the layout requirements passed from parent to child. 即封裝了佈局傳遞的參數。它代表Height和Width,先貼一段使用情況的代碼:
[java] view plaincopy
  1. @Override    
  2. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    
  3.     super.onMeasure(widthMeasureSpec, heightMeasureSpec);    
  4.     int width = MeasureSpec.getSize(widthMeasureSpec);  //獲取真實width  
  5.     int height = MeasureSpec.getSize(heightMeasureSpec);   //獲取真實height  
  6.     setMeasuredDimension(width, height);   //設置ViewGroup的寬高  
  7.     for (int i = 0; i < getChildCount(); i++) {       
  8.         getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);    //遍歷孩子設置寬高    
  9.     }    
  10. }  

爲什麼onMeasure的參數widthMeasureSpec和heightMeasure要經過getSize()方法纔得到真實的寬高 ,既然參數是int類型爲什麼不直接傳遞真實寬高,其實這暗藏玄機。我們當然是直接找到MeasureSpec的源碼來看咯
[java] view plaincopy
  1. public static class MeasureSpec {    
  2.        private static final int MODE_SHIFT = 30;    //    
  3.        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;    
  4.        public static int getMode(int measureSpec) {    
  5.             return (measureSpec & MODE_MASK);    
  6.        }    
  7.        public static int getSize(int measureSpec) {    
  8.             return (measureSpec & ~MODE_MASK);    
  9.        }    
  10. }  

看getSize方法,他是利用傳遞進來的參數來解析的。其實直接這麼看會很暈,根本不知所云,所以回頭看看onMeasure方法,調試onMeasure方法,裏面的widthMeasureSpec、heightMeasureSpec和解析出來的值width、height的值如下:

發現解析前後的值差很遠,再結合源代碼 widthMeasureSpec & ~ MODE_MASK,運算後剛好匹配得到width。運算方法:0x3=0011, 它向左移位30位,得到1100 0000 .....(1後面一共有30個0.) ~取反後就是0011 1111……(0後面有30個1). 上面的widthMeasureSpec是1073742304,轉換成二進制是 0100 0000 0000 0000 0000 0001 1110 0000,和前面那個 ~MODE_MASK &之後(注意MODE_MASK要先取反再與widthMeasureSpec),最前面那個2個1就去掉了,widthMeasureSpec只留下了後面一段有1,即得到0000 …(省略16個0)… 0001 1110 0000,得到的值轉換成 十進制剛好是480,完美,轉換後得到了真實的width。手機的屏幕剛好是480*854,這是小米1的屏幕。

PS:但是爲什麼要費這麼大的周折呢?爲什麼要移位向左移位30呢?
仔細看getMode()方法,它也是使用和getSize()同樣的參數來解析,其實getSize只是用了measureSpec中的一部分來代表width or height,剩下的高位用來代表getMode的值。
且看widthMeasureSpec的值,它左邊最高兩位是01,然後和MODE_MASK & 了之後,得到0100……(1後省了30個0),即0x40000000,查看MeasureSpec中的幾個常量:
AT_MOST = 0x80000000
EXACTLY = 0x40000000
UPSPECIFIED = 0x00000000
getMode解析之後得到EXACTILY。所以說一個measureSpec參數就得到了兩個值,一個是具體的大小值,一個是模式的值,再看看官方文檔的解釋,終於明白爲什麼叫encapsulates 了,不得不說Google工程師牛。
我們通常在XML佈局中使用的layout_width或layout_height可以指定兩種值,一種是具體的,比如100dp,一種是Math_Parent之類的,就是封裝到這裏來了... 對應兩個值哦~

回到前面的爲什麼要左移30位的問題。因爲int類型是32位,原始值0x3左移30位後使用最高兩位來表示MODE值,我們傳遞的measureSpec是正數的時候,怎麼也不會用到最高兩位表示getSize要解析的真實值。也就是即使真實值使用到了3221225471,也可以正確解析出真實值,不用擔心真實值會溢出。地球上也沒那麼大分辨率的屏幕哦!不過這是我個人的猜測而已。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章