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

測量 View 就是測量一個矩形


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

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

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

布局繪畫涉及兩個過程:測量過程和布局過程谱俭。測量過程通過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 找一個父容器進行觀察例驹。父容器背景由特定顏色標(biāo)識。

<?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位做標(biāo)志位
        private static final int MODE_SHIFT = 30;  
          
        // 運算遮罩:0x3為16進制蜘拉,10進制為3,二進制為11
        // 3向左進位30 = 11 00000000000(11后跟30個0)  
        // 作用:用1標(biāo)注需要的值有鹿,0標(biāo)注不要的值旭旭。因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位代表測量大熊怼(size)
            }  
      
        /**
          * 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. 獲取測量大信(Size)
    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

我的目標(biāo)是定義一個文本框崔挖,中間顯示黑色文字,背景色為綠色庵寞。

我們可以輕松地進行編碼狸相。首先,我們定義好它需要的屬性捐川,然后編寫它的 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 一致了滤港。

但是,這不是我想要的芭颗 溅漾!

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)鍵是如何準(zhǔ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()的取值就是這里的右坐標(biāo)減去左坐標(biāo)的寬度;因為我們這里的寬度是,leftStart + childW机蔗,而getMeasuredWidth()與getWidth()的值是一樣的蒲祈。如果我們在調(diào)用layout()的時候傳進去的寬度值不與getMeasuredWidth()相同,那必然getMeasuredWidth()與getWidth()的值就不再一樣了萝嘁。

一定要注意的一點是:getMeasureWidth()方法在measure()過程結(jié)束后就可以獲取到了梆掸,而getWidth()方法要在layout()過程結(jié)束后才能獲取到。再重申一遍Q姥浴K崆铡!

TestViewGroup自己什么時候被布局

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

/* final 標(biāo)識符 , 不能被重載 河爹, 參數(shù)為每個視圖位于父視圖的坐標(biāo)軸 
 * @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è)置每個視圖位于父視圖的坐標(biāo)軸  
    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間距的功能M榷獭F凉俊;婷巍!

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

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

不過自定義一個流式標(biāo)簽控件是另外一個話題了尚辑,也許我會另外開一篇來講解,不過我希望大家親自動手去實現(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)系作者
  • 序言:七十年代末勇皇,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子焚刺,更是在濱河造成了極大的恐慌敛摘,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件乳愉,死亡現(xiàn)場離奇詭異兄淫,居然都是意外死亡,警方通過查閱死者的電腦和手機蔓姚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門捕虽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人坡脐,你說我怎么就攤上這事泄私。” “怎么了备闲?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵晌端,是天一觀的道長。 經(jīng)常有香客問我恬砂,道長咧纠,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任泻骤,我火速辦了婚禮惧盹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘瞪讼。我一直安慰自己钧椰,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布符欠。 她就那樣靜靜地躺著嫡霞,像睡著了一般。 火紅的嫁衣襯著肌膚如雪希柿。 梳的紋絲不亂的頭發(fā)上诊沪,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天养筒,我揣著相機與錄音,去河邊找鬼端姚。 笑死晕粪,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的渐裸。 我是一名探鬼主播巫湘,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼昏鹃!你這毒婦竟也來了尚氛?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤洞渤,失蹤者是張志新(化名)和其女友劉穎阅嘶,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體载迄,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡讯柔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了护昧。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片磷杏。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖捏卓,靈堂內(nèi)的尸體忽然破棺而出极祸,到底是詐尸還是另有隱情,我是刑警寧澤怠晴,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布遥金,位于F島的核電站,受9級特大地震影響蒜田,放射性物質(zhì)發(fā)生泄漏稿械。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一冲粤、第九天 我趴在偏房一處隱蔽的房頂上張望美莫。 院中可真熱鬧,春花似錦梯捕、人聲如沸厢呵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽襟铭。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間寒砖,已是汗流浹背赐劣。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留哩都,地道東北人魁兼。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像漠嵌,于是被迫代替她去往敵國和親咐汞。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容