文章獨家授權公眾號:碼個蛋
更多分享:http://www.cherylgood.cn
我們在上一篇Android之View的誕生之謎分析了從Activity的創建到View開始執行測量、布局、繪制之前所經歷的一些事情以及處理狀態欄的一些小技巧等,如果你也想知道的話,不妨點擊一下-Android之View的誕生之謎哦,或許你面有你想要的呢
死亡三部曲第一部(Measure)->我只想知道你的三圍是多少
- 我們在上一章節Android之View的誕生之謎中分析了系統從啟動actiivty到調用setContentView加載我們的xml布局文件,但是此時我們的View是不可見的,因為我們還沒有對其進行如下操作:
1、測量:我還不知道你的三圍呢(你要占多少屏幕),我怎么能輕易讓你出場呢----測量工作
2、布局:你把三圍給我了,但是你還沒告訴我你要站在那里,對位置的分布有什么要求----行布局操作
3、繪制:好,現在我要給你花點妝,美美地出場----繪制操作
OK,我們在上篇中分析道,系統加載好布局資源之后,會觸發ViewRootImpl的performTraversals方法,在該方法內部會開始執行測量、布局、繪制的工作,也就是我們的死亡三部曲的開始。
-
我們來看ViewRootImpl的performTraversals方法的源碼,為了簡潔,我只留下關鍵的代碼。
private void performTraversals() { ... if (!mStopped) { //1、獲取頂層布局的childWidthMeasureSpec int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); //2、獲取頂層布局的childHeightMeasureSpec int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); //3、測量開始測量 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); } } if (didLayout) { //4、執行布局方法 performLayout(lp, desiredWindowWidth, desiredWindowHeight); ... } if (!cancelDraw && !newSurface) { ... //5、開始繪制了哦 performDraw(); } } ... }
可以看到,里面按順序調用了performMeasure、performLayout、performDraw三個方法,也就是對應的測量、布局、繪制,再繼續深入之前,我們需要先補充點能量,對MeasureSpec已了解的同學可以跳過下面一段。
能量站啟動。。。。。。
1、MeasureSpec
MeasureSpec 是個什么東西呢?其實MeasureSpec是View內部的一個靜態類,在編寫測量控件的代碼中一定能見到其美麗的身影,他的誕生是那么的無私->為何輔助view的測量能夠更好的進行。
-
我們可以先從官方文檔中初步了解一下:
- A MeasureSpec encapsulates the layout requirements passed from parent to child. Each MeasureSpec represents a requirement for either the width or the height. A MeasureSpec is comprised of a size and a mode. There are three possible modes:
- MeasureSpec對象中封裝了從父對象傳遞給孩子的布局所需數數據(你要成為我的子控件,你要在我里面占位置,你先要知道我有多少空間吧?)。每一個MeasureSpec對象包含了對于寬度和高度的描述(也就是父控件告訴子控件,我有多大點地和我對于空間的使用策略等)。 MeasureSpec由大小和模式組成。有三種可能的模式:
- 1、UNSPECIFIED 父控件還不知道子控件的大小,對子控件也沒有任何約束,說你想占多少地方就占吧。(這個一般很少用到)
- 2、EXACTLY 這種狀態下的控件的大小是明確的。
- 3、AT_MOST 父控件對子控件說,我還不知道你的大小,我給你自由,我的地方是這么大,你按你的意愿來,但最大也只能跟我一樣大了,注意哦,可能需要二次測量,后面會講到。
為了更好的理解三種模式,我們可以看一下實際測量的源碼里是如何處理的
-
呃我想想,好吧,我們從ViewGroup.measureChild方法入手吧,這個是viewGroup測量下面的childView的方法,看源碼,解釋我就直接寫源碼里了,便于閱讀:
// 從參數我們能得到一些信息 第一個參數是child, // 也就是我們要測量的子view ,第二、第四個參 // 數分別為父view的MeasureSpec,第三個第五個 // 分別表示parentView的寬和高已經被使用了的大小, //從參數上我們可以猜測,子view的測量結果與父 //View的MeasureSpec是息息相關的 protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { //1、獲取子View的layout參數,因為子View的大小也跟布 //局參數相關哦,這種view很氣人,他要跟別人產生一定的距離 final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); //2、測量childView的寬的MeasureSpec,第一個參數會 //傳入parent的 MeasureSpec,第二個參數經過計算后實際 //得到的是parent已被使用的寬度和child的padding和margin //消耗的寬度,第三個參數為child的的大小,這個大小并 //不一定是child最后的大小哦,只能說是我們希望創建的大小 // 例如在xml文件中的layout_width指定的值 final int childWidthMeasureSpec = getChildMeasureSpec (parentWidthMeasureSpec,mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); //3、測量childView的高的MeasureSpec,參數與測量寬類似 //這里就不多說了 final int childHeightMeasureSpec = getChildMeasureSpec( parentHeightMeasureSpec,mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, lp.height); //4、獲得childview的高、寬的MeasureSpec后,就可以 //確定child的大小了 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
-
上面的代碼經過分析就很好理解了,我們繼續看getChildMeasureSpec方法的源碼,看里面是怎么測量出child的寬、高的MeasureSpec的呢?源碼不多,一百多行,我們一起來看下
//從上面我們知道spec 是parent的MeasureSpec,padding是 //已被使用的大小,childDimension為child的大小 public static int getChildMeasureSpec( int spec, int padding, int childDimension) { //1、獲取parent的specMode int specMode = MeasureSpec.getMode(spec); //2、獲取parent的specSize int specSize = MeasureSpec.getSize(spec); //3、size=剩余的可用大小 int size = Math.max(0, specSize - padding); int resultSize = 0; int resultMode = 0; //4、通過switch語句判斷parent的集中mode,分別處理 switch (specMode) { // 5、parent為MeasureSpec.EXACTLY時 case MeasureSpec.EXACTLY: if (childDimension >= 0) { //5.1、當childDimension大于0時,表示child的大小是 //明確指出的,如layout_width= "100dp"; // 此時child的大小= childDimension, resultSize = childDimension; //child的測量模式= MeasureSpec.EXACTLY resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { //5.2、此時為LayoutParams.MATCH_PARENT //也就是 android:layout_width="match_parent" //因為parent的大小是明確的,child要匹配parent的大小 //那么我們就直接讓child=parent的大小就好 resultSize = size; //同樣,child的測量模式= MeasureSpec.EXACTLY resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { //5.3、此時為LayoutParams.WRAP_CONTENT //也就是 android:layout_width="wrap_content" // 這個模式需要特別對待,child說我要的大小剛好夠放 //需要展示的內容就好,而此時我們并不知道child的內容 //需要多大的地方,暫時先把parent的size給他 resultSize = size; //自然,child的mode就是MeasureSpec.AT_MOST的了 resultMode = MeasureSpec.AT_MOST; } break; // 5、parent為AT_MOST,此時child最大不能超過parent case MeasureSpec.AT_MOST: if (childDimension >= 0) { //同樣child大小明確時, //大小直接時指定的childDimension resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // child要跟parent一樣大,resultSize=可用大小 resultSize = size; //因為parent是AT_MOST,child的大小也還是未定的, //所以也是MeasureSpec.AT_MOST resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { //又是特殊情況,先給child可用的大小 resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // 這種模式是很少用的,我們也看下吧 case MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { // 與前面同樣的處理 resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size... find out how big it should // be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size.... find out how // big it should be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } break; } //通過傳入resultSize和resultMode生成一個MeasureSpec.返回 return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
小結:從上面我們了解的MeasureSpec是用來輔助測量view的大小的一個輔助類,我們分析的MeasureSpec的mode和size是根據parent和child相互決定的。下面是我網上收集的一個MeasureSpec圖片
-
能量補充完畢,我們繼續回到開頭的ViewRootImpl.performMeasure源碼上分析,在1、2兩步我們獲得了DecorView的MeasureSpec,然后通過傳入MeasureSpec開始了我們的測量之旅。那么我們繼續看3里面是如何測量的。
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure"); try { //1、mView其實就是我們的頂層DecorView,從DecorView開始測量 mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } }
-
補充:在Android Touch事件分發機制詳解之由點擊引發的戰爭我們分析過DecorView實際是集成自FrameLayout,那么我們看frameLayout,發現frameLayout并沒有measure方法,但是它又繼承自ViewGroup。所以肯定是ViewGroup了,然而,ViewGroup也沒找到measure方法,那么繼續查看其parent 類View,哈哈,在view中被我找到了吧,我們看代碼。只保留了關鍵的一句,不要打我。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) { ... onMeasure(widthMeasureSpec, heightMeasureSpec); ... }
-
從上面我們看到,里面調用了onMeasure方法,這里要注意了:
- 1、我們的ViewGroup并沒有重寫View的onMeasure方法,而但是我們android開發中的四大布局 FrameLayout、LinearLayout、RelativeLayout、AbsoluteLayout都是通過繼承ViewGroup來實現的,而且里面也重寫onMeasure方法。
- 2、所以我們可以分兩種情況來看待:1、布局類控件;2、一般展示類控件;
- 3、自定義控件過程中,一般情況下我們也需要通過重寫onMeasure來做一些特殊處理。
接下來我們可以從兩個方向去分析onMeasure方法:
1、View.onMeasure
2、布局類的,例如. FrameLayout.onMeasure那么我們先從View.onMeasure吧,畢竟他才是最原始的。
-
View.onMeasure源碼如下,雖然就幾句,但是做的事情可不少哦!
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension( getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
1、調用setMeasuredDimension設置view的大小
2、調用getDefaultSize獲取View的大小,
3、getSuggestedMinimumWidth獲取一個建議最小值
調用順序為onMeasure-> setMeasuredDimension-> getDefaultSize-> getSuggestedMinimumWidth
-
我們逆過來分析一下,首先getSuggestedMinimumWidth這個是什么呢?我們點進源碼看一下:
protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); }
里面代碼很少,判斷是否有背景,沒有的話返回mMinWidth,這個mMinWidth其實就是android:minWidth=""屬性設置的值。也就是假設沒設置有背景的情況下,就以設置minWidth值為準
如果設置有背景,那么就去背景的實際寬度與minWidth中大的一個。
getMinimumWidth()可以理解成背景的bitmap形式下的實際寬度值。
-
然后我們看getDefaultSize這個方法,這是一個靜態工具方法,他返回的是view的大小:
public static int getDefaultSize(int size, int measureSpec) { int result = size; //1、獲得MeasureSpec的mode int specMode = MeasureSpec.getMode(measureSpec); //2、獲得MeasureSpec的specSize int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: //這個我們先不看他 result = size; break; case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: //3、可以看到,最終返回的size就是我們MeasureSpec中測量得到的size result = specSize; break; } return result; }
第3點很重要,你有沒有發現,AT_MOST與EXACTLY模式下,返回的值居然是一樣的,那豈不是wrap_content與match_parent是等效的?不要打我,我可沒騙你哦
那么,我們實際開發中肯定要處理這個情況,所以我們在自定義直接繼承View來實現的控件時,一定要自己處理這兩種情況哦。否則wrap_content屬性是等效于match_parent的哦
-
之后就到我們的setMeasuredDimension方法了,前面說了,setMeasuredDimension是設置view的大小的。我們進去看一下源碼
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { //1、判斷是否使用視覺邊界布局 boolean optical = isLayoutModeOptical(this); //2、判斷view和parentView使用的視覺邊界布局是否一致 if (optical != isLayoutModeOptical(mParent)) { //不一致時要做一些邊界的處理 Insets insets = getOpticalInsets(); int opticalWidth = insets.left + insets.right; int opticalHeight = insets.top + insets.bottom; measuredWidth += optical ? opticalWidth : -opticalWidth; measuredHeight += optical ? opticalHeight : -opticalHeight; } //3、重點來了,經過過濾之后調用了setMeasuredDimensionRaw方法,看來應該是這個方法設置我們的view的大小 setMeasuredDimensionRaw(measuredWidth, measuredHeight); }
-
我們繼續看setMeasuredDimensionRaw方法
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) { //最終將測量好的大小存儲到mMeasuredWidth和mMeasuredHeight上,所以在測量之后我們可以通過調用getMeasuredWidth獲得測量的寬、getMeasuredHeight獲得高 mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight; mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET; }
小結:
- 測量view的順序為measure->onMeasure-> setMeasuredDimension-> setMeasuredDimensionRaw,由setMeasuredDimensionRaw最終保存測量的數據。
- 以上是測量一個view的過程,這樣子我們的view的測量工作就結束了。
-
接下來我們來看下布局類frameLayout是如何測量的,我們同樣看FrameLayout的onMeasure方法
//這里的widthMeasureSpec、heightMeasureSpec //其實就是我們frameLayout可用的widthMeasureSpec 、 //heightMeasureSpec protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //1、獲得frameLayout下childView的個數 int count = getChildCount(); //2、看這里的代碼我們可以根據前面的Measure圖來進行分析,因為只要parent //不是EXACTLY模式,以frameLayout為例,假設frameLayout本身還不是EXACTL模式, // 那么表示他的大小此時還是不確定的,從表得知,此時frameLayout的大小是根據 //childView的最大值來設置的,這樣就很好理解了,也就是childView測量好后還要再 //測量一次,因為此時frameLayout的值已經可以算出來了,對于child為MATCH_PARENT //的,child的大小也就確定了,理解了這里,后面的代碼就很 容易看懂了 final boolean measureMatchParentChildren = MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY || MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY; //3、清理存儲模式為MATCH_PARENT的child的隊列 mMatchParentChildren.clear(); //4、下面三個值最終會用來設置frameLayout的大小 int maxHeight = 0; int maxWidth = 0; int childState = 0; //5、開始便利frameLayout下的所有child for (int i = 0; i < count; i++) { final View child = getChildAt(i); //6、小發現哦,只要mMeasureAllChildren是true,就算child是GONE也會被測量哦, if (mMeasureAllChildren || child.getVisibility() != GONE) { //7、開始測量childView measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); //8、下面代碼是獲取child中的width 和height的最大值,后面用來重新設置frameLayout,有需要的話 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); childState = combineMeasuredStates(childState, child.getMeasuredState()); //9、如果frameLayout不是EXACTLY, if (measureMatchParentChildren) { if (lp.width == LayoutParams.MATCH_PARENT || lp.height == LayoutParams.MATCH_PARENT) { //10、存儲LayoutParams.MATCH_PARENT的child,因為現在還不知道frameLayout大小, //也就無法設置child的大小,后面需重新測量 mMatchParentChildren.add(child); } } } } .... //11、這里開始設置frameLayout的大小 setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT)); //12、frameLayout大小確認了,我們就需要對寬或高為LayoutParams.MATCH_PARENTchild重新測量,設置大小 count = mMatchParentChildren.size(); if (count > 1) { for (int i = 0; i < count; i++) { final View child = mMatchParentChildren.get(i); final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); final int childWidthMeasureSpec; if (lp.width == LayoutParams.MATCH_PARENT) { final int width = Math.max(0, getMeasuredWidth() - getPaddingLeftWithForeground() - getPaddingRightWithForeground() - lp.leftMargin - lp.rightMargin); //13、注意這里,為child是EXACTLY類型的childWidthMeasureSpec, //也就是大小已經測量出來了不需要再測量了 //通過MeasureSpec.makeMeasureSpec生成相應的MeasureSpec childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( width, MeasureSpec.EXACTLY); } else { //14、如果不是,說明此時的child的MeasureSpec是EXACTLY的,直接獲取child的MeasureSpec, childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, getPaddingLeftWithForeground() + getPaddingRightWithForeground() + lp.leftMargin + lp.rightMargin, lp.width); } // 這里是對高做處理,與寬類似 final int childHeightMeasureSpec; if (lp.height == LayoutParams.MATCH_PARENT) { final int height = Math.max(0, getMeasuredHeight() - getPaddingTopWithForeground() - getPaddingBottomWithForeground() - lp.topMargin - lp.bottomMargin); childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( height, MeasureSpec.EXACTLY); } else { childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTopWithForeground() + getPaddingBottomWithForeground() + lp.topMargin + lp.bottomMargin, lp.height); } //最終,再次測量child child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } }
至此,View的三圍已經測出來了,本篇略長,測量在android的死亡三部曲中是第一部,也是里面最復雜、重要的一部,快看下你的三圍是多少吧!
總結:
- View的測量,重點是抓住MeasureSpec在其中體現的作用,MeasureSpec貫穿了View測量的整個過程,明白其的作用,也就明白了View測量的一半知識了。
- View的Layout將在下一章進行分析