Android自定義View的測量過程詳解


在Android開發中,自定義View可以說是從業人員從初級到中級的一個標志性過渡,不論你是老鳥還是新人,不論你是工作還是面試,自定義View都如同一個守關的BOSS一樣,無法回避,《心花路放》里張儷( 那個大長腿)說過,“遇到問題,正面面對。”,好吧,躲不起我還打不過么我?擼起袖子,干!

Activity的組成結構粗講

首先,我們來看一下一個Activity包含哪些組件結構:

1.png

可以看到,每一個Activity都包含了PhoneWindow,PhoneWindow又包含了一個DecorView,而DecorView中包含了TitleView和一個ContentView,TitleView就是我們開發過程中需要設置的ActionBar和StatusBar相關,而界面的主體結構都在這個ContentView中設置了,所以我們每次給Activity設置布局文件的必須調用
setContentVIew(int layoutResID)方法來設置。
而ContentView中就包含了一個或多個ViewGroup和View,關於ViewGroup和View,整個ContentView的視圖結構:最頂層是ViewGroup,ViewGroup下可能有多個ViewGroup和View,就好像布局文件來說,我們寫布局文件的時候,最外層一般都是一個ViewGroup(LinearLayout或者是RelativeLayout等),之后這個ViewGroup內部可能還是一個ViewGroup(LinearLayout或者是RelativeLayout等)或者直接放置了一個或者多個View(如TextView或者是Button等)。

這里需要說明一下,這里所說的ViewGroup包含很多View是說的View的視圖結構,而從View的架構來說,所有的ViewGroup其本質還是View的子類,這里要面試的朋友一定要注意了,如果問到你View的架構,千萬不要和View的視圖結構弄混,否則你來一句View繼承自ViewGroup那你這次面試基本上就會以失敗告終了。

這里就不細講整個布局是如何誕生的了,關鍵是大家要對Activity有一個全局的認識,從而知道我們自定義View在Activity中的具體定位。

自定義View的工作流程

自定義View的工作流程主要指的是Mesusre,Layout和Draw,Measure用來測量View的寬高,Layout用來確定View的位置,Draw則用來繪制View。

雖然話是這么說,但是就好像一個類要運行一定要有一個Main方法一樣,一個View的繪制也一定要有一個開始調用的地方才是,那么這個開始調用的方法就是ViewRootImpl類的performTraversales()方法,在這個方法中,這個方法會通過判斷來分析這個View是否要執行相關的繪制流程。

單一View的測量過程簡述

View的Measure過程根據View的類型分為兩種情況,一種是單一View的測量過程,還有一種是ViewGroup的測量過程,這兩種測量過程是不同的。
首先先看單一View的測量過程:

2.png

整個的測量過程基本如上圖所示,用文字描述一遍就是:
ViewRootImpl調用performMeasure方法后該方法中的DecorView又調用了measure方法,而DecorView的本質其實是集成自FrameLayout,那么本質就是一個ViewGroup,但是在FrameLayout和ViewGroup的源碼中都沒發現measure方法, 這就用到剛才的知識點了,ViewGroup本身也是View的子類,我們就去View的源碼中查看一下,果然,measure方法赫然其中,在measure方法中,View又調用了其自身的方法onMeasure,而起自身的onMeasure方法中又調用setMeasureDimension方法,該方法有兩個參數,這兩個參數分別代表要設置的寬和高,而這兩個參數都是是通過getDefaultSize方法來計算的,而getDefaultSize方法中有一個參數是根據getSuggestedMinimumWidth或getSuggestedMinimumHeight獲得的。

之所以沒貼源碼就直接先說測量過程,是希望大家可以先了解一下View的加載過程,不管是在工作中遇到了問題,或者是面試的時候被人提問,根據這個過程你可以自己分析問題的切入點或者是給面試官描述一下大體的流程,告訴他你已經對View的測量過程有了一個大體的認知了,最起碼不會讓人家覺得你完全沒了解這里的知識。

ViewGroup的測量過程簡述

先看ViewGroup的測量過程:

6.png

整個測量過程基本如上圖所示,其原理就是:
通過遍歷所有的子View進行子View的測量,然后將所有子View的尺寸進行合並,最終得到ViewGroup父視圖的測量值。

測量中的超級尺子——MeasureSpec

在看整個view的加載過程的源碼前,我們先來了解一下View中的一個靜態內部類MeasureSpec,有人叫它測量規格,我更喜歡把它描述成為測量過程中必不可少的工具——尺子。
這個尺子有兩種用法,橫着用就叫做widthMeasureSpec,用來測量寬度,豎着用就叫做heightMeasureSpec,用來測量高度的,不管你的自定義View是什么千奇百怪的形狀,他都是要放在一個矩形中進行包裹展示的,那么為什么會有這兩個測量方式也就不難理解了。
這個尺子有兩個重要的功能,第一個功能自然是測量值了(Size),第二個功能是測量的模式(Mode),這兩個參數通過二進制將其打包成一個int(32位)值來減少對內存的分配,其高2位(31,32位)存放的是測量模式,而低30位則存儲的是其測量值。

測量模式(specMode)

測量模式分為三種:

  • UNSPECIFIED模式:本質就是不限制模式,父視圖不對子View進行任何約束,View想要多大要多大,想要多長要多長,這個在我們寫自定義View中的時候非常少見,一般都是系統內部在設置ListView或者是ScrollView的時候才會用到。
  • EXACTLY模式:該模式其實對應的場景就是match_parent或者是一個具體的數據(50dp或80px),父視圖為子View指定一個確切的大小,無論子View的值設置多大,都不能超出父視圖的范圍。
  • AT_MOST模式:這個模式對應的場景就是wrap_content,其內容就是父視圖給子View設置一個最大尺寸,子View只要不超過這個尺寸即可。

看完這三個模式,大家可能會有一個疑問,AT_MOST模式不是和EXACTLY模式一樣了嗎,都是給個最大值來限制View的范圍?其實不一樣,EXACTLY模式是一個固定的模式,也就是說它是沒得商量的,你只能按照這個模式既有的規律來執行而AT_MOST模式是可以根據開發者自己的需求來定制的,我們寫自定義View的時候所寫的測量其實也就是在寫這個模式的測量邏輯,他是根據子控件來靈活的測量尺寸的。
可能你還是懵逼,那請你跟我一起打開IDE,做這么一個簡單的測試:
請先寫一個TestView,繼承View:

3.png

不用重寫任何父類的方法,只要隨便弄兩個構造方法即可。
然后在布局文件中寫入如下代碼:

4.png

這代碼太簡單了,你想象中的運行效果肯定是這個樣子的:

5.png

沒錯,實際效果和你想的一樣,但是如果我們將match_parent改成wrap_content會怎樣呢?
好了不用想了,我們來看實際效果:

5.png

效果沒有變化!是的,通過這個測試我們知道一個原理,如果不重寫onMeasure的AT_MOST模式的測量邏輯,那么match_parent和wrap_content的效果是一樣的。
請帶着這個疑問繼續看下去。

MeasureSpec確定值

子View的MeasureSpec值是根據子View的布局參數(LayoutParams)和父容器的MeasureSpec至計算而來的,其具體邏輯封裝在了getChildMeasureSpec()方法中,現在來分析一下這個方法:

//從上面我們知道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); } 

代碼挺長的 總結一下:

  • 當子View采用具體數值(dp / px)時
    無論父容器的測量模式是什么,子View的測量模式都是EXACTLY且大小等於設置的具體數值;
  • 當子View采用match_parent時
    • 子View的測量模式與父容器的測量模式一致
    • 若測量模式為EXACTLY,則子View的大小為父容器的剩余空間;若測量模式為AT_MOST,則子View的大小不超過父容器的剩余空間
  • 當子View采用wrap_parent時
    如果父容器測量模式為UNSPECIFIED,子View也為UNSPECIFIED,否則子View為AT_MOST且大小不超過父容器的剩余空間。

單一View的測量過程詳解

2.png

還是回顧一下剛才這張圖,我們一個步驟一個步驟的來看:

performMeasure()

這個圖其實不是那么嚴謹,因為在加載好系統布局資源后,會觸發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(); } } ... } 

可以看到,整個方法內部其實就是做了一些基礎的判斷后,再順序的調用測量、布局和繪制的相關方法,從而完成自定義View的整個工作流程。
現在看performTraversals方法的第一個注釋和第二個注釋處,使用的是getRootMeasureSpec方法來獲取子View的MeasureSpec,根據一開始我們了解的知識,整個Activity的頂層View其實就是一個DecorView,所以這里獲取的其實是DeorView的MeasureSpec,然后將其傳入performMeasure方法中去開始測量,現在看看PerformMeasure方法:

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); } } 

measure()

根據剛才描述的單一View的加載過程,我們在View的源碼中找到了measure方法:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) { ... onMeasure(widthMeasureSpec, heightMeasureSpec); ... } 

這里有一個細節要注意以下
首先,ViewGroup中並沒有重寫View的onMeasure方法,但是我們的四大布局FrameLayout LinearLayout RelativeLayout AbsoluteLayout都是通過繼承ViewGroup來實現的,而且里面也重寫onMeasure方法,所以onMeasure方法是分為兩個方向的,單一View的測量是View.onMeasure而ViewGroup的測量則是XXLayout.onMeasure,這兩種onMeasure方法的實現是不同的。

View.onMeasure()

其實這個方法的關鍵作用有三個:
第一,獲取一個建議最小值。
第二,調用getDefaultSize方法定義對View尺寸的測量邏輯。
第三,調用setMeasureDimension()儲存測量后的View寬/高。

源碼如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension( getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); } 

這里我們逆向思維,從最內層開始說起:

getSuggestedMinimumWidth() / getSuggestedMinimumHeight()

源碼:

protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); } 

代碼很簡單,是一個三目運算,意思就是當前View是否有背景?沒有就返回android:minWidth設置的值:有就返回android:minWidth和mBackground.getMinimumWidth()中較大的那個值。
那么,mBackground.getMinimumWidth()方法有是什么呢?

public int getMinimumWidth() { final int intrinsicWidth = getIntrinsicWidth(); //返回背景圖Drawable的原始寬度 return intrinsicWidth > 0 ? intrinsicWidth :0 ; } 

該處邏輯一樣是三目運算,getIntrinsicWidth()獲取的是背景圖的原始寬度,背景圖是BitmapDrawable則有原始寬度,在沒有原始寬度的情況下則返回0。

getDefaultSize()

這個方法其實是一個靜態工具方法:

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; } 

大家應該注意到了,不論測量模式是AT_MOST還是EXACTLY,最終返回的Size是一樣的,現在我們再回想之前的問題,為什么我們設置wrap_content時match_parent效果一樣呢?

原因其實就在上面的代碼中,因為我們並沒有重寫子View采用wrap_content時的測量方法,所以自定義View不論父視圖的布局模式是EXACTLY(非固定值模式)還是AT_MOST也不論子視圖的布局模式是match_parent還是wrap_content(只要不是具體的值),系統都會將子View的大小(寬或高)都臨時或永久設置成Size(剩余的可用大小),所以在這種情況下,這兩種模式所形成的效果就會一樣了。

所以在實際開發中,我們必須要處理wrap_content這個情況,否則wrap_content和match_parent的效果就是一樣的了。

setMeasureDimension

該方法是儲存測量后的View的寬和高的,在自定義View的時候,我們自己重寫的onMeasure方法最后一定要調用這個方法,否則會報錯。

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); } 

繼續看setMeasureDimensionRaw方法:

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) { //最終將測量好的大小存儲到mMeasuredWidth和mMeasuredHeight上,所以在測量之后 //我們可以通過調用getMeasuredWidth獲得測量的寬、getMeasuredHeight獲得高 mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight; mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET; } 

以上就是一個單一View的測量過程了,其順序為:
performTraversals->performMeasure->measure->onMeasure-> setMeasuredDimension-> setMeasuredDimensionRaw,由setMeasuredDimensionRaw最終保存測量的數據。

ViewGroup的測量過程詳解

6.png

根據上圖,我們還是一個步驟一個步驟的進行分析:

measureChildren()

其作用就是遍歷子View並調用measureChild()進行下一步測量

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) { //參數說明:父視圖的測量規格(MeasureSpec) final int size = mChildrenCount; final View[] children = mChildren; //遍歷所有的子view for (int i = 0; i < size; ++i) { final View child = children[i]; //如果View的狀態不是GONE就調用measureChild()去進行下一步的測量 if ((child.mViewFlags & VISIBILITY_MASK) != GONE) { measureChild(child, widthMeasureSpec, heightMeasureSpec); } } } 

measureChild()

其作用就是計算單個子View的MeasureSpec,調用子View的measure進行每個子View最后的寬、高的測量

protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { // 獲取子視圖的布局參數 final LayoutParams lp = child.getLayoutParams(); // 調用getChildMeasureSpec(),根據父視圖的MeasureSpec & 布局參數LayoutParams,計算單個子View的MeasureSpec // getChildMeasureSpec()請回看上面的解析 // 獲取 ChildView 的 widthMeasureSpec final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width); // 獲取 ChildView 的 heightMeasureSpec final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height); // 將計算好的子View的MeasureSpec值傳入measure(),進行最后的測量 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } 

measure()

與單一View的measure一致

public final void measure(int widthMeasureSpec, int heightMeasureSpec) { ... int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 : mMeasureCache.indexOfKey(key); if (cacheIndex < 0 || sIgnoreMeasureCache) { // 調用onMeasure()計算視圖大小 onMeasure(widthMeasureSpec, heightMeasureSpec); mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } else { ... } 

XXXLayout.onMeasure()

仔細看過文章前面的朋友應該記得我在4.2 measure() 中提到過,ViewGroup的onMeasure和View的onMeasure是不同的,究其原因其實是因為ViewGroup是一個抽象類,所以即便它繼承了View也不用必須實現View中的onMeasure方法,而它的子類不具備通用的布局特性,這導致他們的子View的測量方法各不相同,因此,ViewGroup無法對onMeasure()做統一的實現。
這里以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和ViewGroup的測量過程有沒有一個清楚的認識呢?其實我們在學習的過程中,不必要完全了解源碼中的每個細節,而是應當了解源碼的邏輯順序並將其大致過程記錄下來,這樣不論是在工作還是面試的過程中,我們根據大致的邏輯流程來分析問題,從而達到學以致用的目的。
最后我付一個Android自定義View的面試題答案,我認為講的還是比較不錯的,希望大家看完后再結合之前文章中提到的內容進行記憶整理。

  1. ViewRootImpl 會調用 performTraversals(), 其內部會調用performMeasure()、performLayout、performDraw()。
  1. performMeasure() 會調用最外層的 ViewGroup的measure()-->onMeasure(), ViewGroup 的 onMeasure() 是抽象方法,但其提供了measureChildren(),這之中會遍歷子View然后循環調用measureChild() 這之中會用 getChildMeasureSpec()+父View的MeasureSpec+子View的LayoutParam一起獲取本View的MeasureSpec,然后調用子View的measure()到View的onMeasure()-->setMeasureDimension(getDefaultSize(),getDefaultSize()),getDefaultSize()默認返回measureSpec的測量數值,所以繼承View進行自定義的wrap_content需要重寫。
    3 .performLayout()會調用最外層的ViewGroup的layout(l,t,r,b),本View在其中使用setFrame()設置本View的四個頂點位置。在onLayout(抽象方法)中確定子View的位置,如LinearLayout會遍歷子View,循環調用setChildFrame()-->子View.layout()。
  2. performDraw() 會調用最外層 ViewGroup的draw():其中會先后調用background.draw()(繪制背景)、onDraw()(繪制自己)、dispatchDraw()(繪制子View)、onDrawScrollBars()(繪制裝飾)。
  3. MeasureSpec由2位SpecMode(UNSPECIFIED、EXACTLY(對應精確值和match_parent)、AT_MOST(對應warp_content))和30位SpecSize組成一個int,DecorView的MeasureSpec由窗口大小和其LayoutParams決定,其他View由父View的MeasureSpec和本View的LayoutParams決定。ViewGroup中有getChildMeasureSpec()來獲取子View的MeasureSpec。
  4. 三種方式獲取measure()后的寬高:
    1.Activity#onWindowFocusChange()中調用獲取
    2.view.post(Runnable)將獲取的代碼投遞到消息隊列的尾部。
    3.ViewTreeObservable.


作者:賈亦真亦賈
鏈接:https://www.jianshu.com/p/1db39d6ee4be
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並注明出處。


免責聲明!

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



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