Android之自定義View的死亡三部曲之(Measure)

文章獨家授權公眾號:碼個蛋
更多分享: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將在下一章進行分析
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容