Android圖形系統(tǒng)(三)-View繪制流程

接上篇 繪制優(yōu)化-原理篇2-DecorView布局加載流程 講到的ViewRootImpl衷敌,在ViewRootImpl的setView()方法里主要做兩件事:
1.執(zhí)行requestLayout()方法完成view的繪制流程
2.通過WindowSession將View和InputChannel添加到WmS中草冈,從而將View添加到Window上并且接收觸摸事件乃摹。

2的部分 window加載視圖已經(jīng)介紹了术奖,那么今天就來講講1的部分:執(zhí)行requestLayout()方法完成view的繪制流程

//ViewRootImpl
  public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
                 //在 Window add之前調(diào)用现柠,確保 UI 布局繪制完成 --> measure , layout , draw
                requestLayout();//View的繪制流程
                ...
                    //通過WindowSession進(jìn)行IPC調(diào)用裆赵,將View添加到Window上
                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mInputChannel);
                }
                ...
    }

一逢并、從requestLayout開始

從requestLayout代碼一層層往下追(具體源碼不貼了,非常簡單)郭卫,最終確認(rèn)view的繪制流程是從performTraversals開始筒狠。順一下整個流程:

1.1 performTraversals
private void performTraversals() {
     //獲得view寬高的測量規(guī)格,mWidth和mHeight表示窗口的寬高箱沦,lp.width和lp.height表示DecorView根布局寬和高
     WindowManager.LayoutParams lp = mWindowAttributes;
     ...
        //頂層視圖DecorView所需要窗口的寬度和高度
        int desiredWindowWidth;
        int desiredWindowHeight;
     ...
        //在構(gòu)造方法中mFirst已經(jīng)設(shè)置為true,表示是否是第一次繪制DecorView
        if (mFirst) {
            mFullRedrawNeeded = true;
            mLayoutRequested = true;
            //如果窗口的類型是有狀態(tài)欄的雇庙,那么頂層視圖DecorView所需要窗口的寬度和高度就是除了狀態(tài)欄
            if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL
                    || lp.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD) {
                // NOTE -- system code, won't try to do compat mode.
                Point size = new Point();
                mDisplay.getRealSize(size);
                desiredWindowWidth = size.x;
                desiredWindowHeight = size.y;
            } else {//否則頂層視圖DecorView所需要窗口的寬度和高度就是整個屏幕的寬高
                DisplayMetrics packageMetrics =
                    mView.getContext().getResources().getDisplayMetrics();
                desiredWindowWidth = packageMetrics.widthPixels;
                desiredWindowHeight = packageMetrics.heightPixels;
            }
    }
    ...
     int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
     int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
     ...
     // Ask host how big it wants to be
     //執(zhí)行測量操作
     performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
      ...
     //執(zhí)行布局操作
    performLayout(lp, mWidth, mHeight);
     ...
     //執(zhí)行繪制操作
     performDraw();
}

performTraversals()中做了非常多的處理谓形,代碼接近800行,這里我們重點(diǎn)關(guān)注繪制相關(guān)流程疆前。

1.2 MeasureSpec

在分析繪制過程之前寒跳,我們需要先了解MeasureSpec,它是干什么的呢竹椒?簡而言之童太,MeasureSpec 是View的尺寸一種封裝手段。

MeasureSpec代表一個32位int值胸完,高2位代表SpecMode,低30位代表SepcSize. 這樣的打包方式好處是避免過多的對象內(nèi)存分配书释。為了方便操作,其提供了打包和解包的方法:

public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;
        public static final int EXACTLY     = 1 << MODE_SHIFT;
        public static final int AT_MOST     = 2 << MODE_SHIFT;
...
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                  @MeasureSpecMode int mode) {
    if (sUseBrokenMakeMeasureSpec) {
        return size + mode;
    } else {
        return (size & ~MODE_MASK) | (mode & MODE_MASK);
    }
}
...
public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK); //高2位運(yùn)算
        }
public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);//低30位運(yùn)算
        }
}

getMode方法中ModeMask 為 0x3 << 30 轉(zhuǎn)換成二進(jìn)制為 0011 << 30 赊窥,也就是向左移動30位 則 ModeMask高兩位為1爆惧,低三十位為0,整形measureSpec 為32位锨能, measureSpec & mode_mask 就是高2位的運(yùn)算扯再,getSize方法中 ~Mode_MASK 則是除了高兩位為0 外剩下的低30位均為 1,那么和measure 進(jìn)行 & 運(yùn)算就是在求得低30為中存儲的值址遇。

SpecMode:測量模式

模式 描述
UNSPECIFIED 父容器不作限制熄阻,一般用于系統(tǒng)內(nèi)部
EXACTLY 精確模式,大小為SpecSize倔约,對應(yīng)LayoutParams中的match_parent或者具體數(shù)值
AT_MOST 最大模式秃殉,大小不能大于SpecSize,對應(yīng)于LayoutParams中的warp_content

SpecSize:對應(yīng)某種測量模式下的尺寸大小

下面針對DecorView和普通View分別來看看其MeasureSpec的組成:

//ViewRootImpl
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
 DecorView, 其MeasureSpec由窗口尺寸和其自身LayoutParams共同決定跺株,
}

//ViewGroup
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
   普通View复濒,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同決定。
}

對普通View的MeasureSpec的創(chuàng)建規(guī)則進(jìn)行總結(jié):

圖片摘自Android開發(fā)藝術(shù)探索

這個表怎么用呢乒省?舉個例子:
如果View在布局中使用wrap_content,那么它的specMode是AT_MOST,這種模式下巧颈,它的寬高為specSize, 而查表可得View的specSize是parentSize,而parentSize是當(dāng)前父容器剩余空間大小袖扛,這種效果和在布局中使用match_parent完全一致砸泛,所以如果是對尺寸有具體要求的自定義控件需要指定specSize大小十籍。

注:
LayoutParams類是用于子視圖向父視圖傳達(dá)自己尺寸意愿的一個參數(shù)包,包含了Layout的高唇礁、寬信息勾栗。LayoutParams在LayoutInflater.inflater過程中與View一起被解析成對象,保存在WindowManagerGlobal集合中盏筐。

二围俘、View繪制流程

performTraversals里面執(zhí)行了三個方法,分別是performMeasure()琢融、performLayout()界牡、performDraw()這三個方法,這三個方法分別完成DecorView的measure漾抬、layout宿亡、和draw這三大流程,其中performMeasure()中會調(diào)用measure()方法纳令,在measure()方法中又會調(diào)用onMeasure()方法挽荠,在onMeasure()方法中會對所有子元素進(jìn)行measure過程,這個時候measure流程就從父容器傳遞到子元素中了平绩,這樣就完成了一次measure過程圈匆。接著子元素會重復(fù)父容器的measure過程,如此反復(fù)就實(shí)現(xiàn)了從DecorView開始對整個View樹的遍歷測量捏雌,measure過程就這樣完成了臭脓。同理,performLayout()和performDraw()也是類似的傳遞流程腹忽。針對performTraveals()的大致流程来累,可以用以下流程圖來表示:

from:http://www.reibang.com/p/4a68f9dc8f7c

以上的流程圖只是一個為了便于理解而簡化版的流程,真正的流程應(yīng)該分為以下五個工作階段:

  • 預(yù)測量階段:這是進(jìn)入performTraversals()方法后的第一個階段窘奏,它會對View樹進(jìn)行第一次測量嘹锁。在此階段中將會計算出View樹為顯示其內(nèi)容所需的尺寸,即期望的窗口尺寸着裹。(調(diào)用measureHierarchy())

  • 窗口布局階段:根據(jù)預(yù)測量的結(jié)果领猾,通過IWindowSession.relayout()方法向WMS請求調(diào)整窗口的尺寸等屬性,這將引發(fā)WMS對窗口進(jìn)行重新布局骇扇,并將布局結(jié)果返回給ViewRootImpl摔竿。(調(diào)用relayoutWindow())

  • 測量階段:預(yù)測量的結(jié)果是View樹所期望的窗口尺寸。然而由于在WMS中影響窗口布局的因素很多少孝,WMS不一定會將窗口準(zhǔn)確地布局為View樹所要求的尺寸继低,而迫于WMS作為系統(tǒng)服務(wù)的強(qiáng)勢地位,View樹不得不接受WMS的布局結(jié)果稍走。因此在這一階段袁翁,performTraversals()將以窗口的實(shí)際尺寸對View樹進(jìn)行最終測量柴底。(調(diào)用performMeasure())

  • 布局階段:完成最終測量之后便可以對View樹進(jìn)行布局了。(調(diào)用performLayout())

  • 繪制階段:這是performTraversals()的最終階段粱胜。確定了控件的位置與尺寸后柄驻,便可以對View樹進(jìn)行繪制了。(調(diào)用performDraw())

下面分別來闡述:

2.1 預(yù)測量階段(performTraversals())

這個階段在performTraversals中最先發(fā)生焙压,對View樹進(jìn)行第一次測量鸿脓,會判斷當(dāng)前期望窗口尺寸是否能滿足布局要求。

private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
            final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
        int childWidthMeasureSpec;
        int childHeightMeasureSpec;
        // 表示測量結(jié)果是否可能導(dǎo)致窗口的尺寸發(fā)生變化
        boolean windowSizeMayChange = false;
        //goodMeasure表示了測量是否能滿足View樹充分顯示內(nèi)容的要求
        boolean goodMeasure = false;
        //測量協(xié)商僅發(fā)生在LayoutParams.width被指定為WRAP_CONTENT的情況下
        if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
            //第一次協(xié)商涯曲。measureHierarchy()使用它最期望的寬度限制進(jìn)行測量答憔。
            //這一寬度限制定義為一個系統(tǒng)資源。
            //可以在frameworks/base/core/res/res/values/config.xml找到它的定義
            final DisplayMetrics packageMetrics = res.getDisplayMetrics();
            res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);
            // 寬度限制被存放在baseSize中
            int baseSize = 0;
            if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
                baseSize = (int)mTmpValue.getDimension(packageMetrics);
            }
            if (baseSize != 0 && desiredWindowWidth > baseSize) {
                childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
                childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
                //第一次測量。調(diào)用performMeasure()進(jìn)行測量
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                //View樹的測量結(jié)果可以通過mView的getmeasuredWidthAndState()方法獲取。
                //View樹對這個測量結(jié)果不滿意丈冬,則會在返回值中添加MEASURED_STATE_TOO_SMALL位
                if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                    goodMeasure = true;  // 控件樹對測量結(jié)果滿意回官,測量完成
                } else {
                  //第二次協(xié)商。上次的測量結(jié)果表明View樹認(rèn)為measureHierarchy()給予的寬度太小城榛,在此
                  //在此適當(dāng)?shù)胤艑拰挾鹊南拗凭纠褂米畲髮挾扰c期望寬度的中間值作為寬度限制
                    baseSize = (baseSize+desiredWindowWidth)/2;
                    childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
                  //第二次測量
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                  // 再次檢查控件樹是否滿足此次測量
                    if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                     // 控件樹對測量結(jié)果滿意,測量完成
                        goodMeasure = true;
                    }
                }
            }
        }
        if (!goodMeasure) {
          //最終測量狠持。當(dāng)View樹對上述兩次協(xié)商的結(jié)果都不滿意時疟位,measureHierarchy()放棄所有限制
          //做最終測量。這一次將不再檢查控件樹是否滿意了喘垂,因?yàn)榧幢闫洳粷M意甜刻,measurehierarchy()也沒
          //有更多的空間供其使用了
            childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
          //如果測量結(jié)果與ViewRootImpl中當(dāng)前的窗口尺寸不一致,則表明隨后可能有必要進(jìn)行窗口尺寸的調(diào)整
            if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
                windowSizeMayChange = true;
            }
        }
        // 返回窗口尺寸是否可能需要發(fā)生變化
        return windowSizeMayChange;
    }

measureHierarchy()方法最終也是調(diào)用了performMeasure()方法對View樹進(jìn)行測量正勒,只是多了協(xié)商測量的過程得院。

2.2 窗口布局階段(relayoutWindow())

調(diào)用relayoutWindow()來請求WindowManagerService服務(wù)計算Activity窗口的大小以及過掃描區(qū)域邊襯大小和可見區(qū)域邊襯大小。計算完畢之后章贞,Activity窗口的大小就會保存在成員變量mWinFrame中祥绞,而Activity窗口的內(nèi)容區(qū)域邊襯大小和可見區(qū)域邊襯大小分別保存在ViewRoot類的成員變量mPendingOverscanInsets和mPendingVisibleInsets中。

這部分不在這細(xì)講了鸭限,有耐心的可以把老羅的文章看完:Android窗口管理服務(wù)WindowManagerService計算Activity窗口大小的過程分析

2.3 測量過程(performMeasure())

WMS的布局結(jié)果已經(jīng)確定了蜕径,不管是否滿意都得開始終極布局過程了,下面介紹下measure:
measure是對View進(jìn)程測量败京,確定各View的尺寸的過程,這個過程分View和ViewGroup兩種情況來看兜喻,對于View,通過measure完成自身的測量就行了赡麦,而ViewGroup除了完成自身的測量外虹统,還需要遍歷去調(diào)用所有子view的measure方法弓坞,各個子view遞歸去執(zhí)行這個過程。

那么先從performMeasure開始:

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    try {
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

這里的mView是 ViewRootImpl setView傳進(jìn)來的rootView.
接下來我們看下View的measure()方法

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
     ...
    //根據(jù)widthMeasureSpec和heightMeasureSpec計算key值车荔,在下面用key值作為鍵渡冻,緩存我們測量得到的結(jié)果
    long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
    if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
    …
    //forceLayout 是通過上次的mPrivateFlags標(biāo)記位來判斷這次是否需要觸發(fā)重繪
    //View中有個forceLayout()方法可以設(shè)置mPrivateFlags.
   // needsLayout 簡單看就是spec發(fā)生了某些規(guī)則約束下的變化
    if (forceLayout || needsLayout) {
        // first clears the measured dimension flag
        mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
        resolveRtlPropertiesIfNeeded();
        //在View真正進(jìn)行測量之前,View還想進(jìn)一步確認(rèn)能不能從已有的緩存mMeasureCache中讀取緩存過的測量結(jié)果 //如果是強(qiáng)制layout導(dǎo)致的測量忧便,那么將cacheIndex設(shè)置為-1族吻,即不從緩存中讀取測量結(jié)果 //如果不是強(qiáng)制layout導(dǎo)致的測量,那么我們就用上面根據(jù)measureSpec計算出來的key值作為緩存索引cacheIndex,這時候有可能找到相應(yīng)的值,找到就返回對應(yīng)索引;也可能找不到,找不到就返回-1
        int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            //在緩存中找不到相應(yīng)的值或者需要忽略緩存結(jié)果的時候,重新測量一次 //此處調(diào)用onMeasure方法珠增,并把尺寸限 制條件widthMeasureSpec和heightMeasureSpec傳入進(jìn)去 //onMeasure方法中將會進(jìn)行實(shí)際的測量工作超歌,并把測量的結(jié)果保存到成員變量中
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        } else {
            //如果運(yùn)行到此處,那么表示當(dāng)前的條件允許View從緩存成員變量mMeasureCache中讀取測量過的結(jié)果
           //用上面得到的cacheIndex從緩存mMeasureCache中取出值蒂教,不必在調(diào)用onMeasure方法進(jìn)行測量了
            long value = mMeasureCache.valueAt(cacheIndex);
          //一旦我們從緩存中讀到值巍举,我們就可以調(diào)用setMeasuredDimensionRaw方法將當(dāng)前測量的結(jié)果保存到成員變量中        
            setMeasuredDimensionRaw((int) (value >> 32), (int) value);
            mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }
          ...
        }
      ...
    }
    ...
}

先判斷一下是否有必要進(jìn)行測量操作,如果有,先看是否能在緩存mMeasureCache中找到上次的測量結(jié)果,如果找到了那直接從緩存中獲取就可以了凝垛,如果找不到懊悯,那么乖乖地調(diào)用onMeasure()方法去完成實(shí)際的測量工作,并且將尺寸限制條件widthMeasureSpec和heightMeasureSpec傳遞給onMeasure()方法梦皮。

另外炭分,measure()這個方法是final的,因此我們無法在子類中去重寫這個方法剑肯,說明Android是不允許我們改變View的measure框架. 主要看onMeasure()方法捧毛,這里才是真正去測量并設(shè)置View大小的地方。

2.3.1 View的measure過程:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

setMeasuredDimension方法會設(shè)置View寬高的測量值让网,因此主要看下getDefaultSize方法:

public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);
    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

很顯然看出:
AT_MOST 和 EXACTLY兩種情況返回的就是specSize呀忧。

UNSPECIFIED返回的是size, 即getSuggestedMinimumWidth 和 getSuggestedMinimumHeight的返回值。

protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

mMinWidth 對應(yīng)于android:minWidth屬性指定的值溃睹〖雠埃總結(jié):如果View沒有設(shè)置背景,那么返回mMinWidth的值丸凭,否則返回mMinWidth和背景最小寬度的最大值福扬。

2.3.2 ViewgGroup的measure過程:

ViewGroup 繼承自 View,我們知道View的 measure是 final方法惜犀,那這個方法是肯定會走的铛碑,但是具體實(shí)現(xiàn)是在onMeasure中,ViewGroup提供了幾個方法來幫助ViewGroup的子類來實(shí)現(xiàn)onMeasure邏輯虽界,包括:

protected void measureChild(View child, int parentWidthMeasureSpec,
       int parentHeightMeasureSpec) {
   final LayoutParams lp = child.getLayoutParams();
   final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
           mPaddingLeft + mPaddingRight, lp.width);
   final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
           mPaddingTop + mPaddingBottom, lp.height);
   child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

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);
}

仔細(xì)看其實(shí)最終還是讓child去執(zhí)行自己對于的measure汽烦,只是getChildMeasureSpec有差別,這里加上了margin 和 padding.

具體onMeasure的實(shí)現(xiàn)可以參考LinearLayout莉御、FrameLayout撇吞、RelativeLayout等俗冻。

另外需要關(guān)注的是ViewGroup 的 getChildMeasureSpec方法,我們從上面代碼中很明顯看出牍颈,傳入的Spec是父容器的measureSpec

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 = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
           resultMode = MeasureSpec.UNSPECIFIED;
       } else if (childDimension == LayoutParams.WRAP_CONTENT) {
           // Child wants to determine its own size.... find out how
           // big it should be
           resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
           resultMode = MeasureSpec.UNSPECIFIED;
       }
       break;
   }
   //noinspection ResourceType
   return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

很明顯看出來迄薄,對于普通View來說,getChildMeasureSpec的邏輯是通過其父View提供的MeasureSpec參數(shù)得到specMode和specSize煮岁,然后根據(jù)計算出來的specMode以及子View的childDimension(layout_width或layout_height)來計算自身的measureSpec 讥蔽。

measure總結(jié):

  • MeasureSpec 由specMode和specSize組成:
    DecorView, 其MeasureSpec由窗口尺寸和其自身LayoutParams共同決定。
    普通View画机,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同決定冶伞。

  • View的measure方法是final的,不允許重載步氏,View子類只能重載onMeasure來完成自己的測量邏輯响禽。

  • ViewGroup類提供了measureChild,measureChild和measureChildWithMargins方法荚醒,以及getChildMeasureSpec方法 芋类,供具體實(shí)現(xiàn)ViewGroup的子類重寫onMeasure的時候方便使用。

  • 只要是ViewGroup的子類就必須要求LayoutParams繼承子MarginLayoutParams腌且,否則無法使用layout_margin參數(shù)。

  • 使用View的getMeasuredWidth()和getMeasuredHeight()方法來獲取View測量的寬高榛瓮,必須保證這兩個方法在onMeasure流程之后被調(diào)用才能返回有效值铺董。
    比較常用的方式:
    view.post(runnable)
    view.measure(0,0)之后 get

measure整體執(zhí)行流程:

form: 工匠若水
2.4 布局過程 (performLayout())

Layout的作用是ViewGroup用來確定子view的位置,當(dāng)ViewGroup的位置被確定之后禀晓,它在onLayout中會遍歷所有子view并調(diào)用其layout方法精续,在layout方法中onLayout又被調(diào)用。

先從performLayout看起:

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
           int desiredWindowHeight) {
       ...
       //標(biāo)記當(dāng)前開始布局
       mInLayout = true;
       //mView就是DecorView
       final View host = mView;
       ...
       //DecorView請求布局 layout參數(shù)分別是 左 上 右 下
       host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
       //標(biāo)記布局結(jié)束
       mInLayout = false;
       ...
}

跟蹤代碼進(jìn)入View類的layout方法

public void layout(int l, int t, int r, int b) {
       //判斷是否需要重新測量
       if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
           onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
           mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
       }
       //保存上一次View的四個位置
       int oldL = mLeft;
       int oldT = mTop;
       int oldB = mBottom;
       int oldR = mRight;
       //設(shè)置當(dāng)前視圖View的左粹懒,頂重付,右,底的位置凫乖,并且判斷布局是否有改變
       boolean changed = isLayoutModeOptical(mParent) ?
               setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
       //如果布局有改變确垫,條件成立,則視圖View重新布局
           if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
           //調(diào)用onLayout帽芽,將具體布局邏輯留給子類實(shí)現(xiàn)
           onLayout(changed, l, t, r, b);
           mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
           ListenerInfo li = mListenerInfo;
           if (li != null && li.mOnLayoutChangeListeners != null) {
               ArrayList<OnLayoutChangeListener> listenersCopy =
                       (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
               int numListeners = listenersCopy.size();
               for (int i = 0; i < numListeners; ++i) {
                   listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
               }
           }
       }
       mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
       mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
   }

首先關(guān)注下需要重新layout的條件:

boolean changed = isLayoutModeOptical(mParent) ?
               setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

其中setOpticalFrame內(nèi)部也會調(diào)用setFrame删掀,所以就看下setFrame好了:

protected boolean setFrame(int left, int top, int right, int bottom) {
    boolean changed = false;
    ...
    if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
        changed = true;
        // Remember our drawn bit
        int drawn = mPrivateFlags & PFLAG_DRAWN;
        int oldWidth = mRight - mLeft;
        int oldHeight = mBottom - mTop;
        int newWidth = right - left;
        int newHeight = bottom - top;
        boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
        // Invalidate our old position
        invalidate(sizeChanged);
        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
        mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
       ...
    }
    return changed;
}

通過setFrame方法設(shè)定View的四個頂點(diǎn)的位置,并更新本地值导街,同時判斷頂點(diǎn)位置較之前是否有變化披泪,并return是否有變化的boolean值,如果有變化還會執(zhí)行invalidate(sizeChanged)搬瑰。

然后款票,咱們再看看onLayout方法:
View中的onLayout是個空方法控硼,可實(shí)現(xiàn)可不實(shí)現(xiàn)

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

ViewGroup中是個抽象方法,子類必須實(shí)現(xiàn)

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

下面以RelativeLayout為例艾少,對onLayout具體實(shí)現(xiàn)做簡單的分析:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
   //  The layout has actually already been performed and the positions
   //  cached.  Apply the cached values to the children.
   final int count = getChildCount();
   for (int i = 0; i < count; i++) {
       View child = getChildAt(i);
       if (child.getVisibility() != GONE) {//只有不為GONE的才會執(zhí)行布局
           RelativeLayout.LayoutParams st =
                   (RelativeLayout.LayoutParams) child.getLayoutParams();
           child.layout(st.mLeft, st.mTop, st.mRight, st.mBottom);
       }
   }
}

遍歷所有子view卡乾,并通過其LayoutParams 獲取四個方向的位置值,將位置信息傳入子view的layout方法進(jìn)行布局姆钉。

layout總結(jié):

  • Layout的作用是ViewGroup用來確定子view的位置, 是ViewGroup需要干的活说订,View不需要,所以View中是空方法潮瓶,而ViewGroup中是抽象方法陶冷,但是View你也可以重寫,大多數(shù)是利用這個生命周期階段加寫邏輯操作毯辅。

  • 當(dāng)我們的視圖View在布局中使用 android:visibility=”gone” 屬性時埂伦,是不占據(jù)屏幕空間的,因?yàn)樵诓季謺rViewGroup會遍歷每個子視圖View思恐,判斷當(dāng)前子視圖View是否設(shè)置了 Visibility==GONE,如果設(shè)置了沾谜,當(dāng)前子視圖View就不會添加到父容器上,因此也就不占據(jù)屏幕空間胀莹。具體可以參考RelativeLayout的onLayout.

  • 必須在View布局完之后調(diào)用getHeight( )和getWidth( )方法獲取到的View的寬高才大于0.

layout的整體執(zhí)行流程:

from:工匠若水
2.5 繪制過程 (performDraw())

Draw作用是將View繪制到屏幕上.過程相對比較簡單基跑。
draw是從performDraw開始

//ViewRootImpl
private void performDraw() {
       ...
       draw(fullRedrawNeeded);
       ...
}

然后看ViewRootImpl的draw方法:

 //ViewRootImpl
   private void draw(boolean fullRedrawNeeded) {
        Surface surface = mSurface;
     ...
                if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
                    return;
                }
     ...
    }

再看ViewRootImpl的drawSoftware方法:

 //ViewRootImpl
  private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) {
...
 mView.draw(canvas);
...
}

最終在drawSoftware方法中,會走到View的draw并傳入了canvas畫布描焰。這部分先不細(xì)說媳否,之后的Surface部分會分析。

那么接著往下的話就是真正View繪制的部分了:

   //View
   public void draw(Canvas canvas) {
       ......
       /*
        * Draw traversal performs several drawing steps which must be executed
        * in the appropriate order:
        *
        *      1\. Draw the background
        *      2\. If necessary, save the canvas' layers to prepare for fading
        *      3\. Draw view's content
        *      4\. Draw children
        *      5\. If necessary, draw the fading edges and restore layers
        *      6\. Draw decorations (scrollbars for instance)
        */
       // Step 1, draw the background, if needed
       ......
       if (!dirtyOpaque) {
           drawBackground(canvas);
       }
       // skip step 2 & 5 if possible (common case)
       ......
       // Step 2, save the canvas' layers
       ......
           if (drawTop) {
               canvas.saveLayer(left, top, right, top + length, null, flags);
           }
       ......
       // Step 3, draw the content
       if (!dirtyOpaque) onDraw(canvas);
       // Step 4, draw the children
       dispatchDraw(canvas);
       // Step 5, draw the fade effect and restore layers
       ......
       if (drawTop) {
           matrix.setScale(1, fadeHeight * topFadeStrength);
           matrix.postTranslate(left, top);
           fade.setLocalMatrix(matrix);
           p.setShader(fade);
           canvas.drawRect(left, top, right, top + length, p);
       }
       ......
       // Step 6, draw decorations (scrollbars)
       onDrawScrollBars(canvas);
       ......
   }

從摘要可以看出荆秦,繪制過程分如下幾步:

  • 繪制背景 background.draw(canvas)
   private void drawBackground(Canvas canvas) {
       //獲取xml中通過android:background屬性或者代碼中setBackgroundColor()篱竭、setBackgroundResource()等方法進(jìn)行賦值的背景Drawable
       final Drawable background = mBackground;
       ......
       //根據(jù)layout過程確定的View位置來設(shè)置背景的繪制區(qū)域
       if (mBackgroundSizeChanged) {
           background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);
           mBackgroundSizeChanged = false;
           rebuildOutline();
       }
       ......
           //調(diào)用Drawable的draw()方法來完成背景的繪制工作
           background.draw(canvas);
       ......
   }
  • 繪制自己(onDraw)
    View中onDraw是一個空方法,ViewGroup也沒有重新實(shí)現(xiàn)步绸。
protected void onDraw(Canvas canvas) {
}
  • 繪制children(dispatchDraw)
    View的dispatchDraw()方法是一個空方法掺逼,而且注釋說明了如果View包含子類需要重寫他。所以ViewGroup肯定重寫了瓤介,來看看:
   @Override
    protected void dispatchDraw(Canvas canvas) {
        ......
        final int childrenCount = mChildrenCount;
        final View[] children = mChildren;
        ......
        for (int i = 0; i < childrenCount; i++) {
            ......
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                more |= drawChild(canvas, child, drawingTime);
            }
        }
        ......
        // Draw any disappearing views that have animations
        if (mDisappearingChildren != null) {
            ......
            for (int i = disappearingCount; i >= 0; i--) {
                ......
                more |= drawChild(canvas, child, drawingTime);
            }
        }
        ......
    }

可以看見吕喘,ViewGroup確實(shí)重寫了View的dispatchDraw()方法,該方法內(nèi)部會遍歷每個子View刑桑,然后調(diào)用drawChild()方法兽泄,我們可以看下ViewGroup的drawChild方法,如下:

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    return child.draw(canvas, this, drawingTime);
}
  • 繪制裝飾(onDrawScrollBars)

可以看見其實(shí)任何一個View都是有(水平垂直)滾動條的漾月,只是一般情況下沒讓它顯示而已病梢。這部分不做詳細(xì)分析了。

draw拓展點(diǎn):
1)View的setWillNotDraw方法:
如果一個View不需要繪制任何內(nèi)容,那么設(shè)置這個標(biāo)記位為true后蜓陌,系統(tǒng)會進(jìn)行相應(yīng)的優(yōu)化觅彰,如果需要通過onDraw繪制內(nèi)容,則需要設(shè)置為false钮热。(默認(rèn)View是false ViewGroup是true)這是個優(yōu)化手段填抬。

2)區(qū)分View動畫和ViewGroup布局動畫,前者指的是View自身的動畫隧期,可以通過setAnimation添加飒责,后者是專門針對ViewGroup顯示內(nèi)部子視圖時設(shè)置的動畫,可以在xml布局文件中對ViewGroup設(shè)置layoutAnimation屬性(譬如對LinearLayout設(shè)置子View在顯示時出現(xiàn)逐行仆潮、隨機(jī)宏蛉、下等顯示等不同動畫效果)。

3)默認(rèn)情況下子View的ViewGroup.drawChild繪制順序和子View被添加的順序一致性置,但是你也可以重載ViewGroup.getChildDrawingOrder()方法提供不同順序拾并。

draw整體執(zhí)行流程:


from:工匠若水

三、forceLayout 鹏浅、invalidate 嗅义、requestLayout簡述

在之前分析的繪制流程中,我們或多或少都見過這三個方法隐砸,他們到底是干什么的之碗,下面做下簡單說明:

View#forceLayout( )
public void forceLayout() {
    if (mMeasureCache != null) mMeasureCache.clear();
    mPrivateFlags |= PFLAG_FORCE_LAYOUT;
    mPrivateFlags |= PFLAG_INVALIDATED;
}

官方描述:強(qiáng)制此視圖在下一次布局傳遞期間進(jìn)行布局。此方法不調(diào)用父類的requestLayout()或forceLayout()季希。

每個View都有個成員變量:mPrivateFlags褪那,在不同的繪制執(zhí)行路徑會對它賦值。在measure方法內(nèi)部forceLayout用來判斷是否執(zhí)行onMeasure:

final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
View#invalidate( ) 和 View#postInvalidate( )

invalidate和postInvalidate:都是用來重繪View胖眷,區(qū)別就是invalidate只能在主線程中調(diào)用武通,postInvalidate可以在子線程中調(diào)用.

View#requestLayout

requestLayout: 當(dāng)前view及其以上的viewGroup部分都重新走ViewRootImpl 重新繪制 霹崎,分別重新onMeasure onLayout onDraw ,其中onDraw比較特殊珊搀,有內(nèi)容變化才會觸發(fā)。

最后一張圖總結(jié)下invalidate/postInvalidate 和 requestLayout

參考:
https://blog.csdn.net/yanbober/article/details/46128379
https://blog.csdn.net/feiduclear_up/article/details/46772477
http://www.reibang.com/p/4a68f9dc8f7c
http://www.reibang.com/p/a65861e946cb
《Android開發(fā)藝術(shù)探索》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載尾菇,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者境析。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市派诬,隨后出現(xiàn)的幾起案子劳淆,更是在濱河造成了極大的恐慌,老刑警劉巖默赂,帶你破解...
    沈念sama閱讀 219,366評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件沛鸵,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)曲掰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評論 3 395
  • 文/潘曉璐 我一進(jìn)店門疾捍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人栏妖,你說我怎么就攤上這事乱豆。” “怎么了吊趾?”我有些...
    開封第一講書人閱讀 165,689評論 0 356
  • 文/不壞的土叔 我叫張陵宛裕,是天一觀的道長。 經(jīng)常有香客問我论泛,道長揩尸,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,925評論 1 295
  • 正文 為了忘掉前任孵奶,我火速辦了婚禮疲酌,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘了袁。我一直安慰自己朗恳,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評論 6 392
  • 文/花漫 我一把揭開白布载绿。 她就那樣靜靜地躺著粥诫,像睡著了一般。 火紅的嫁衣襯著肌膚如雪崭庸。 梳的紋絲不亂的頭發(fā)上怀浆,一...
    開封第一講書人閱讀 51,727評論 1 305
  • 那天,我揣著相機(jī)與錄音怕享,去河邊找鬼执赡。 笑死,一個胖子當(dāng)著我的面吹牛函筋,可吹牛的內(nèi)容都是我干的沙合。 我是一名探鬼主播,決...
    沈念sama閱讀 40,447評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼跌帐,長吁一口氣:“原來是場噩夢啊……” “哼首懈!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起谨敛,我...
    開封第一講書人閱讀 39,349評論 0 276
  • 序言:老撾萬榮一對情侶失蹤究履,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后脸狸,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體最仑,經(jīng)...
    沈念sama閱讀 45,820評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了泥彤。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片紊搪。...
    茶點(diǎn)故事閱讀 40,127評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖全景,靈堂內(nèi)的尸體忽然破棺而出耀石,到底是詐尸還是另有隱情,我是刑警寧澤爸黄,帶...
    沈念sama閱讀 35,812評論 5 346
  • 正文 年R本政府宣布滞伟,位于F島的核電站,受9級特大地震影響炕贵,放射性物質(zhì)發(fā)生泄漏梆奈。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評論 3 331
  • 文/蒙蒙 一称开、第九天 我趴在偏房一處隱蔽的房頂上張望亩钟。 院中可真熱鬧,春花似錦鳖轰、人聲如沸清酥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽焰轻。三九已至,卻和暖如春昆雀,著一層夾襖步出監(jiān)牢的瞬間辱志,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評論 1 272
  • 我被黑心中介騙來泰國打工狞膘, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留揩懒,地道東北人。 一個月前我還...
    沈念sama閱讀 48,388評論 3 373
  • 正文 我出身青樓挽封,卻偏偏與公主長得像已球,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子场仲,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評論 2 355