View 工作原理(一)| 藝術(shù)探索筆記

ViewRoot 和 DecorView

ViewRoot

ViewRoot 對應 ViewRootImpl 類,是連接 WindowManager 和 DecorView 的紐帶。View 的繪制流程從 ViewRoot 的 performTraversals 方法開始伤柄,經(jīng)過 Measure膀跌、Layout号俐、Draw 三個過程最終將 View 繪制出來皆怕。其中 Measure 測量 View 的寬高娃殖,Layout 來確定 View 在父容器中放置的位置值戳,Draw 負責將 View 繪制在屏幕上。

performTraversals 的工作流程

performMeasure炉爆、performLayout堕虹、performDraw 這三個方法分別完成頂級 View 的 Measure、Layout叶洞、Draw 三大流程鲫凶。其中在 performMeasure 中會調(diào)用 measure 方法禀崖,在 measure 中又會調(diào)用 onMeasure 方法衩辟,在 onMeasure 中會對所有子元素進行 measure 過程,這時 measure 流程從父容器傳遞到子元素中波附,這就完成了一次 Measure 過程艺晴。接著子元素會重復父容器的這個過程,這樣進行下去就完成了整個 View 的遍歷掸屡。performLayout 和 performDraw 類似封寞。不過,在 performDraw 中的傳遞過程是在 draw 方法中通過 dispatchDraw 實現(xiàn)仅财。

Measure 完成以后狈究,可以通過 getMeasuredWidth 和 getMeasuredHeight 方法獲取測量后的寬高,在大部分的情況下等于最終的寬高(待補充)盏求。Layout 完成后抖锥,可以通過 getTop、getBottom碎罚、getLeft 和 getRight 來得到 View 四個頂點的位置磅废,并通過 getWidth 和 getHeight 方法得到最終的寬高。

DecorView

DecorView 作為頂級 View荆烈,通常內(nèi)部會包含一個豎直方向的 LinearLayout拯勉,這個 LinearLayout 分為兩個部分(與 Android 版本以及主題相關(guān))竟趾,上面為標題欄,下面為內(nèi)容欄宫峦。在 Activity 的 setContentView 方法中所設(shè)置的布局文件就是加在內(nèi)容欄中岔帽。

DecorView 結(jié)構(gòu)

可以通過如下代碼得到內(nèi)容欄

ViewGroup content = (ViewGroup) findViewById(android.R.id.content);

其中,DecorView 是一個 FrameLayout斗遏。

理解 MeasureSpec

MeasureSpec 在與父容器的共同作用下決定了一個 View 的尺寸山卦。在 Measure 的過程中,系統(tǒng)會將 View 的 LayoutParams 根據(jù)父容器的規(guī)則轉(zhuǎn)換成對應的 MeasureSpec诵次,然后再根據(jù)這個 MeasureSpec 來測量出 View 的寬高账蓉。

MeasureSpec

MeasureSpec 代表一個 32 位的 int 值。其中逾一,高 2 位代表 SpecMode铸本,低 30 位代表 SpecSize。SpecMode 指測量模式遵堵,SpecSize 指在某種測量模式下的大小箱玷。

其中,MeasureSpec 提供了打包和解包的方法

public static int makeMeasureSpec(int size, int mode) {
    if (sUseBrokenMakeMeasureSpec) {
        return size + mode;
    } else {
        return (size & ~MODE_MASK) | (mode & MODE_MASK);
    }
}

public static int getMode(int measureSpec) {
    return (measureSpec & MODE_MASK);
}

public static int getSize(int measureSpec) {
    return (measureSpec & ~MODE_MASK);
}

SpecMode 有三類:

  • UNSPECIFIED
  • EXACTLY
  • AT_MOST

在 UNSPECIFIED 中陌宿,父容器不對 View 限制锡足,要多大給多大。一般存在于系統(tǒng)內(nèi)部壳坪,表示測量的狀態(tài)舶得。

在 EXACTLY 中,父容器已經(jīng)檢測出 View 所需要的精確大小爽蝴,這時 View 的最終大小就是 SpecSize 所指定的值沐批。對應于 LayoutParams 的 match_parent 和具體的數(shù)值。

在 AT_MOST 中蝎亚,父容器指定了可用大小 SpecSize九孩,View 的大小不能超過這個值。對應于 LayoutParams 的 wrap_content发框。

MeasureSpec 和 LayoutParams 的對應關(guān)系

對于 DecorView躺彬,MeasureSpec 由窗口尺寸和自身 LayoutParams 來決定。對于普通 View梅惯,MeasureSpec 由父容器的 MeasureSpec 和自身 LayoutParams 決定宪拥。MeasureSpec 一旦確定,onMeasure 中就可以確定 View 的寬高个唧。

在 ViewRootImpl 中的 measureHierarchy 方法中有一個 getRootMeasureSpec 方法

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
        case ViewGroup.LayoutParams.MATCH_PARENT:
        // Window can't resize. Force root View to be windowSize.
        break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
        // Window can resize. Set max size for root view.
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
        break;
        default:
        // Window wants to be an exact size. Force root view to be that size.
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
        break;
    }
    return measureSpec;
}

由上面的代碼可以得出 DecorView 中 MeasureSpec 對應于 LayoutParams 的創(chuàng)建規(guī)則:

  • LayoutParams.MATCH_PARENT:EXACTLY江解,大小為窗口大小
  • LayoutParams.WRAP_CONTENT:AT_MOST,大小不定徙歼,但不能超過窗口
  • 固定大欣绾印:EXACTLY鳖枕,為 LayoutParams 中指定大小

普通 View 中,Measure 過程由 ViewGroup 傳遞而來桨螺,于是找到 ViewGroup 中的 measureChildWithMargins 方法

protected void measureChildWithMargins(View child,
    int parentWidthMeasureSpec, int widthUsed,
    int parentHeightMeasureSpec, int heightUsed) {
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
        mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
        + widthUsed, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
        mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
        + heightUsed, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

可以看到宾符,在調(diào)用子元素的 measure 方法之前會調(diào)用 getChildMeasureSpec 方法來得到子元素的 MeasureSpec。其中灭翔,子元素的 MeasureSpec 與父容器的 MeasureSpec魏烫、子元素本身的 LayoutParams,以及 View 的 margin 和 padding 有關(guān)肝箱。

接著看 ViewGroup 的 getChildMeasureSpec 方法

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);
    
    int size = Math.max(0, specSize - padding);
    
    int resultSize = 0;
    int resultMode = 0;
    
    switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
    }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

可以看到哄褒,getChildMeasureSpec 方法主要作用是根據(jù)父容器的 MeasureSpec 并結(jié)合 View 本身的 LayoutParams 來確定子元素的 MeasureSpec央拖。

歸納后得到下表

普通 View 的 MeasureSpec 創(chuàng)建規(guī)則

可以看到门驾,在第一行,即 View 采用固定寬高時焚虱,不管父容器的 MeasureSpec 是什么骏融,View 都是精確模式且大小遵循 LayoutParams 中的大小链嘀。當 View 的寬高為 match_parent 時,View 的 MeasureSpec 模式隨著父容器的模式改變档玻,但大小都為父容器的剩余空間怀泊。當 View 為 wrap_content 時,View 總為最大模式且大小為父容器剩余空間误趴。UNSPECIFIED 主要用于系統(tǒng)內(nèi)部 Measure 情況霹琼,這里暫不關(guān)注。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末冤留,一起剝皮案震驚了整個濱河市碧囊,隨后出現(xiàn)的幾起案子树灶,更是在濱河造成了極大的恐慌纤怒,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件天通,死亡現(xiàn)場離奇詭異泊窘,居然都是意外死亡,警方通過查閱死者的電腦和手機像寒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進店門烘豹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人诺祸,你說我怎么就攤上這事携悯。” “怎么了筷笨?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵憔鬼,是天一觀的道長龟劲。 經(jīng)常有香客問我,道長轴或,這世上最難降的妖魔是什么昌跌? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮照雁,結(jié)果婚禮上蚕愤,老公的妹妹穿的比我還像新娘。我一直安慰自己饺蚊,他們只是感情好萍诱,可當我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著污呼,像睡著了一般砂沛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上曙求,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天碍庵,我揣著相機與錄音,去河邊找鬼悟狱。 笑死静浴,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的挤渐。 我是一名探鬼主播苹享,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼浴麻!你這毒婦竟也來了得问?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤软免,失蹤者是張志新(化名)和其女友劉穎宫纬,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體膏萧,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡漓骚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了榛泛。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蝌蹂。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖曹锨,靈堂內(nèi)的尸體忽然破棺而出孤个,到底是詐尸還是另有隱情,我是刑警寧澤沛简,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布齐鲤,位于F島的核電站硅急,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏佳遂。R本人自食惡果不足惜营袜,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望丑罪。 院中可真熱鬧荚板,春花似錦、人聲如沸吩屹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽煤搜。三九已至免绿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間擦盾,已是汗流浹背嘲驾。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留迹卢,地道東北人辽故。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像腐碱,于是被迫代替她去往敵國和親誊垢。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,086評論 2 355

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