自定義View Android最易懂的測量與布局

測量 View 就是測量一個矩形


透過另一個視角來觀察,所有的 Widget,我們使用的小控件都是Widget。如果TextView和Buttton等

因此,自定義 View 的第一步,我們要在心里默念 – 我們現(xiàn)在要確定一個矩形了!

既然是矩形,那么它肯定有明確的寬高和位置坐標,寬高是在測量階段得出。然后在布局階段,確定好位置信息對矩形進行布局,之后的視覺效果就交給繪制流程了,我們是最好的畫家。

布局繪畫涉及兩個過程:測量過程和布局過程。測量過程通過measure方法實現(xiàn),是View樹自頂向下的遍歷,每個View在循環(huán)過程中將尺寸細節(jié)往下傳遞,當(dāng)測量過程完成之后,所有的View都存儲了自己的尺寸。第二個過程則是通過方法layout來實現(xiàn)的,也是自頂向下的。在這個過程中,每個父View負責(zé)通過計算好的尺寸放置它的子View。

好了,我們知道了測量的就是長和寬,我們的目的也就是長和寬。

View 設(shè)置尺寸的基本方法

接下來的過程,我將會用一系列比較細致的實驗來說明問題,我們先看看在 Android 中使用 Widget 的時候,怎么定義大小。比如我們要在屏幕上使用一個 Button。

<Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="test"/>

這樣屏幕上就出現(xiàn)了一個按鈕。

我們再把寬高固定。

<Button
        android:layout_width="200dp"
        android:layout_height="50dp"
        android:text="test"/>

再換一種情況,將按鈕的寬度由父容器決定

<Button
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:text="test"/>

上面就是我們?nèi)粘i_發(fā)中使用的步驟,通過 layout_width 和 layout_height 屬性來設(shè)置一個 View 的大小。而在 xml 中,這兩個屬性有 3 種取值可能。

android:layout_height="wrap_content"   //View 本身的內(nèi)容決定高度
android:layout_height="match_parent"   //與父視圖等高  
android:layout_height="fill_parent"    //與父視圖等高  
android:layout_height="100dip"         //精確設(shè)置高度值為 100dip  

我們再進一步,現(xiàn)在給 Button 找一個父容器進行觀察。父容器背景由特定顏色標識。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="#ff0000">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="test" />

</RelativeLayout>

可以看到 RelativeLayout 包裹著 Button。我們再換一種情況。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="150dp"
    android:layout_height="wrap_content"
    android:background="#ff0000">

    <Button
        android:layout_width="120dp"
        android:layout_height="wrap_content"
        android:text="test" />
</RelativeLayout>

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="150dp"
    android:layout_height="wrap_content"
    android:background="#ff0000">

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="test" />
    
</RelativeLayout>

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="150dp"
    android:layout_height="wrap_content"
    android:background="#ff0000">

    <Button
        android:layout_width="1000dp"
        android:layout_height="wrap_content"
        android:text="test" />

</RelativeLayout>

似乎發(fā)生了不怎么愉快的事情,Button 想要的長度是 1000 dp,而 RelativeLayout 最終給予的卻仍舊是在自己的有限范圍參數(shù)內(nèi)。就好比山水莊園向光明開發(fā)區(qū)政府要地 1 萬畝,政府說沒有這么多,最多 2000 畝。

Button 是一個 View,RelativeLayout 是一個 ViewGroup。那么對于一個 View 而言,它相當(dāng)于山水莊園,而 ViewGroup 類似于政府的角色。View 蕓蕓眾生,它們的多姿多彩構(gòu)成了美麗的 Android 世界,ViewGroup 卻有自己的規(guī)劃,所謂規(guī)劃也就是以大局為重嘛,盡可能協(xié)調(diào)管轄區(qū)域內(nèi)各個成員的位置關(guān)系。

山水莊園拿地蓋樓需要同政府協(xié)商溝通,自定義一個 View 也需要同它所處的 ViewGroup 進行協(xié)商。

那么,它們的協(xié)議是什么?

View 和 ViewGroup 之間的測量協(xié)議 MeasureSpec

我們自定義一個 View,onMeasure()是一個關(guān)鍵方法。也是本文重點研究內(nèi)容。測量自己的大小,為正式布局提供建議。(注意,只是建議,至于用不用,要看onLayout);

public class TestView extends View {
    public TestView(Context context) {
        super(context);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}

onMeasure() 中有兩個參數(shù) widthMeasureSpec、heightMeasureSpec。它們是什么?看起來和寬高有關(guān)。

它們確實和寬高有關(guān),了解它們需要從一個類說起。MeasureSpec。

MeasureSpec

MeasureSpec 是 View.java 中一個靜態(tài)類

/**
  * MeasureSpec類的源碼分析
  **/
    public class MeasureSpec {

        // 進位大小 = 2的30次方
        // int的大小為32位,所以進位30位 = 使用int的32和31位做標志位
        private static final int MODE_SHIFT = 30;  
          
        // 運算遮罩:0x3為16進制,10進制為3,二進制為11
        // 3向左進位30 = 11 00000000000(11后跟30個0)  
        // 作用:用1標注需要的值,0標注不要的值。因1與任何數(shù)做與運算都得任何數(shù)、0與任何數(shù)做與運算都得0
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;  
  
        // UNSPECIFIED的模式設(shè)置:0向左進位30 = 00后跟30個0,即00 00000000000
        // 通過高2位
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;  
        
        // EXACTLY的模式設(shè)置:1向左進位30 = 01后跟30個0 ,即01 00000000000
        public static final int EXACTLY = 1 << MODE_SHIFT;  

        // AT_MOST的模式設(shè)置:2向左進位30 = 10后跟30個0,即10 00000000000
        public static final int AT_MOST = 2 << MODE_SHIFT;  
  
        /**
          * makeMeasureSpec()方法
          * 作用:根據(jù)提供的size和mode得到一個詳細的測量結(jié)果嗎,即measureSpec
          **/ 
            public static int makeMeasureSpec(int size, int mode) {  
            
               return (size & ~MODE_MASK) | (mode & MODE_MASK);
     
               // 設(shè)計目的:使用一個32位的二進制數(shù),其中:第32和第31位代表測量模式(mode)、后30位代表測量大?。╯ize)
            }  
      
        /**
          * getMode()方法
          * 作用:通過measureSpec獲得測量模式(mode)
          **/    

            public static int getMode(int measureSpec) {  
             
                return (measureSpec & MODE_MASK);  
                // 即:測量模式(mode) = measureSpec & MODE_MASK;  
                // MODE_MASK = 運算遮罩 = 11 00000000000(11后跟30個0)
                //原理:保留measureSpec的高2位(即測量模式)、使用0替換后30位
                // 例如10 00..00100 & 11 00..00(11后跟30個0) = 10 00..00(AT_MOST),這樣就得到了mode的值

            }  
        /**
          * getSize方法
          * 作用:通過measureSpec獲得測量大小size
          **/       
            public static int getSize(int measureSpec) {  
             
                return (measureSpec & ~MODE_MASK);  
                // size = measureSpec & ~MODE_MASK;  
               // 原理類似上面,即 將MODE_MASK取反,也就是變成了00 111111(00后跟30個1),將32,31替換成0也就是去掉mode,保留后30位的size  
            } 

    }  

MeasureSpec 代表測量規(guī)則,而它的手段則是用一個 int 數(shù)值來實現(xiàn)。我們知道一個 int 數(shù)值有 32 bit。MeasureSpec 將它的高 2 位用來代表測量模式 Mode,低 30 位用來代表數(shù)值大小 Size。

  • wrap_content-> MeasureSpec.AT_MOST
  • match_parent -> MeasureSpec.EXACTLY
  • 具體值 -> MeasureSpec.EXACTLY

實際使用

/**
  * MeasureSpec類的具體使用
  **/

    // 1. 獲取測量模式(Mode)
    int specMode = MeasureSpec.getMode(measureSpec)

    // 2. 獲取測量大?。⊿ize)
    int specSize = MeasureSpec.getSize(measureSpec)

    // 3. 通過Mode 和 Size 生成新的SpecMode
    int measureSpec=MeasureSpec.makeMeasureSpec(size, mode);

上面講了那么久MeasureSpec,那么MeasureSpec值到底是如何計算得來?
結(jié)論:子View的MeasureSpec值根據(jù)子View的布局參數(shù)(LayoutParams)和父容器的MeasureSpec值計算得來的,具體計算邏輯封裝在getChildMeasureSpec()里。如下圖:


  • 子view的大小由父view的MeasureSpec值 和 子view的LayoutParams屬性 共同決定

下面,我們來看getChildMeasureSpec()的源碼分析:

/**
  * 源碼分析:getChildMeasureSpec()
  * 作用:根據(jù)父視圖的MeasureSpec & 布局參數(shù)LayoutParams,計算單個子View的MeasureSpec
  * 注:子view的大小由父view的MeasureSpec值 和 子view的LayoutParams屬性 共同決定
  **/

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {  

         //參數(shù)說明
         * @param spec 父view的詳細測量值(MeasureSpec) 
         * @param padding view當(dāng)前尺寸的的內(nèi)邊距和外邊距(padding,margin) 
         * @param childDimension 子視圖的布局參數(shù)(寬/高)

            //父view的測量模式
            int specMode = MeasureSpec.getMode(spec);     

            //父view的大小
            int specSize = MeasureSpec.getSize(spec);     
          
            //通過父view計算出的子view = 父大小-邊距(父要求的大小,但子view不一定用這個值)   
            int size = Math.max(0, specSize - padding);  
          
            //子view想要的實際大小和模式(需要計算)  
            int resultSize = 0;  
            int resultMode = 0;  
          
            //通過父view的MeasureSpec和子view的LayoutParams確定子view的大小  


            // 當(dāng)父View的模式為EXACITY時,父view強加給子View確切的值
           //一般是父View設(shè)置為match_parent或者固定值的ViewGroup 
            switch (specMode) {  
            case MeasureSpec.EXACTLY:  
                // 當(dāng)子View的LayoutParams>0,即有確切的值  
                if (childDimension >= 0) {  
                    //子View大小為子自身所賦的值,模式大小為EXACTLY  
                    resultSize = childDimension;  
                    resultMode = MeasureSpec.EXACTLY;  

                // 當(dāng)子View的LayoutParams為MATCH_PARENT時(-1)  
                } else if (childDimension == LayoutParams.MATCH_PARENT) {  
                    //子view大小為父view大小,模式為EXACTLY  
                    resultSize = size;  
                    resultMode = MeasureSpec.EXACTLY;  

                // 當(dāng)子view的LayoutParams為WRAP_CONTENT時(-2)      
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
                    //子view決定自己的大小,但最大不能超過父view,模式為AT_MOST  
                    resultSize = size;  
                    resultMode = MeasureSpec.AT_MOST;  
                }  
                break;  
          
            // 當(dāng)父View的模式為AT_MOST時,父view強加給子View一個最大的值。(一般是父view設(shè)置為wrap_content)  
            case MeasureSpec.AT_MOST:  
                // 道理同上  
                if (childDimension >= 0) {  
                    resultSize = childDimension;  
                    resultMode = MeasureSpec.EXACTLY;  
                } else if (childDimension == LayoutParams.MATCH_PARENT) {  
                    resultSize = size;  
                    resultMode = MeasureSpec.AT_MOST;  
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
                    resultSize = size;  
                    resultMode = MeasureSpec.AT_MOST;  
                }  
                break;  
          
            // 當(dāng)父View的模式為UNSPECIFIED時,父容器不對View有任何限制,要多大給多大
            // 多見于ListView、GridView  
            case MeasureSpec.UNSPECIFIED:  
                if (childDimension >= 0) {  
                    // 子view大小為子自身所賦的值  
                    resultSize = childDimension;  
                    resultMode = MeasureSpec.EXACTLY;  
                } else if (childDimension == LayoutParams.MATCH_PARENT) {  
                    // 因為父View為UNSPECIFIED,所以MATCH_PARENT的話子類大小為0  
                    resultSize = 0;  
                    resultMode = MeasureSpec.UNSPECIFIED;  
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
                    // 因為父view為UNSPECIFIED,所以WRAP_CONTENT的話子類大小為0  
                    resultSize = 0;  
                    resultMode = MeasureSpec.UNSPECIFIED;  
                }  
                break;  
            }  
            return MeasureSpec.makeMeasureSpec(resultSize, resultMode);  
        }  

MeasureSpec.UNSPECIFIED

子元素告訴父容器它的寬高想要多大就要多大,你不要限制我(自己的事情自己做主,沒有任何限制)。一般開發(fā)者幾乎不需要處理這種情況,在 ScrollView 或者是 AdapterView 中都會處理這樣的情況。所以我們可以忽視它。本文中的示例,基本上會跳過它。

MeasureSpec.EXACTLY

此模式說明可以給子元素一個精確的數(shù)值

MeasureSpec.AT_MOST

當(dāng)一個 View 的 layout_width 或者 layout_height 的取值為 wrap_content 時,它的測量模式就是 MeasureSpec.AT_MOST。
此模式下,子 View 希望它的寬或者高由自己決定。ViewGroup 當(dāng)然要尊重它的要求,但是也有個前提,那就是子視圖不能超過ViewGroup 提供的最大值,也就是它期望寬高不能超過父類提供的建議寬高。(自己的事情只能在一個范圍內(nèi)做主)

了解上面的測量模式后,我們就要動手編寫實例來驗證一些想法了。

自定義 View

我的目標是定義一個文本框,中間顯示黑色文字,背景色為綠色。

我們可以輕松地進行編碼。首先,我們定義好它需要的屬性,然后編寫它的 java 代碼。
attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
   <declare-styleable name="TestView">
        <attr name="android:text" format="string" />
        <attr name="android:textSize" format="dimension"/>
    </declare-styleable>
</resources>

TestView.java

public class TestView extends View {

    private  int mTextSize;
    private TextPaint mPaint;
    private String mText;

    public TestView(Context context) {
        this(context,null);
    }

    public TestView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public TestView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.TestView);
        mText = ta.getString(R.styleable.TestView_android_text);
        mTextSize = ta.getDimensionPixelSize(R.styleable.TestView_android_textSize,24);
        ta.recycle();

        mPaint = new TextPaint();
        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.BLACK);
        mPaint.setTextSize(mTextSize);
        mPaint.setTextAlign(Paint.Align.CENTER);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int cx = (getWidth() - getPaddingLeft() - getPaddingRight()) / 2;
        int cy = (getHeight() - getPaddingTop() - getPaddingBottom()) / 2;

        canvas.drawColor(Color.RED);
        if (TextUtils.isEmpty(mText)) {
            return;
        }
        canvas.drawText(mText,cx,cy,mPaint);

    }
}

布局文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_margin="20dp"
    android:layout_height="match_parent">

    <com.example.improve.TestView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:text="test" />

</RelativeLayout>

我們可以看到在自定義 View 的 TestView 代碼中,我們并沒有做測量有關(guān)的工作,因為我們根本就沒有復(fù)寫它的 onMeasure() 方法。但它卻完成了任務(wù),給定 layout_width 和 layout_height 兩個屬性明確的值之后,它就能夠正常顯示了。我們再改變一下數(shù)值。

<com.example.improve.TestView
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:text="test" />

將 layout_width 的值改為 match_parent,所以它的寬是由父類決定,但同樣它也正常。


我們已經(jīng)知道,上面的兩種情況其實就是對應(yīng) MeasureSpec.EXACTLY 這種測量模式,在這種模式下 TestView 本身不需要進行處理。

那么有人會問,如果 layout_width 或者 layout_height 的值為 wrap_content 的話,那么會怎么樣呢?
我們繼續(xù)測試觀察。

<com.example.improve.TestView
        android:layout_width="wrap_content"
        android:layout_height="100dp"
        android:text="test" />

效果和前面的一樣,寬度和它的 ViewGroup 同樣了。我們再看。

<com.example.improve.TestView
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:text="test"/>

寬度正常,高度卻和 ViewGroup 一樣了。

再看一種情況

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="20dp">

    <com.example.improve.TestView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="test" />

</RelativeLayout>


這次可以看到,寬高都和 ViewGroup 一致了。

但是,這不是我想要的?。?/p>

wrap_content 對應(yīng)的測量模式是 MeasureSpec.AT_MOST,所以它的第一要求就是 size 是由 View 本身決定,最大不超過 ViewGroup 能給予的建議數(shù)值。

TestView 如果在寬高上設(shè)置 wrap_content 屬性,也就代表著,它的大小由它的內(nèi)容決定,在這里它的內(nèi)容其實就是它中間位置的字符串。顯然上面的不符合要求,那么就顯然需要我們自己對測量進行處理。

我們的思路可以如下:

  • 對于 MeasureSpec.EXACTLY 模式,我們不做處理,將 ViewGroup 的建議數(shù)值作為最終的寬高。
  • 對于 MeasureSpec.AT_MOST 模式,我們要根據(jù)自己的內(nèi)容計算寬高,但是數(shù)值不得超過 ViewGroup 給出的建議值。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        /**resultW 代表最終設(shè)置的寬,resultH 代表最終設(shè)置的高*/
        int resultW = widthSize;
        int resultH = heightSize;

        int contentW = 0;
        int contentH = 0;

        /**重點處理 AT_MOST 模式,TestView 自主決定數(shù)值大小,但不能超過 ViewGroup 給出的
         * 建議數(shù)值
         * */
        if (widthMode == MeasureSpec.AT_MOST) {

            if (!TextUtils.isEmpty(mText)) {
                contentW = (int) mPaint.measureText(mText);
                contentW += getPaddingLeft() + getPaddingRight();
                resultW = Math.min(contentW, widthSize);
            }

        }

        if (heightMode == MeasureSpec.AT_MOST) {
            if (!TextUtils.isEmpty(mText)) {
                contentH = mTextSize;
                contentH += getPaddingTop() + getPaddingBottom();
                resultH = Math.min(contentH, heightSize);
            }
        }

        //一定要設(shè)置這個函數(shù),不然會報錯
        setMeasuredDimension(resultW, resultH);

    }

    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int cx = getPaddingLeft() + (getWidth() - getPaddingLeft() - getPaddingRight()) / 2;
        int cy = getPaddingTop() + (getHeight() - getPaddingTop() - getPaddingBottom()) / 2;

        Paint.FontMetrics metrics = mPaint.getFontMetrics();
        cy += metrics.descent;

        canvas.drawColor(Color.GREEN);
        if (TextUtils.isEmpty(mText)) {
            return;
        }
        canvas.drawText(mText, cx, cy, mPaint);
    }

代碼并不難,我們可以做驗證。

<com.example.improve.TestView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingLeft="10dp"
        android:paddingTop="10dp"
        android:paddingRight="10dp"
        android:text="test"
        android:textSize="24sp" />

在 MeasureSpec.EXACTLY 模式下同樣沒有問題。

現(xiàn)在,我們已經(jīng)掌握了自定義 View 的測量方法,其實也很簡單的嘛。

但是,還沒有完。我們驗證的剛剛是自定義 View,對于 ViewGroup 的情況是有些許不同的。

View 和 ViewGroup,雞生蛋,蛋生雞的關(guān)系

ViewGroup 是 View 的子類,但是 ViewGroup 的使命卻是裝載和組織 View。這好比是母雞是雞,母雞下蛋是為了孵化小雞,小雞長大后如果是母雞又下蛋,那么到底是蛋生雞還是雞生蛋?


自定義 View 的測量,我們已經(jīng)掌握了,那現(xiàn)在我們編碼來測試自定義 ViewGroup 時的測量變現(xiàn)。
假設(shè)我們要制定一個 ViewGroup,我們就給它起一個名字叫 TestViewGroup 好了,它里面的子元素按照對角線鋪設(shè),前面說過 ViewGroup 本質(zhì)上也是一個 View,只不過它多了布局子元素的義務(wù)。既然是 View 的話,那么自定義一個 ViewGroup 也需要從測量開始,問題的關(guān)鍵是如何準確地得到這個 ViewGroup 尺寸信息?

我們還是需要仔細討論。

  • 當(dāng) TestViewGroup 測量模式為 MeasureSpec.EXACTLY 時,這時候的尺寸就可以按照父容器傳遞過來的建議尺寸。要知道 ViewGroup 也有自己的 parent,在它的父容器中,它也只是一個 View。
  • 當(dāng) TestViewGroup 測量模式為 MeasureSpec.AT_MOST 時,這就需要 TestViewGroup 自己計算尺寸數(shù)值。就上面給出的信息而言,TestViewGroup 的尺寸非常簡單,那就是用自身 padding + 各個子元素的尺寸(包含子元素的寬高+子元素設(shè)置的 marging )得到一個可能的尺寸數(shù)值。然后用這個尺寸數(shù)值與 TestViewGroup 的父容器給出的建議 Size 進行比較,最終結(jié)果取最較小值。
  • 當(dāng) TestViewGroup 測量成功后,就需要布局了。自定義 View 基本上不要處理這一塊,但是自定義 ViewGroup,這一部分卻不可缺少。onLayout()是實現(xiàn)所有子控件布局的函數(shù)。注意,是所有子控件!那它自己的布局怎么辦?后面我們再講,先講講在onLayout()中我們應(yīng)該做什么。
    我們先看看ViewGroup onLayout()函數(shù)的默認行為是什么
    在ViewGroup.java中
@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);

是一個抽象方法,說明凡是派生自ViewGroup的類都必須自己去實現(xiàn)這個方法。像LinearLayout、RelativeLayout等布局,都是重寫了這個方法,然后在內(nèi)部按照各自的規(guī)則對子視圖進行布局的。

接下來,我們就可以具體編碼了。

public class TestViewGroup extends ViewGroup {


    public TestViewGroup(Context context) {
        this(context,null);
    }

    public TestViewGroup(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public TestViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        //只關(guān)心子元素的 margin 信息,所以這里用 MarginLayoutParams
        return new MarginLayoutParams(getContext(),attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        /**resultW 代表最終設(shè)置的寬,resultH 代表最終設(shè)置的高*/
        int resultW = widthSize;
        int resultH = heightSize;

        /**計算尺寸的時候要將自身的 padding 考慮進去*/
        int contentW = getPaddingLeft() + getPaddingRight();
        int contentH = getPaddingTop() + getPaddingBottom();

        /**對子元素進行尺寸的測量,這一步必不可少*/
        measureChildren(widthMeasureSpec,heightMeasureSpec);

        MarginLayoutParams layoutParams = null;

        for ( int i = 0;i < getChildCount();i++ ) {
            View child = getChildAt(i);
            layoutParams = (MarginLayoutParams) child.getLayoutParams();

            //子元素不可見時,不參與布局,因此不需要將其尺寸計算在內(nèi)
            if ( child.getVisibility() == View.GONE ) {
                continue;
            }

            contentW += child.getMeasuredWidth()
                    + layoutParams.leftMargin + layoutParams.rightMargin;

            contentH += child.getMeasuredHeight()
                    + layoutParams.topMargin + layoutParams.bottomMargin;
        }

        /**重點處理 AT_MOST 模式,TestViewGroup 通過子元素的尺寸自主決定數(shù)值大小,但不能超過
         *  ViewGroup 給出的建議數(shù)值
         * */
        if ( widthMode == MeasureSpec.AT_MOST ) {
            resultW = contentW < widthSize ? contentW : widthSize;
        }

        if ( heightMode == MeasureSpec.AT_MOST ) {
            resultH = contentH < heightSize ? contentH : heightSize;
        }

        //一定要設(shè)置這個函數(shù),不然會報錯
        setMeasuredDimension(resultW,resultH);

    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        int topStart = getPaddingTop();
        int leftStart = getPaddingLeft();
        int childW = 0;
        int childH = 0;
        MarginLayoutParams layoutParams = null;
        for ( int i = 0;i < getChildCount();i++ ) {
            View child = getChildAt(i);
            layoutParams = (MarginLayoutParams) child.getLayoutParams();

            //子元素不可見時,不參與布局,因此不需要將其尺寸計算在內(nèi)
            if ( child.getVisibility() == View.GONE ) {
                continue;
            }

            childW = child.getMeasuredWidth();
            childH = child.getMeasuredHeight();

            leftStart += layoutParams.leftMargin;
            topStart += layoutParams.topMargin;


            child.layout(leftStart,topStart, leftStart + childW, topStart + childH);

            leftStart += childW + layoutParams.rightMargin;
            topStart += childH + layoutParams.bottomMargin;
        }

    }

}

然后我們將之添加進 xml 布局文件中進行測試。

<?xml version="1.0" encoding="utf-8"?>
<com.example.improve.TestViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <com.example.improve.TestView
        android:layout_width="120dp"
        android:layout_height="wrap_content"
        android:paddingLeft="2dp"
        android:paddingTop="2dp"
        android:paddingRight="2dp"
        android:text="test"
        android:textSize="24sp" />

    <TextView
        android:layout_width="120dp"
        android:layout_height="50dp"
        android:background="#00ff40"
        android:paddingLeft="2dp"
        android:paddingTop="2dp"
        android:paddingRight="2dp"
        android:text="test"
        android:textSize="24sp" />

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher"
        android:text="test" />

</com.example.improve.TestViewGroup>

再試驗一下給 TestViewGroup 加上固定寬高。

<?xml version="1.0" encoding="utf-8"?>
<com.example.improve.TestViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="350dp"
    android:layout_height="400dp"
    android:background="#c3c3c3">

    <com.example.improve.TestView
        android:layout_width="120dp"
        android:layout_height="wrap_content"
        android:paddingLeft="2dp"
        android:paddingTop="2dp"
        android:paddingRight="2dp"
        android:text="test"
        android:textSize="24sp" />

    <TextView
        android:layout_width="120dp"
        android:layout_height="50dp"
        android:background="#00ff40"
        android:paddingLeft="2dp"
        android:paddingTop="2dp"
        android:paddingRight="2dp"
        android:text="test"
        android:textSize="24sp" />

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher"
        android:text="test" />

</com.example.improve.TestViewGroup>

結(jié)果如下:


自此,我們也知道了自定義 ViewGroup 的基本步驟,并且能夠處理 ViewGroup 的各種測量模式。

但是,在現(xiàn)實工作開發(fā)過程中,需求是不定的,我上面講的內(nèi)容只是基本的規(guī)則,大家熟練于心的時候才能從容應(yīng)對各種狀況。

getMeasuredWidth()與getWidth()

趁熱打鐵,就這個例子,我們講一個很容易出錯的問題:getMeasuredWidth()與getWidth()的區(qū)別。他們的值大部分時間都是相同的,但意義確是根本不一樣的,我們就來簡單分析一下。

  • 首先getMeasureWidth()方法在measure()過程結(jié)束后就可以獲取到了,而getWidth()方法要在layout()過程結(jié)束后才能獲取到。
  • getMeasureWidth()方法中的值是通過setMeasuredDimension()方法來進行設(shè)置的,而getWidth()方法中的值則是通過layout(left,top,right,bottom)方法設(shè)置的。

setMeasuredDimension()提供的測量結(jié)果只是為布局提供建議,最終的取用與否要看layout()函數(shù)。大家再看看我們上面寫的TestViewGroup,是不是我們自己使用child.layout(leftStart,topStart, leftStart + childW, topStart + childH)來定義了各個子控件所應(yīng)在的位置:

            childW = child.getMeasuredWidth();
            childH = child.getMeasuredHeight();

            leftStart += layoutParams.leftMargin;
            topStart += layoutParams.topMargin;


            child.layout(leftStart, topStart, leftStart + childW, topStart + childH);

從代碼中可以看到,我們使用child.layout(leftStart, topStart, leftStart + childW, topStart + childH);來布局控件的位置,其中g(shù)etWidth()的取值就是這里的右坐標減去左坐標的寬度;因為我們這里的寬度是,leftStart + childW,而getMeasuredWidth()與getWidth()的值是一樣的。如果我們在調(diào)用layout()的時候傳進去的寬度值不與getMeasuredWidth()相同,那必然getMeasuredWidth()與getWidth()的值就不再一樣了。

一定要注意的一點是:getMeasureWidth()方法在measure()過程結(jié)束后就可以獲取到了,而getWidth()方法要在layout()過程結(jié)束后才能獲取到。再重申一遍?。。?/strong>

TestViewGroup自己什么時候被布局

在onLayout()中布局它所有的子控件。那它自己什么時候被布局呢?它當(dāng)然也有父控件,它的布局也是在父控件中由它的父控件完成的,就這樣一層一層地向上由各自的父控件完成對自己的布局。真到所有控件的最頂層結(jié)點,在所有的控件的最頂部有一個ViewRoot,它才是所有控件的最終祖先結(jié)點。那讓我們來看看它是怎么來做的吧。

/* final 標識符 , 不能被重載 , 參數(shù)為每個視圖位于父視圖的坐標軸 
 * @param l Left position, relative to parent 
 * @param t Top position, relative to parent 
 * @param r Right position, relative to parent 
 * @param b Bottom position, relative to parent 
 */  
public final void layout(int l, int t, int r, int b) {  
    boolean changed = setFrame(l, t, r, b); //設(shè)置每個視圖位于父視圖的坐標軸  
    if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {  
        if (ViewDebug.TRACE_HIERARCHY) {  
            ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);  
        }  
  
        onLayout(changed, l, t, r, b);//回調(diào)onLayout函數(shù) ,設(shè)置每個子視圖的布局  
        mPrivateFlags &= ~LAYOUT_REQUIRED;  
    }  
    mPrivateFlags &= ~FORCE_LAYOUT;  

在setFrame(l,t,r,b)就是設(shè)置自己的位置,設(shè)置結(jié)束以后才會調(diào)用onLayout(changed, l, t, r, b)來設(shè)置內(nèi)部所有子控件的位置。
OK啦,到這里有關(guān)onMeasure()和onLayout()的內(nèi)容就講完啦,想必大家應(yīng)該也對整個布局流程有了一個清楚的認識了,下面我們再看一個緊要的問題:如何得到自定義控件的左右間距margin值。

獲取子控件Margin的方法

我會先簡單粗暴的教大家怎么先獲取到margin值,然后再細講為什么這樣寫,他們的原理是怎樣的。

如果要自定義ViewGroup支持子控件的layout_margin參數(shù),則自定義的ViewGroup類必須重載generateLayoutParams()函數(shù),并且在該函數(shù)中返回一個ViewGroup.MarginLayoutParams派生類對象,這樣才能使用margin參數(shù)。
我們在上面TestViewGroup例子的基礎(chǔ)上,添加上layout_margin參數(shù);

<?xml version="1.0" encoding="utf-8"?>
<com.as.customview.TestViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:background="#ff00ff"
    android:layout_height="match_parent">

    <com.as.customview.TestView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_margin="10dp"
        android:text="test"
        android:background="#FF5722"
        android:textSize="60sp" />

    <com.as.customview.TestView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_margin="10dp"
        android:text="test"
        android:background="#4CAF50"
        android:textSize="60sp" />

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:src="@mipmap/ic_launcher"
        android:text="test" />

</com.as.customview.TestViewGroup>

我們在每個TestView中都添加了一android:layout_margin參數(shù),而且值是10dp;背景也都分別改為了橙色,綠色和一張圖片;
現(xiàn)在我們運行一上,看看效果:


我們在onLayout()中沒有根據(jù)Margin來布局,當(dāng)然不會出現(xiàn)有關(guān)Margin的效果啦。需要特別注意的是,如果我們在onLayout()中根據(jù)margin來布局的話,那么我們在onMeasure()中計算TestViewGroup的大小時,也要加上margin,不然會導(dǎo)致TestViewGroup太小,而控件顯示不全的問題。費話不多說,我們直接看代碼實現(xiàn)。

@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
    return new MarginLayoutParams(p);
}
 
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new MarginLayoutParams(getContext(), attrs);
}
 
@Override
protected LayoutParams generateDefaultLayoutParams() {
    return new MarginLayoutParams(LayoutParams.MATCH_PARENT,
            LayoutParams.MATCH_PARENT);
}

首先,在TestViewGroup在初始化子控件時,會調(diào)用LayoutParams generateLayoutParams(LayoutParams p)來為子控件生成對應(yīng)的布局屬性,但默認只是生成layout_width和layout_height所以對應(yīng)的布局參數(shù),即在正常情況下的generateLayoutParams()函數(shù)生成的LayoutParams實例是不能夠取到margin值的。即:

/**
*從指定的XML中獲取對應(yīng)的layout_width和layout_height值
*/
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new LayoutParams(getContext(), attrs);
}
/*
*如果要使用默認的構(gòu)造方法,就生成layout_width="wrap_content"、layout_height="wrap_content"對應(yīng)的參數(shù)
*/
protected LayoutParams generateDefaultLayoutParams() {
     return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}

所以,如果我們還需要margin相關(guān)的參數(shù)就只能重寫generateLayoutParams()函數(shù)了:

public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new MarginLayoutParams(getContext(), attrs);
}

由于generateLayoutParams()的返回值是LayoutParams實例,而MarginLayoutParams是派生自LayoutParam的;所以根據(jù)類的多態(tài)的特性,可以直接將此時的LayoutParams實例直接強轉(zhuǎn)成MarginLayoutParams實例;
所以下面這句在這里是不會報錯的:

MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

大家也可以為了安全起見利用instanceOf來做下判斷,如下:

MarginLayoutParams lp = null
if (child.getLayoutParams() instanceof  MarginLayoutParams) {
    lp = (MarginLayoutParams) child.getLayoutParams();
}

所以整體來講,就是利用了類的多態(tài)特性!下面來看看MarginLayoutParams和generateLayoutParams()都做了什么。

generateLayoutParams()實現(xiàn)

//位于ViewGrop.java中
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new LayoutParams(getContext(), attrs);
}
public LayoutParams(Context c, AttributeSet attrs) {
    TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
    setBaseAttributes(a,
            R.styleable.ViewGroup_Layout_layout_width,
            R.styleable.ViewGroup_Layout_layout_height);
    a.recycle();
}
protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
    width = a.getLayoutDimension(widthAttr, "layout_width");
    height = a.getLayoutDimension(heightAttr, "layout_height");
}

從上面的代碼中明顯可以看出,generateLayoutParams()調(diào)用LayoutParams()產(chǎn)生布局信息,而LayoutParams()最終調(diào)用setBaseAttributes()來獲得對應(yīng)的寬,高屬性。
這里是通過TypedArray對自定義的XML進行值提取的過程,難度不大,不再細講。從這里也可以看到,generateLayoutParams生成的LayoutParams屬性只有l(wèi)ayout_width和layout_height的屬性值。

下面再來看看MarginLayoutParams的具體實現(xiàn),其實通過上面的過程,大家也應(yīng)該想到,它也是通過TypeArray來解析自定義屬性來獲得用戶的定義值的(大家看到長代碼不要害怕,先列出完整代碼,下面會分段講):

public MarginLayoutParams(Context c, AttributeSet attrs) {
    super();
 
    TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
    int margin = a.getDimensionPixelSize(
            com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1);
    if (margin >= 0) {
        leftMargin = margin;
        topMargin = margin;
        rightMargin= margin;
        bottomMargin = margin;
    } else {
       leftMargin = a.getDimensionPixelSize(
               R.styleable.ViewGroup_MarginLayout_layout_marginLeft,
               UNDEFINED_MARGIN);
       rightMargin = a.getDimensionPixelSize(
               R.styleable.ViewGroup_MarginLayout_layout_marginRight,
               UNDEFINED_MARGIN);
 
       topMargin = a.getDimensionPixelSize(
               R.styleable.ViewGroup_MarginLayout_layout_marginTop,
               DEFAULT_MARGIN_RESOLVED);
 
       startMargin = a.getDimensionPixelSize(
               R.styleable.ViewGroup_MarginLayout_layout_marginStart,
               DEFAULT_MARGIN_RELATIVE);
       endMargin = a.getDimensionPixelSize(
               R.styleable.ViewGroup_MarginLayout_layout_marginEnd,
               DEFAULT_MARGIN_RELATIVE);
    }
    a.recycle();
}

這段代碼分為兩部分:
第一部分:提取layout_margin的值并設(shè)置

TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
int margin = a.getDimensionPixelSize(
        com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1);
if (margin >= 0) {
    leftMargin = margin;
    topMargin = margin;
    rightMargin= margin;
    bottomMargin = margin;
} else {
  …………
}

在這段代碼中就是通過提取layout_margin的值來設(shè)置上,下,左,右邊距的。
第二部分:如果用戶沒有設(shè)置layout_margin,而是單個設(shè)置的,那么就一個個提取,代碼如下:

leftMargin = a.getDimensionPixelSize(
        R.styleable.ViewGroup_MarginLayout_layout_marginLeft,
        UNDEFINED_MARGIN);
rightMargin = a.getDimensionPixelSize(
        R.styleable.ViewGroup_MarginLayout_layout_marginRight,
        UNDEFINED_MARGIN);
 
topMargin = a.getDimensionPixelSize(
        R.styleable.ViewGroup_MarginLayout_layout_marginTop,
        DEFAULT_MARGIN_RESOLVED);
 
startMargin = a.getDimensionPixelSize(
        R.styleable.ViewGroup_MarginLayout_layout_marginStart,
        DEFAULT_MARGIN_RELATIVE);
endMargin = a.getDimensionPixelSize(
        R.styleable.ViewGroup_MarginLayout_layout_marginEnd,
        DEFAULT_MARGIN_RELATIVE);

這里就是對layout_marginLeft、layout_marginRight、layout_marginTop、layout_marginBottom的值一個個提取的過程。
從這里大家也可以看到為什么非要重寫generateLayoutParams()函數(shù)了,就是因為默認的generateLayoutParams()函數(shù)只會提取layout_width、layout_height的值,只有MarginLayoutParams()才具有提取margin間距的功能!?。?!

TestViewGroup 作為一個演示用的例子,只為了說明測量規(guī)則和基本的自定義方法。對于 Android 開發(fā)初學(xué)者而言,還是要多閱讀代碼,關(guān)鍵是要多臨摹別人的優(yōu)秀的自定義 View 或者 ViewGroup。

我個人覺得,嘗試自己動手去實現(xiàn)一個流式標簽控件,對于提高自定義 ViewGroup 的能力是有很大的提高,因為只有在自己實踐中思考,在思考和實驗的過程你才會深刻的理解測量機制的用途。

不過自定義一個流式標簽控件是另外一個話題了,也許我會另外開一篇來講解,不過我希望大家親自動手去實現(xiàn)它。


洋洋灑灑寫了這么多的內(nèi)容,其實基本上已經(jīng)完結(jié)了,已經(jīng)不耐煩的同學(xué)可以直接跳轉(zhuǎn)到后面的總結(jié)。但是,對于有鉆研精神的同學(xué)來講,其實還不夠。還沒有完。

問題1:到底是誰在測量 View ?

問題2:到底是什么時候需要測量 View ?
針對問題 1:
我們在自定義 TestViewGroup 的時候,在 onMeasure() 方法中,通過了一個 API 對子元素進行了測量,這個 API 就是 measureChildren()。這個方法進行了什么樣的處理呢?我們可以去看看。

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

/**
  * 分析2:measureChild()
  * 作用:a. 計算單個子View的MeasureSpec
  *      b. 測量每個子View最后的寬 / 高:調(diào)用子View的measure()
  **/ 
  protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {

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

        // 2. 根據(jù)父視圖的MeasureSpec & 布局參數(shù)LayoutParams,計算單個子View的MeasureSpec
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,// 獲取 ChildView 的 widthMeasureSpec
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,// 獲取 ChildView 的 heightMeasureSpec
                mPaddingTop + mPaddingBottom, lp.height);

        // 3. 將計算好的子View的MeasureSpec值傳入measure(),進行最后的測量
        // 下面的流程即類似單一View的過程,此處不作過多描述
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

代碼簡短易懂,分別調(diào)用 child 的 measure() 方法。值得注意的是,傳遞給 child 的測量規(guī)格已經(jīng)發(fā)生了變化,比如 widthMeasureSpec 變成了 childWidthMeasureSpec。原因是這兩行代碼:
一開始我們就了解子視圖是如何測量的

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
        mPaddingTop + mPaddingBottom, lp.height);

我們繼續(xù)向前,ViewGroup 的 measureChild() 方法最終會調(diào)用 View.measure() 方法。我們進一步跟蹤。

/**
  * 源碼分析:measure()
  * 定義:Measure過程的入口;屬于View.java類 & final類型,即子類不能重寫此方法
  * 作用:基本測量邏輯的判斷
  **/ 

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

        // 參數(shù)說明:View的寬 / 高測量規(guī)格

        ...

        int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
                mMeasureCache.indexOfKey(key);

        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            // 計算視圖大小 ->>分析1

        } else {
            ...
      
    }

/**
  * 分析1:onMeasure()
  * 作用:a. 根據(jù)View寬/高的測量規(guī)格計算View的寬/高值:getDefaultSize()
  *      b. 存儲測量后的View寬 / 高:setMeasuredDimension()
  **/ 
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
    // 參數(shù)說明:View的寬 / 高測量規(guī)格
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),  
                         getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));  
}

protected int getSuggestedMinimumWidth() {
     //mMinWidth  = android:minWidth屬性所指定的值;
    return (mBackground == null) ? mMinWidth : max(mMinWidth,mBackground.getMinimumWidth());
}
//getSuggestedMinimumHeight()同理


/**
  * 分析2:setMeasuredDimension()
  * 作用:存儲測量后的View寬 / 高
  * 注:該方法即為我們重寫onMeasure()所要實現(xiàn)的最終目的
  **/
    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {  

        // 將測量后子View的寬 / 高值進行傳遞
            mMeasuredWidth = measuredWidth;  
            mMeasuredHeight = measuredHeight;  
          
            mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;  
        } 
    // 由于setMeasuredDimension()的參數(shù)是從getDefaultSize()獲得的
    // 下面我們繼續(xù)看getDefaultSize()的介紹

/**
  * 分析3:getDefaultSize()
  * 作用:根據(jù)View寬/高的測量規(guī)格計算View的寬/高值
  **/
  public static int getDefaultSize(int size, int measureSpec) {  

        // 參數(shù)說明:
        // size:提供的默認大小
        // measureSpec:寬/高的測量規(guī)格(含模式 & 測量大?。?
            // 設(shè)置默認大小
            int result = size; 
            
            // 獲取寬/高測量規(guī)格的模式 & 測量大小
            int specMode = MeasureSpec.getMode(measureSpec);  
            int specSize = MeasureSpec.getSize(measureSpec);  
          
            switch (specMode) {  
                // 模式為UNSPECIFIED時,使用提供的默認大小 = 參數(shù)Size
                case MeasureSpec.UNSPECIFIED:  
                    result = size;  
                    break;  

                // 模式為AT_MOST,EXACTLY時,使用View測量后的寬/高值 = measureSpec中的Size
                case MeasureSpec.AT_MOST:  
                case MeasureSpec.EXACTLY:  
                    result = specSize;  
                    break;  
            }  

         // 返回View的寬/高值
            return result;  
        }    

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

// 由源碼可知:mBackground.getMinimumWidth()的大小 = 背景圖Drawable的原始寬度
// 若無原始寬度,則為0;
// 注:BitmapDrawable有原始寬度,而ShapeDrawable沒有

最后,我們在看看測量的流程圖


Activity 中的道,最頂層的那個 View?

道生一,一生二,二生三,三生萬物,萬物負陰而抱陽,沖氣以為和。– 《道德經(jīng)》

我們已經(jīng)知道,不管是對于 View 還是 ViewGroup 而言,測量的起始是 measure() 方法,沿著控件樹一路遍歷下去。那么,對于 Android 一個 Activity 而言,它的頂級 View 或者頂級 ViewGroup 是哪一個呢?

從 setContentView 說起

我們知道給 Activity 布局的時候,在 onCreate() 中設(shè)置 setContentView() 的資源文件就是我們普通開發(fā)者所能想到的比較頂層的 View 了。比如在 activity_main.xml 中設(shè)置一個 RelativeLayout,那么這個 RelativeLayout 就是 Activity 最頂層的 View 嗎?誰調(diào)用它的 measure() 方法觸發(fā)整個控件樹的測量?


public void setContentView(int layoutResID) {
    getWindow().setContentView(layoutResID);
    initActionBar();
}

public Window getWindow() {
    return mWindow;
}

/**
 * Abstract base class for a top-level window look and behavior policy.  An
 * instance of this class should be used as the top-level view added to the
 * window manager. It provides standard UI policies such as a background, title
 * area, default key processing, etc.
 *
 * <p>The only existing implementation of this abstract class is
 * android.policy.PhoneWindow, which you should instantiate when needing a
 * Window.  Eventually that class will be refactored and a factory method
 * added for creating Window instances without knowing about a particular
 * implementation.
 */
public abstract class Window {

}

public class PhoneWindow extends Window implements MenuBuilder.Callback {

}

可以看到,調(diào)用 Activity.setContentView() 其實就是調(diào)用 PhoneWindow.setContentView()。

PhoneWindow.java

@Override
public void setContentView(View view) {
    setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}

@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
    if (mContentParent == null) {
        installDecor();
    } else {
        mContentParent.removeAllViews();
    }
    mContentParent.addView(view, params);
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
}

注意,在上面代碼中顯示,通過 setContentView 傳遞進來的 view 被添加到了一個 mContentParent 變量上了,所以可以回答上面的問題,通過 setContentView() 中傳遞的 View 并不是 Activity 最頂層的 View。我們再來看看 mContentParent。

它只是一個 ViewGroup。我們再把焦點聚集到 installDecor() 這個函數(shù)上面。

private void installDecor() {
    if (mDecor == null) {
        mDecor = generateDecor();

    }
    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor);

        // Set up decor part of UI to ignore fitsSystemWindows if appropriate.
        mDecor.makeOptionalFitsSystemWindows();

        mTitleView = (TextView)findViewById(com.android.internal.R.id.title);
        if (mTitleView != null) {
            mTitleView.setLayoutDirection(mDecor.getLayoutDirection());
            if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {
                View titleContainer = findViewById(com.android.internal.R.id.title_container);
                if (titleContainer != null) {
                    titleContainer.setVisibility(View.GONE);
                } else {
                    mTitleView.setVisibility(View.GONE);
                }
                if (mContentParent instanceof FrameLayout) {
                    ((FrameLayout)mContentParent).setForeground(null);
                }
            } else {
                mTitleView.setText(mTitle);
            }
        } else {
            mActionBar = (ActionBarView) findViewById(com.android.internal.R.id.action_bar);


        }
    }
}

代碼很長,我刪除了一些與主題無關(guān)的代碼。這個方法體內(nèi)引出了一個 mDecor 變量,它通過 generateDecor() 方法創(chuàng)建。DecorView 是 PhoneWindow 定義的一個內(nèi)部類,實際上是一個 FrameLayout。

private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {

}

我們回到 generate() 方法

protected DecorView generateDecor() {
    return new DecorView(getContext(), -1);
}

DecorView 怎么創(chuàng)建的我們已經(jīng)知曉,現(xiàn)在看看 mContentParent 創(chuàng)建方法 generateLayout()。它傳遞進了一個 DecorView,所以它與 mDecorView 肯定有某種關(guān)系。

protected ViewGroup generateLayout(DecorView decor) {
        // Apply data from current theme.

    WindowManager.LayoutParams params = getAttributes();
    // Inflate the window decor.
    // Embedded, so no decoration is needed.
    layoutResource = com.android.internal.R.layout.screen_simple;
    // System.out.println("Simple!");
    mDecor.startChanging();
    View in = mLayoutInflater.inflate(layoutResource, null);

    decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));

    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    if (contentParent == null) {
        throw new RuntimeException("Window couldn't find content container view");
    }


    mDecor.finishChanging();

    return contentParent;
}

原代碼很長,我刪除了一些繁瑣的代碼,整個流程變得很清晰,這個方法內(nèi) inflate 了一個 xml 文件,然后被添加到了 mDecorView。而 mContentParent 就是這個被添加進去的 view 中。
這個 xml 文件是 com.android.internal.R.layout.screen_simple,我們可以從 SDK 包中找出它來。


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content" />
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

就是一個 LinearLayout ,方向垂直。2 個元素,一個是 actionbar,一個是 content。并且 ViewStub 導(dǎo)致 actionbar 需要的時候才會進行加載。

總之由以上信息,我們可以得到 Activity 有一個 PhoneWindow 對象,PhoneWindow 中有一個 DecorView,DecorView 內(nèi)部有一個 LinearLayout,LinearLayout 中存在 id 為 android:id/content 的布局 mContentParent。 mContentParent加載Activity 通過 setContentView 傳遞進來的 View,所以整個結(jié)構(gòu)呼之欲出。

注意:因為代碼有刪簡,實際上 LinearLayout 由兩部分組成,下面的是 Content 無疑,上面的部分不一定是 ActionBar,也可能是 title,不過這不影響我們,我們只需要記住 content 就好了。



DecorView 才是 Activity 中整個控件樹的根。

誰測繪了頂級 View ?

既然 DecorView 是整個測繪的發(fā)起點,那么誰對它進行了測繪?誰調(diào)用了它的 measure() 方法,從而導(dǎo)致整個控件樹自上至下的尺寸測量?

我們平常開發(fā)知道調(diào)用一個 View.requestLayout() 方法,可以引起界面的重新布局,那么 requestLayout() 干了什么?

我們再回到 PhoneWindow 的 setContentView() 中來。

public void setContentView(View view, ViewGroup.LayoutParams params) {
    if (mContentParent == null) {
        installDecor();
    } else {
        mContentParent.removeAllViews();
    }
    mContentParent.addView(view, params);
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
}

我們看看 mContentParent.addView(view, params) 的時候發(fā)生了什么。

public void addView(View child, int index, LayoutParams params) {
    if (DBG) {
        System.out.println(this + " addView");
    }

    // addViewInner() will call child.requestLayout() when setting the new LayoutParams
    // therefore, we call requestLayout() on ourselves before, so that the child's request
    // will be blocked at our level
    requestLayout();
    invalidate(true);
    addViewInner(child, index, params, false);
}

我們這篇文章就到這里

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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