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

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

Activity的組成結(jié)構(gòu)粗講

首先,我們來看一下一個Activity包含哪些組件結(jié)構(gòu):

1.png

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

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

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

自定義View的工作流程

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

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

單一View的測量過程簡述

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

2.png

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

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

ViewGroup的測量過程簡述

先看ViewGroup的測量過程:

6.png

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

測量中的超級尺子——MeasureSpec

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

測量模式(specMode)

測量模式分為三種:

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

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

3.png

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

4.png

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

5.png

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

5.png

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

MeasureSpec確定值

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

//從上面我們知道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、當(dāng)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說我要的大小剛好夠放
    //需要展示的內(nèi)容就好,而此時我們并不知道child的內(nèi)容
    //需要多大的地方,暫時先把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);
}

代碼挺長的 總結(jié)一下:

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

單一View的測量過程詳解

2.png

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

performMeasure()

這個圖其實(shí)不是那么嚴(yán)謹(jǐn),因為在加載好系統(tǒng)布局資源后,會觸發(fā)ViewRootImpl的performTraversals()方法,該方法內(nèi)容會開始執(zhí)行測量、布局和繪制的工作,我們來看這個方法的源碼關(guān)鍵部分:

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、執(zhí)行布局方法
      performLayout(lp, desiredWindowWidth, desiredWindowHeight);
      ...
  }
  if (!cancelDraw && !newSurface) {
   ...
    //5、開始繪制了哦
          performDraw();
      }
  } 
  ...
}

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

private void performMeasure(int childWidthMeasureSpec, 
  int childHeightMeasureSpec) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
  //1、mView其實(shí)就是我們的頂層DecorView,從DecorView開始測量
  mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
  Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}

measure()

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

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

onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}

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

View.onMeasure()

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

源碼如下:

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

這里我們逆向思維,從最內(nèi)層開始說起:

getSuggestedMinimumWidth() / getSuggestedMinimumHeight()

源碼:

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

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

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

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

getDefaultSize()

這個方法其實(shí)是一個靜態(tài)工具方法:

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

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

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

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

setMeasureDimension

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

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、重點(diǎn)來了,經(jīng)過過濾之后調(diào)用了setMeasuredDimensionRaw方法,看來應(yīng)該是這個方法設(shè)置我們的view的大小
  setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}

繼續(xù)看setMeasureDimensionRaw方法:

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

  mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

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

ViewGroup的測量過程詳解

6.png

根據(jù)上圖,我們還是一個步驟一個步驟的進(jìn)行分析:

measureChildren()

其作用就是遍歷子View并調(diào)用measureChild()進(jìn)行下一步測量

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
//參數(shù)說明:父視圖的測量規(guī)格(MeasureSpec)

        final int size = mChildrenCount;
        final View[] children = mChildren;

        //遍歷所有的子view
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
        //如果View的狀態(tài)不是GONE就調(diào)用measureChild()去進(jìn)行下一步的測量
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

measureChild()

其作用就是計算單個子View的MeasureSpec,調(diào)用子View的measure進(jìn)行每個子View最后的寬、高的測量

protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {

        // 獲取子視圖的布局參數(shù)
        final LayoutParams lp = child.getLayoutParams();

        // 調(diào)用getChildMeasureSpec(),根據(jù)父視圖的MeasureSpec & 布局參數(shù)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(),進(jìn)行最后的測量
        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) {

        // 調(diào)用onMeasure()計算視圖大小
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    } else {
        ...

}

XXXLayout.onMeasure()

仔細(xì)看過文章前面的朋友應(yīng)該記得我在4.2 measure() 中提到過,ViewGroup的onMeasure和View的onMeasure是不同的,究其原因其實(shí)是因為ViewGroup是一個抽象類,所以即便它繼承了View也不用必須實(shí)現(xiàn)View中的onMeasure方法,而它的子類不具備通用的布局特性,這導(dǎo)致他們的子View的測量方法各不相同,因此,ViewGroup無法對onMeasure()做統(tǒng)一的實(shí)現(xiàn)。
這里以FrameLayout為例,看看它的onMeasure是如何實(shí)現(xiàn)的:

//這里的widthMeasureSpec、heightMeasureSpec
//其實(shí)就是我們frameLayout可用的widthMeasureSpec 、
//heightMeasureSpec
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  //1、獲得frameLayout下childView的個數(shù)
  int count = getChildCount();
//2、看這里的代碼我們可以根據(jù)前面的Measure圖來進(jìn)行分析,因為只要parent
//不是EXACTLY模式,以frameLayout為例,假設(shè)frameLayout本身還不是EXACTL模式,
 // 那么表示他的大小此時還是不確定的,從表得知,此時frameLayout的大小是根據(jù)
 //childView的最大值來設(shè)置的,這樣就很好理解了,也就是childView測量好后還要再
//測量一次,因為此時frameLayout的值已經(jīng)可以算出來了,對于child為MATCH_PARENT
//的,child的大小也就確定了,理解了這里,后面的代碼就很 容易看懂了
  final boolean measureMatchParentChildren =
          MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
          MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
   //3、清理存儲模式為MATCH_PARENT的child的隊列
  mMatchParentChildren.clear();
  //4、下面三個值最終會用來設(shè)置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、小發(fā)現(xiàn)哦,只要mMeasureAllChildren是true,就算child是GONE也會被測量哦,
      if (mMeasureAllChildren || child.getVisibility() != GONE) {
          //7、開始測量childView 
          measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);

          //8、下面代碼是獲取child中的width 和height的最大值,后面用來重新設(shè)置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,因為現(xiàn)在還不知道frameLayout大小,
//也就無法設(shè)置child的大小,后面需重新測量
                  mMatchParentChildren.add(child);
              }
          }
      }
  }

    ....
  //11、這里開始設(shè)置frameLayout的大小
  setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
          resolveSizeAndState(maxHeight, heightMeasureSpec,
                  childState << MEASURED_HEIGHT_STATE_SHIFT));

//12、frameLayout大小確認(rèn)了,我們就需要對寬或高為LayoutParams.MATCH_PARENTchild重新測量,設(shè)置大小
  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,
  //也就是大小已經(jīng)測量出來了不需要再測量了
  //通過MeasureSpec.makeMeasureSpec生成相應(yīng)的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);
      }
  }
}

總結(jié)

通過本文的講解,不知道你對自定義View和ViewGroup的測量過程有沒有一個清楚的認(rèn)識呢?其實(shí)我們在學(xué)習(xí)的過程中,不必要完全了解源碼中的每個細(xì)節(jié),而是應(yīng)當(dāng)了解源碼的邏輯順序并將其大致過程記錄下來,這樣不論是在工作還是面試的過程中,我們根據(jù)大致的邏輯流程來分析問題,從而達(dá)到學(xué)以致用的目的。
最后我付一個Android自定義View的面試題答案,我認(rèn)為講的還是比較不錯的,希望大家看完后再結(jié)合之前文章中提到的內(nèi)容進(jìn)行記憶整理。

  1. ViewRootImpl 會調(diào)用 performTraversals(), 其內(nèi)部會調(diào)用performMeasure()、performLayout、performDraw()。
  1. performMeasure() 會調(diào)用最外層的 ViewGroup的measure()-->onMeasure(), ViewGroup 的 onMeasure() 是抽象方法,但其提供了measureChildren(),這之中會遍歷子View然后循環(huán)調(diào)用measureChild() 這之中會用 getChildMeasureSpec()+父View的MeasureSpec+子View的LayoutParam一起獲取本View的MeasureSpec,然后調(diào)用子View的measure()到View的onMeasure()-->setMeasureDimension(getDefaultSize(),getDefaultSize()),getDefaultSize()默認(rèn)返回measureSpec的測量數(shù)值,所以繼承View進(jìn)行自定義的wrap_content需要重寫。
    3 .performLayout()會調(diào)用最外層的ViewGroup的layout(l,t,r,b),本View在其中使用setFrame()設(shè)置本View的四個頂點(diǎn)位置。在onLayout(抽象方法)中確定子View的位置,如LinearLayout會遍歷子View,循環(huán)調(diào)用setChildFrame()-->子View.layout()。
  2. performDraw() 會調(diào)用最外層 ViewGroup的draw():其中會先后調(diào)用background.draw()(繪制背景)、onDraw()(繪制自己)、dispatchDraw()(繪制子View)、onDrawScrollBars()(繪制裝飾)。
  3. MeasureSpec由2位SpecMode(UNSPECIFIED、EXACTLY(對應(yīng)精確值和match_parent)、AT_MOST(對應(yīng)warp_content))和30位SpecSize組成一個int,DecorView的MeasureSpec由窗口大小和其LayoutParams決定,其他View由父View的MeasureSpec和本View的LayoutParams決定。ViewGroup中有g(shù)etChildMeasureSpec()來獲取子View的MeasureSpec。
  4. 三種方式獲取measure()后的寬高:
    1.Activity#onWindowFocusChange()中調(diào)用獲取
    2.view.post(Runnable)將獲取的代碼投遞到消息隊列的尾部。
    3.ViewTreeObservable.
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容