Android——View的工作流程——ViewGroup的measure過程

一、measure 過程

對 ViewGroup 來說椅亚,除了完成自己的 measure 過程以外葡秒,還要遍歷所有子 View 的 measure 方法,各個子元素再去遞歸執(zhí)行這個過程砂碉。

ViewGroup 的 measure 過程代碼調(diào)用順序如下:

measure 過程

1. measure()

Android 源碼中,View 類的聲明是

public class View implements Drawable.Callback, KeyEvent.Callback,
    AccessibilityEventSource{}

ViewGroup 類的聲明是

public abstract class ViewGroup extends View implements ViewParent, ViewManager {}

ViewGroup 類是 View 類的子類刻两,ViewGroup 的 measure 過程入口是 View 類中的measure()方法增蹭,與單一 View 的測量過程中介紹的measure()方法一致。

/**
 * 源碼分析:measure()
 * 作用:基本測量邏輯的判斷闹伪;調(diào)用onMeasure()
 * 注:與單一View measure過程中講的measure()一致
 **/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ...
    int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 : mMeasureCache.indexOfKey(key);
    if (cacheIndex < 0 || sIgnoreMeasureCache) {

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

2. onMeasure()

ViewGroup 是一個抽象類沪铭,它沒有重寫 View 的 onMeasure()方法壮池,該方法作用是父容器測量子 View 的尺寸偏瓤,其測量過程的onMeasure()方法需要各個子類去具體實現(xiàn)杀怠。

Q:為什么 ViewGroup 的 measure 過程不像單一 View 的 measure 過程那樣對 onMeasure() 做統(tǒng)一的實現(xiàn)?

A:因為不同的 ViewGroup 子類(LinearLayout厅克、RelativeLayout 及自定義的 ViewGroup 子類等)具備不同的布局特性赔退,這導(dǎo)致他們的測量細節(jié)各有不同,ViewGroup 無法對onMeasure()做統(tǒng)一實現(xiàn)证舟。

在自定義 ViewGroup 中硕旗,關(guān)鍵在于:根據(jù)需求復(fù)寫onMeasure()從而實現(xiàn)你的子View測量邏輯。復(fù)寫onMeasure()的套路如下:

/**
 * 根據(jù)自身測量邏輯復(fù)寫onMeasure()女责,分3步:
 * (1)遍歷所有子view漆枚,依次進行測量
 * (2)合并所有子View的尺寸大小,最終得到ViewGroup父視圖的測量值(自身實現(xiàn))
 * (3)存儲測量后View寬/高值抵知,調(diào)用setMeasuredDimension()
 * @param widthMeasureSpec
 * @param heightMeasureSpec
 */
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    // 定義存放測量后view寬/高的變量
    int widthMeasure;
    int heightMeasure;

    // 1,遍歷所有子View墙基,依次進行測量
    measureChildren(widthMeasureSpec, heightMeasureSpec);

    // 2,合并所有子View的尺寸大小,最終得到父視圖的測量值
    void measureCarson {
        // 自己實現(xiàn)
    }

    // 3,存儲測量后view的寬/高
    setMeasuredDimension(widthMeasure, heightMeasure);

}

下面看下 LinearLayout 類中實現(xiàn)的onMeasure()方法刷喜。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (mOrientation == VERTICAL) {
        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
}

measureVertical()measureHorizontal()原理類似残制,我們以前者為例,代碼主要分三個步驟:

① 遍歷所有子View 掖疮,依次進行測量
系統(tǒng)首先遍歷子元素初茶,并對每個子元素執(zhí)行measureChildBeforeLayout()方法,這個方法內(nèi)部會調(diào)用子元素的measure()方法浊闪,這樣各個元素就開始進入 measure 過程

② 合并所有子View的尺寸大小恼布,得到 ViewGroup 的測量值
系統(tǒng)通過mTotalLength存儲 LinearLayout 在豎直方向上的高度

③ 存儲 ViewGroup 的測量值
使用setMeasuredDimension()方法

/**
 * 測量 LinearLayout 垂直方向的測量尺寸
 *
 * @param widthMeasureSpec  父容器的寬測量規(guī)格
 * @param heightMeasureSpec 父容器的高測量規(guī)格
 */
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    final int count = getVirtualChildCount();

    final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

    // 對于每一個垂直方向上的子View
    /**
     *  步驟1:遍歷所有子View ,依次進行測量
     *  注:該方法內(nèi)部搁宾,最終會調(diào)用measureChildren()桥氏,從而遍歷所有子View & 測量
     **/
    for (int i = 0; i < count; ++i) {
        final View child = getVirtualChildAt(i);
        if (child == null) {
            mTotalLength += measureNullChild(i);
            continue;
        }
        // 若子View不可見,則直接跳過該View的測量過程
        // 注:若可見屬性設(shè)置為VIEW.INVISIBLE猛铅,則仍需測量
        if (child.getVisibility() == View.GONE) {
            i += getChildrenSkipCount(child, i);
            continue;
        }

        final LayoutParams lp = (LayoutParams) child.getLayoutParams();

        totalWeight += lp.weight;

        final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
        if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
            // 如果LinearLayout的specMode為EXACTLY且子View設(shè)置了weight屬性字支,在這里會跳過子View的measure過程
            // 同時標記skippedMeasure屬性為true,后面會根據(jù)該屬性決定是否進行第二次measure
            // 若LinearLayout的子View設(shè)置了weight奸忽,會進行兩次measure計算堕伪,比較耗時
            // 這就是為什么LinearLayout的子View需要使用weight屬性時候,最好替換成RelativeLayout布局
            final int totalLength = mTotalLength;
            mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
            skippedMeasure = true;
        } else {
            if (useExcessSpace) {
                // The heightMode is either UNSPECIFIED or AT_MOST, and
                // this child is only laid out using excess space. Measure
                // using WRAP_CONTENT so that we can find out the view's
                // optimal height. We'll restore the original height of 0
                // after measurement.
                lp.height = LayoutParams.WRAP_CONTENT;
            }

            /**
             * 調(diào)用measureChildBeforeLayout()測量子View
             */
            final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
            measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                heightMeasureSpec, usedHeight);

            /**
             * 通過 getMeasuredHeight() 獲取View的測量寬/高
             * 注:在某些極端情況下栗菜,系統(tǒng)可能需要多次 measure 才能確定最終的寬/高欠雌,在這種情形下,
             * 在onMeasure()中通過 getMeasuredHeight() 獲取測量寬高是不準確的疙筹。
             * 一個好的習(xí)慣是富俄,在 onLayout() 方法中獲取 View 的最終寬/高
             */
            final int childHeight = child.getMeasuredHeight();
            if (useExcessSpace) {
                // Restore the original height and record how much space
                // we've allocated to excess-only children so that we can
                // match the behavior of EXACTLY measurement.
                lp.height = 0;
                consumedExcessSpace += childHeight;
            }

            // totalLength
            /**
             * 系統(tǒng)通過 mTotalLength 這個變量來存儲 LinearLayout 在豎直方向的初步高度禁炒。
             * 每測量一個元素, mTotalLength就會增加霍比,增加的部分主要包括了子元素的高度以及子元素在豎直方向上的 margin 等幕袱。
             */
            final int totalLength = mTotalLength;
            mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                lp.bottomMargin + getNextLocationOffset(child));


        }
    }

    /**
     *  步驟2:合并所有子View的尺寸大小,最終得到ViewGroup的測量值
     *  子元素測量完畢后,LinearLayout 根據(jù)子元素的情況來測量自己的大小悠瞬。
     **/
    // 記錄 LinearLayout 占用的總高度们豌,除了子View占用的高度,還有本身的 padding 屬性
    // Add in our padding
    mTotalLength += mPaddingTop + mPaddingBottom;

    int heightSize = mTotalLength;

    // Check against our minimum height
    heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

    // Reconcile our calculated size with the heightMeasureSpec
    int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
    heightSize = heightSizeAndState & MEASURED_SIZE_MASK;

    /**
     *  步驟3:存儲測量后View寬/高的值:調(diào)用setMeasuredDimension()
     **/
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
        heightSizeAndState);
}

3. measureChildren()

ViewGroup 類中的方法浅妆。在該方法中遍歷父容器中的所有子元素望迎,逐個進行 measure。

/**
 * 遍歷凌外,測量父容器中的所有子View
 *
 * @param widthMeasureSpec 父容器的寬測量規(guī)格
 * @param heightMeasureSpec 父容器的高測量規(guī)格
 */
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) {
            // 調(diào)用measureChild()方法對子View進行進一步測量
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

4. measureChild()

ViewGroup 類中的方法辩尊。該方法測量某一具體的子元素,測量過程參見自定義View——單一View的measure過程康辑。

/**
 * 計算子View的MeasureSpec摄欲,并且測量子View最終的寬/高
 *
 * @param child The child to measure
 * @param parentWidthMeasureSpec The width requirements for this view
 * @param parentHeightMeasureSpec The height requirements for this view
 */
protected void measureChild(View child, int parentWidthMeasureSpec,
    int parentHeightMeasureSpec) {
    // 子View自身的布局參數(shù)
    final LayoutParams lp = child.getLayoutParams();

    // 計算子View的測量規(guī)格
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
        mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
        mPaddingTop + mPaddingBottom, lp.height);
    // 根據(jù)上述參數(shù)測量子View
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

二、重點說明

參考文獻

自定義View Measure過程 - 最易懂的自定義View原理系列(2)
任玉剛_Android開發(fā)藝術(shù)探索

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末晾捏,一起剝皮案震驚了整個濱河市蒿涎,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌惦辛,老刑警劉巖劳秋,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異胖齐,居然都是意外死亡玻淑,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進店門呀伙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來补履,“玉大人,你說我怎么就攤上這事剿另◇锎福” “怎么了?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵雨女,是天一觀的道長谚攒。 經(jīng)常有香客問我,道長氛堕,這世上最難降的妖魔是什么馏臭? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮讼稚,結(jié)果婚禮上括儒,老公的妹妹穿的比我還像新娘绕沈。我一直安慰自己,他們只是感情好帮寻,可當我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布乍狐。 她就那樣靜靜地躺著,像睡著了一般规婆。 火紅的嫁衣襯著肌膚如雪澜躺。 梳的紋絲不亂的頭發(fā)上蝉稳,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天抒蚜,我揣著相機與錄音,去河邊找鬼耘戚。 笑死嗡髓,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的收津。 我是一名探鬼主播饿这,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼撞秋!你這毒婦竟也來了长捧?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤吻贿,失蹤者是張志新(化名)和其女友劉穎串结,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體舅列,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡肌割,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了帐要。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片把敞。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖榨惠,靈堂內(nèi)的尸體忽然破棺而出奋早,到底是詐尸還是另有隱情,我是刑警寧澤赠橙,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布耽装,位于F島的核電站,受9級特大地震影響简烤,放射性物質(zhì)發(fā)生泄漏剂邮。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一横侦、第九天 我趴在偏房一處隱蔽的房頂上張望挥萌。 院中可真熱鬧绰姻,春花似錦、人聲如沸引瀑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽憨栽。三九已至帜矾,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間屑柔,已是汗流浹背屡萤。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留掸宛,地道東北人死陆。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像唧瘾,于是被迫代替她去往敵國和親措译。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,435評論 2 359