View的工作流程

4.3 View的工作流程

View的工作流程主要是指measure希柿、layout集侯、draw這三大流程,即測量试躏、布局和繪制猪勇,其中measure確定View的測量寬/高,layout確定View的最終寬/高和四個頂點的位置颠蕴,而draw則將View繪制到屏幕上泣刹。

4.3.1 measure過程

measure過程要分情況來看,如果只是一個原始的View犀被,那么通過measure方法就完成了其測量過程椅您,如果是一個ViewGroup,除了完成自己的測量過程外寡键,還會遍歷去調(diào)用所有子元素的measure方法掀泳,各個子元素再遞歸去執(zhí)行這個流程,下面針對這兩種情況分別討論。

1. View的measure過程

View的measure過程由其measure方法來完成开伏,measure方法是一個final類型的方法膀跌,這意味著子類不能重寫此方法遭商,在View的measure方法中會去調(diào)用View的onMeasure方法固灵,因此只需要看onMeasure的實現(xiàn)即可,View的onMeasure方法如下所示劫流。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasureDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasure), getDefaultSize(getSuggestedMinimumHeight(). heightMeasureSpec));
}

上述代碼很簡潔巫玻,但是簡潔并不代表簡單,setMeasureDimension方法會設(shè)置View寬/高的測量值祠汇,因此我們只需要看getDefault這個方法即可:

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

可以看出仍秤,getDefaultSize這個地方的邏輯很簡答,對于我們來說可很,我們只需要看AT_MOST和EXACTLY這兩種情況诗力。簡單地理解,其實getDefaultSize返回的大小就是measureSpec中的specSize我抠,而這個specSize就是View測量后的大小苇本,這里多次提到測量后的大小,是因為View最終的大小是在layout階段確定的菜拓,所以這里必須要加以區(qū)分瓣窄,但是幾乎所有情況下View的測量大小和最終大小是相等的。

至于UNSPECIFIED這種情況纳鼎,一般用于系統(tǒng)內(nèi)部的測量過程俺夕,在這種情況下,View的大小為getDefaultSize的第一個參數(shù)size贱鄙,即寬/高分別為getSuggestedMinimumWidth和getSuggestedMinimumHeight這兩個方法的返回值劝贸,看一下他們的源碼:

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

protected int getSuggestedMinimumHeight() {
    return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.gerMinimumHeight())
}

這里只分析getSuggestedMinimumWidth方法的實現(xiàn),getSuggestedMinimumHeight和它的實現(xiàn)原理是一樣的逗宁。從getSuggestedMinimumWidth的代碼可以看出悬荣,如果View沒有設(shè)置背景,那么View的寬度為mMinWidth疙剑,而mMinWidth對應(yīng)于android:minWidth這個屬性所指定的值氯迂,因此View的寬度即為android:minWidth屬性所指定的值。這個屬性如果不指定言缤,那么mMinWidth則默認(rèn)為0嚼蚀;如果View指定了背景,則View的寬度為max(mMinWidth, mBackground.gerMinimumWidth())管挟。mMinWidth的含義我們已經(jīng)知道了轿曙,那么mBackground。getMinimumWidth()是什么呢?我們看一下Drawable的getMinimumWidth方法导帝,如下所示守谓。

public int getMinimumWidth() {
    final int intrinsicWidth = getInstrinsicWidrh();
    return intrinsicWidth > 0 ? intrinsicWidth : 0;
}

可以看出,getMinimumWidth返回的就是Drawable的原始寬度您单,前提是這個Drawable有原始寬度斋荞,否則就返回0。那么Drawable在什么情況下有原始寬度呢虐秦?這里先舉個例子說明一下平酿,ShapeDrawable無原始寬/高,而BitmapDrawable有原始寬/高(圖片的尺寸)悦陋,詳細(xì)內(nèi)容會在第6章進(jìn)行介紹蜈彼。

這里再總結(jié)一下getSuggestedMinimumWidth的邏輯:如果View沒有設(shè)置背景,那么返回android:minWidth這個屬性所指定的值俺驶,這個值可以為0幸逆;如果View設(shè)置了背景,則返android:minWidth和背景的最小寬度這兩者中的最大值暮现,getSuggestedMinimumWidth和getSuggestedMinimumHeight的返回值就是View在UNSPECIFIED情況下的測量寬/高还绘。

從getDefaultSize方法的實現(xiàn)來看,View的寬/高由specSize決定送矩,所以我們可以得出如下結(jié)論:直接繼承View的自定義控件需要重寫onMeasure方法并設(shè)置wrap_content時的自身大小蚕甥,否則在布局中使用wrap_content就相當(dāng)于match_parent。為什么呢栋荸?這個原因需要結(jié)合上述代碼和表4-1才能更好地理解菇怀。從上述代碼中我們知道,如果View在布局中使用wrap_content晌块,那么它的specMode是AT_MOST模式爱沟,在這種模式下,它的寬/高等于specSize匆背;查表4-1可知呼伸,這種情況下View的specSize是parentSize,而parentSize是父容器中目前可以使用的大小钝尸,也就是父容器當(dāng)前剩余的空間大小括享。很顯然,View的寬/高就等于父容器當(dāng)前剩余的空間大小珍促,這種效果和在布局中使用match_parent完全一致铃辖,如何解決這個問題呢?也很簡單猪叙,代碼如下所示娇斩。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSoecSize = MeasureSpec.gerSize(heightMeasureSpec);
    if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(mWidth, mHeight);
    } else if (widthSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(mWidth, heightSpecSize);
    } else if (heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasureDimension(widthSpecSize, mHeight);
    }
}

在上面的代碼中仁卷,我們只需要給View指定一個默認(rèn)的內(nèi)部寬/高(mWidth和mHeight),并在wrap_content時設(shè)置此寬/高即可犬第。對于非wrap_content情形锦积,我們沿用系統(tǒng)的測量值即可,至于這個默認(rèn)的內(nèi)部寬/高的大小如何指定歉嗓,這個沒有固定的依據(jù)丰介,根據(jù)需要靈活指定即可。如果查看TextView遥椿、ImageView等的源碼就可以知道基矮,針對wrap_content情形淆储,他們的onMeasure方法均做了特殊處理冠场,讀者可以自行查看它們的源碼。

2. ViewGroup的measure過程

對于ViewGroup來說本砰,除了完成自己的measure過程以外碴裙,還會遍歷去調(diào)用所有子元素的measure方法,各個子元素再遞歸去執(zhí)行這個過程点额。和View不同的是舔株,ViewGroup是一個抽象類,因此它沒有重寫View的onMeasure方法还棱,但是它提供了一個叫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);
        }
    }
}

從上述代碼來看珍手,ViewGroup在measure時办铡,會對每一個子元素進(jìn)行measure,measureChild這個方法的實現(xiàn)也很好理解琳要,如下所示寡具。

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

很顯然,measureChild的思想就是取出子元素的LayoutParams稚补,然后再通過getChildMeasureSpec來創(chuàng)建子元素的MeasureSpec童叠,接著將MeasureSpec直接傳遞給View的measure方法來進(jìn)行測量。getChildMeasureSpec的工作過程已經(jīng)在上面進(jìn)行了詳細(xì)分析课幕,通過表4-1可以更清楚地了解它的邏輯厦坛。

我們知道ViewGroup并沒有定義其測量的具體過程,這是因為ViewGroup是一個抽象類乍惊,其測量過程的onMeasure方法需要各個子類去具體實現(xiàn)杜秸,比如LinearLayout、RelativeLayout等污桦,為什么ViewGroup不像View一樣對其onMeasure方法做統(tǒng)一的實現(xiàn)呢亩歹?那是因為不同的ViewGroup子類有不同的布局特性匙监,這導(dǎo)致他們的測量細(xì)節(jié)各不相同,比如LinearLayout和RelativeLatyout這兩者的布局特性顯然不同小作,因此ViewGroup無法做統(tǒng)一實現(xiàn)亭姥。下面就通過LinearLayout的onMeasure方法來分析ViewGroup的measure過程,其他Layout類型讀者可以自行分析顾稀。

首先來看LinearLayout的onMeasure方法达罗,如下所示。

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

上述代碼很簡單静秆,我們選一個來看一下粮揉,比如選擇查看豎直布局的LinearLayout的測量過程,即measureVertical方法抚笔,measureVertical的源碼比較長扶认,下面只描述其大概邏輯,首先看一段代碼:

// See how tall everyone is. Also remenber max width.
for (int i = 0; i < count; ++i) {
    final View child = gerVirtualChildAt(i);
    ...
    // Determine how big this child would like to be. If this or
    // previous children have given a weight, then we allow it to
    // use all available space (and we will shrink things later if needed).
    measureChildBeforeLayout(child, i, widthMeasureSpec, 0, heightMeasureSpec, totalWeighr == 0 ? mTotalLength : 0);
    
    if (oldHeight != Integer.MIN_VALUE) {
        lp.height = oldHeight;
    }
    
    final int childHeight = child.getMeasuredHeight();
    final int totalLength = mTotalLength;
    mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}

從上面這段代碼可以看出殊橙,系統(tǒng)會遍歷子元素并對每個子元素執(zhí)行measureChildBeforeLayout方法辐宾,這個方法內(nèi)部會調(diào)用子元素的measure方法,這樣各個子元素就開始依次進(jìn)入measure過程膨蛮,并且系統(tǒng)會通過mTotalLength這個變量來存儲LinearLayout在豎直方向的初步高度。每測量一個子元素敞葛,mTotalLength就會增加惹谐,增加的部分主要包括了子元素的高度以及子元素再豎直方向上的margin等持偏。當(dāng)子元素測量完畢后,LinearLayout會測量自己的大小豺鼻,源碼如下所示。

// 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;
...
setMeasureDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), heightSizeAndState);

這里對上述代碼進(jìn)行說明谬莹,當(dāng)子元素測量完畢后,LinarLayout會根據(jù)子元素的情況來測量自己的大小桩了。針對豎直的LinearLayout而言附帽,他在水平方向的測量過程遵循View的測量過程井誉,在豎直方向的測量過程則和View有所不同颗圣。具體來說是指屁使,如果它的布局中高度采用的是match_parent或者具體數(shù)值奔则,那么它的測量過程和View一致,即高度為specSize酬蹋;如果它的布局中高度采用的是wrap_content抽莱,那么它的高度是所有子元素所占用的高度綜合,但是仍然不能超過它的父容器的剩余空間匕垫,當(dāng)然它的最終高度還需要考慮其在豎直方向的padding璃岳,這個過程可以進(jìn)一步參看如下源碼:

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    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:
            if (specSize < size) {
                result = specSize | MEASURE_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
    }
    return result | (ChildMeasuredState & MEASURED_STATE_MASK);
}

View的measure過程是三大流程中最復(fù)雜的一個铃慷,measure完成以后蜕该,通過getMeasuredWidth/Height方法就可以正確地獲取到View的測量寬/高堂淡。需要注意的是,在某些極端情況下萤悴,系統(tǒng)可能需要多次measure才能確定最終的測量寬/高皆的,在這種情形下,在onMeasure方法中拿到的測量寬/高很可能是不準(zhǔn)確的费薄。一個比較好的習(xí)慣是在onLayout方法中去獲取View的測量寬/高或者最終寬/高楞抡。

上面已經(jīng)對View的measure過程進(jìn)行了詳細(xì)的分析,現(xiàn)在考慮一種情況凳厢,比如我們想在Activity已啟動的時候就做一件任務(wù),但是這一件任務(wù)需要獲取某個View的寬/高先紫。讀者可能會說泡孩,這很簡單啊,在onCreate或者onResume里面去獲取這個View的寬/高不就行了吮播?讀者可以自行試一下眼俊,實際上在onCreate、onStart环戈、onResume中均無法正確得到某個View的寬/高信息澎灸,這是因為View的measure過程和Activity的生命周期方法不是同步執(zhí)行的,因此無法保證Activity執(zhí)行了onCreate拦止、onStart糜颠、onResume時某個View已經(jīng)測量完畢了其兴,如果View還沒有測量完畢,那么獲得的寬/高就是0榴徐。有沒有什么方法能解決這個問題呢法绵?答案是有的朋譬,這里給出四種方法來解決這個問題:

(1)Activity/View#onWindowFocusChanges。

onWindowFocusChanged這個方法的含義是:View已經(jīng)初始化完畢了字柠,寬/高已經(jīng)準(zhǔn)備好了,這個時候去獲取寬/高是沒問題的钦幔。但是要注意的是常柄,onWindowFocusChanged會被調(diào)用多次西潘,當(dāng)Activity的窗口得到焦點和失去焦點時均會被調(diào)用一次。具體來說相种,當(dāng)Activity繼續(xù)執(zhí)行和暫停執(zhí)行時品姓,onWindowFocusChanged均會被調(diào)用,如果頻繁的進(jìn)行onResume和onPause衬潦,那么onWindowFocusChanged也會被頻繁的調(diào)用别渔。典型代碼如下:

public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    if (hasFocus) {
        int width = view.getMeasuredWidth();
        int height = view.getMeasuredHeight();
    }
}

(2)View.post(runnable)惧互。

通過post可以將一個runnable投遞到消息隊列的尾部喊儡,然后等待Looper調(diào)用此runnable的時候稻据,View也已經(jīng)初始化好了。典型代碼如下:

protected void onStart() {
    super.onStart();
    view.post(new Runnable(){
        
        @Override
        public void run() {
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    });
}

(3)ViewTreeObserver匆赃。

使用ViewTreeObserver的眾多回調(diào)可以完成這個功能算柳,比如使用OnGlobalLayoutListener這個接口姓言,當(dāng)View樹的狀態(tài)發(fā)生改變或者View樹內(nèi)部的View的可見性發(fā)現(xiàn)改變時蔗蹋,onGlobalLayout方法將會被回調(diào)猪杭,因此這是獲取View的寬/高一個很好的時機妥衣。需要注意的是税手,伴隨著View樹的狀態(tài)改變等,onGlobalLayout會被調(diào)用多次狂票。典型代碼如下:

protected void onStart() {
    super.onStart();
    
    ViewTreeObserver observer = view.getViewTreeObserver();
    observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
        
        @SuppressWarnings("deprecation")
        @Override
        public void onGlobalLayout() {
            view.gerViewTreeObserver().removeGlobalOnLayoutListener(this);
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    });
}

(4)view.measure(int widthMeasureSpec, int heightMeasureSpec)熙暴。

通過手動對View進(jìn)行measure來得到View的寬/高周霉。這種方法比較復(fù)雜,這里要分情況處理国瓮,根據(jù)View的LayoutParams來分:

match_parent

直接放棄乃摹,無法measure出具體的寬/高跟衅。原因很簡單,根據(jù)View的measure過程掰读,如表4-1所示叭莫,構(gòu)造此種MeasureSpec需要知道parentSize雇初,即父容器的剩余空間,而這個時候我們無法知道parentSize的大小善榛,所以理論上不可能測量出View的大小。

具體的數(shù)值(dp/px)

比如寬/高都是100px悼院,如下measure:

int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);

wrap_content

如下measure:

int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec, heightMeasureSpec);

注意到(1 << 30)- 1据途,通過分析MeasureSpec的實現(xiàn)可以知道叙甸,View的尺寸使用30位二進(jìn)制表示裆蒸,也就是說最大是30個1(即2^30 - 1),也就是(1 << 30)- 1佛致,早最大化模式下辙谜,我們用View理論上能支持的最大值去構(gòu)造MeasureSpec是合理的装哆。

關(guān)于View的measure,網(wǎng)絡(luò)上有兩個錯誤的用法萍桌。為什么說是錯誤的奸绷,首先其違背了系統(tǒng)的內(nèi)部實現(xiàn)規(guī)范(因為無法通過錯誤的MeasureSpec去得出合法的SpecMode号醉,從而導(dǎo)致measure過程出錯)辛块,其次不能保證一定能measure出正確的結(jié)果润绵。

第一種錯誤用法:

int widthMeasureSpec = MeasureSpec.makeMeasureSpec(-1, MeasureSpec.UNSPECIFIED);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(-1, MeasureSpec.UNSPECIFIED);
view.measure(widthMeasureSpec. heightMeasureSpec);

第二種錯誤用法:

view.meastre(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);

4.3.2 layout過程

Layout的作用是ViewGroup用來確定子元素的位置,當(dāng)ViewGroup的位置被確定后憨愉,它在onLayout中會遍歷所有的子元素并調(diào)用其layout方法配紫,在layout方法中onLayout方法又會被調(diào)用。Layout過程和measure過程就簡單多了享扔,layout方法確定View本身的位置植袍,而onLayout方法則會確定所有子元素的位置于个,先看View的layout方法,如下所示秀存。

public void layout(int l, int t, int r, int b) {
    if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
        onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
        mPrivateFlag3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }
    
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;
    
    boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        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;
}

layout方法的大致流程如下:首先會通過setFrame方法來設(shè)定View的四個頂點的位置应又,即初始化mLeft株扛、mRight汇荐、mTop和mBottom這四個值掀淘,View的四個頂點一旦確定,那么View在父容器中的位置也就確定了倾贰;接著會調(diào)用onLayout方法拦惋,這個方法的用途是父容器確定子元素的位置厕妖,和onMeasure方法類似,onLayout的具體實現(xiàn)同樣和具體的布局有關(guān)软能,所以View和ViewGroup均沒有真正實現(xiàn)onLayout方法。接下來凳枝,我們可以看一下LinearLayout的onLayout方法范舀,如下所示了罪。

potected void onLayout(boolean changed, int l, int t, int r, int b) {
    if (mOrientation == VERTICAL) {
        layoutVertical(l, t, r, b);
    } else {
        layoutHorizontal(l, t, r, b);
    }
}

LinearLayout中onLayout的實現(xiàn)邏輯和onMeasure的實現(xiàn)邏輯類似泊藕,這里選擇layoutVertical繼續(xù)講解,為了更好地理解其邏輯玫锋,這里只給出了主要的代碼:

void layoutVertical(int left, int top, int right, int bottom) {
    ... 
    final int count = getVirtualChildCount();
    for (int i = 0; i < count; i++) {
        final View child = getVirtualChildAt(i);
        if (child == null) {
            childTop += measureNullChild(i);
        } else if (child.gerVisibility() != GONE) {
            final int childWidth = child.getMeasuredWidth();
            final int childHeight = child.getMeasureHeight();
            
            final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
            ...
            if (hasDividerBeforeChildAt(i)) {
                childTop += mDividerHeight;
            }
            
            childTop += lp.topMargin;
            setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight);
            childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
            
            i += getChildrenSkipCount(child, i);
        }
    }
}

這里分析一下layoutVertical的代碼邏輯撩鹿,可以看到此方法會遍歷所有子元素并調(diào)用setChildFrame方法來為子元素指定對應(yīng)的位置节沦,其中childTop會逐漸增大础爬,這就意味著后面的子元素會被放置在靠下的位置,這剛好符合豎直方向的LinearLayout的特性叫搁。至于setChildFrame渴逻,它僅僅是調(diào)用子元素的layout方法而已音诫,這樣父元素在layout方法中完成自己的定位以后奕删,就通過onLayout方法去調(diào)用子元素的layout方法甜熔,子元素又會通過自己的layout方法來確定自己的位置,這樣一層一層地傳遞下去就完成了整個View樹的layout過程幽勒。setChildFrame方法的實現(xiàn)如下所示港令。

priavte void setChildFrame(View child, int left, int top, int width, int height) {
    child.layout(left, top, left + width, top + height);
}

我們注意到顷霹,setChildFrame中的width和height實際上就是子元素的測量寬/高,從下面的代碼可以看出這一點:

final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight);

而在layout方法中會通過setFrame去設(shè)置子元素的四個頂點的位置遥昧,在setFrame中有如下幾句賦值語句炭臭,這樣一來子元素的位置就確定了:

mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;

下面我們來回答一個在4.2.3節(jié)中提到的問題:View的測量寬/高和最終寬/高有什么區(qū)別袍辞?這個問題可以具體為:View的getMeasuredWidth和getWidth這兩個方法有什么區(qū)別搅吁,至于getMeasuredHeight和getHeight的區(qū)別和前兩者完全一樣。為了回答這個問題那婉,首先党瓮,我們看一下getWidth和getHeight這兩個方法的具體實現(xiàn):

public final int getWidth() {
    return mRight - mLeft;
}

public final int getHeight() {
    return mBottom - mTop;
}

從getWidth和getHeight的源碼再結(jié)合mLeft寞奸、mRight枪萄、mTop和mBottom這四個變量的賦值過程來看,getWidth方法的返回值剛好就是View的測量高度聚凹,而getHeight方法的返回值也剛好就是View的測量高度。經(jīng)過上述分析彼哼,現(xiàn)在我們可以回答這個問題了:在View的默認(rèn)實現(xiàn)中敢朱,View的測量寬/高和最終寬/高是相等的摩瞎,只不過測量寬/高形成于View的measure過程旗们,而最終寬/高形成于View的layout過程,即兩者的賦值時機不同杖剪,測量寬/高的賦值時機稍微早一些盛嘿。因此括袒,在日常開發(fā)中,我們可以認(rèn)為View的測量寬/高就等于最終寬/高芥炭,但是的確存在某些特殊情況會導(dǎo)致兩者不一致园蝠,下面舉例說明痢士。

如果重寫View的layout方法怠蹂,代碼如下:

public void layout(iny l, int t, int r, int b) {
    super.layout(l, t, r + 100, b + 100);
}

上述代碼會導(dǎo)致在任何情況下View的最終寬/高總是比測量寬/高大100px,雖然這樣做會導(dǎo)致View顯示不正常并且也沒有實際意義易遣,但是這證明了測量寬/高的確可以不等于最終寬/高豆茫。另外一種情況是在某些情況下,View需要多次measure才能確定自己的測量寬/高为肮,在前幾次的測量過程中,其得出的測量寬/高有可能和最終寬/高不一致茅特,但最終來說白修,測量寬/高還是和最終寬/高相同兵睛。

4.3.3 draw過程

Draw過程就比較簡單了,它的作用是將View繪制到屏幕上面笛丙。View的繪制過程遵循如下幾步:

(1)繪制背景background.draw(canvas).

(2)繪制自己(onDraw)胚鸯。

(3)繪制children(dispatchDraw)笨鸡。

(4)繪制裝飾(onDrawScrollBars)形耗。

這一點通過draw方法的源碼可以明顯看出來,如下所示拟糕。

public void draw(Canvas canvas) {
    final int privateFlags = mPrivateFlags;
    final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
    mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
    
    /**
     * 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
    int saveCount;
    
    if (!dirtyOpaque) {
        drawBackground(canvas);
    }
    
    // skip step 2 & 5 if possible (common case)
    final int viewFlags = mViewFlags;
    boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
    boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
    if (!verticalEdge && !horizontalEdges) {
        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);
        
        // Step 4, draw the children
        dispatchDraw(canvas);
        
        // Step 6, draw decorations (scrollbars)
        onDrawScrollBars(canvas);
        
        if (mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }
        
        // we're done...
        return;
    }
}

View繪制過程的傳遞是通過dispatchDraw來實現(xiàn)的,dispatchDraw會遍歷調(diào)用所有子元素的draw方法累澡,如此draw事件就一層層地傳遞了下去般贼。View有一個特殊的方法setWillNotDraw,先看一下它的源碼霞赫,如下所示肥矢。

/**
 * If this view doesn't do any drawing on its own, set this flag to 
 * allow further opyimizations. By default, this flag is not set on
 * view, but could be set on some View subclasses such as ViewGroup.
 * 
 * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
 * you should clear this flag.
 *
 * @param willNotDraw whether or not this View draw on its own
 */
public void setWillNotDraw(boolean willNotDraw) {
    setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}

從setWillNotDraw這個方法的注釋中可以看出甘改,如果一個View不需要繪制任何內(nèi)容十艾,那么設(shè)置這個標(biāo)記為為true以后,系統(tǒng)會進(jìn)行相應(yīng)的優(yōu)化荤牍。默認(rèn)情況下康吵,View沒有啟動這個優(yōu)化標(biāo)記為愧杯,但是ViewGroup會默認(rèn)啟動這個優(yōu)化標(biāo)記位力九。這個標(biāo)記位對實際開發(fā)的意義是:當(dāng)我們的自定義控件繼承于ViewGroup并且本身不具備繪制功能時,就可以開啟這個標(biāo)記位從而使于系統(tǒng)進(jìn)行后續(xù)的優(yōu)化棕兼。當(dāng)然伴挚,當(dāng)明確知道一個ViewGroup需要通過onDraw來繪制內(nèi)容時灾炭,我們需要顯示地關(guān)閉WILL_NOT_DRAW這個標(biāo)記位蜈出。

?著作權(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
  • 文/潘曉璐 我一進(jìn)店門勋功,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人库说,你說我怎么就攤上這事狂鞋。” “怎么了潜的?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長啰挪。 經(jīng)常有香客問我信不,道長,這世上最難降的妖魔是什么亡呵? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任抽活,我火速辦了婚禮,結(jié)果婚禮上锰什,老公的妹妹穿的比我還像新娘下硕。我一直安慰自己,他們只是感情好汁胆,可當(dāng)我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布梭姓。 她就那樣靜靜地躺著,像睡著了一般嫩码。 火紅的嫁衣襯著肌膚如雪誉尖。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天谢谦,我揣著相機與錄音释牺,去河邊找鬼萝衩。 笑死,一個胖子當(dāng)著我的面吹牛没咙,可吹牛的內(nèi)容都是我干的猩谊。 我是一名探鬼主播,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼祭刚,長吁一口氣:“原來是場噩夢啊……” “哼牌捷!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起涡驮,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤暗甥,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后捉捅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體撤防,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年棒口,在試婚紗的時候發(fā)現(xiàn)自己被綠了寄月。 大學(xué)時的朋友給我發(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
  • 正文 我出身青樓芙贫,卻偏偏與公主長得像搂鲫,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子磺平,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,086評論 2 355

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