Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android開發 第四章 View的工作原理

Android開發 第四章 View的工作原理

編輯:關於Android編程

一、初識ViewRoot和DecorView

ViewRoot類對應ViewRootImpl類,它是連接WindowManage和DecorView的紐帶。在ActivityThread中,當Activity對象被創建完畢後,會將DecorView添加到Window中,同時會創建ViewRootImpl對象,並將ViewRootImpl對象和DecoView建立關聯。

View的繪制流程是從ViewRoot的performTraversals方法開始的,經過measure、layout、draw三個過程將一個View繪制出來。

Measure過程決定了View的寬和高,Measure完成後,一般情況下可以通過getMeasureWidth和getMeasureHeight獲取View測量後的高,特殊情況除外;Layout過程決定了View四個頂點的坐標和實際的View的寬高;Draw過程則決定了View的顯示。

DecorView作為頂級View,當我們setContentView時,布局添加到了id為content的FrameLayout中,以下方式可以得到content和我們設置的view:

     ViewGroup content  = (ViewGroup)findViewById(android.R.id.content);
     View view = content.getChildAt(0);

二、理解MeasureSpec

MeasureSpec
MeasureSpec代表一個32位int值,高2位代表SpecMode,低30位代表specSize,前者指測量模式,後者指某種測量模式下的規格大小。
SpecMode有三類:
UNSPECIFIED
父容器不對View有任何限制,要多大給多大,一般用於系統內部,表示一種測量的狀態。 EXACTLY
父容器已經檢測出View所需要的精確大小,這個時候View的最終大小就是SpecSize所指定的值,對應LayoutParams中的match_parent和具體的數值兩種模式。 AT_MOST
指定一個可用大小即SpecSize,View的大小不能大於這個值,對應LayoutParams中的wrap_content。

MeasureSpec和LayoutParams的對應關系
對於DecorView,其MeasureSpec由其窗口的尺寸和其自身的LayoutParams來共同確定;對於普通view,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams來共同決定,MeasureSpec一旦確定後,onMeasure就可以確定View的測量寬/高。
DecorView的MeasureSpec產生過程根據LayoutParams中的寬和高來劃分:

LayoutParams.MATCH_PARENT:精確模式,大小就是窗口的大小; LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超過窗口的大小; 固定大小:精確模式,大小為LayoutParams指定的大小;

普通View來說,針對不同的父容器和View本身不同的LayoutParams,View就可以有多種MeasureSpec。

當View采用固定寬高的時候,不管父容器的MeasureSpec是什麼,View的MeasureSpec都是精確模式並且其大小遵循LayoutParams的大小。 當View的寬高是match_parent時,如果父容器的模式是精確模式,那麼view也是精確模式並且其大小是父容器的剩余空間;如果父容器是最大模式,那麼View也是最大模式並且其大小不會超過父容器的剩余空間。 當view的寬高是wrap_content時,不管父容器的模式是精確還是最大化嗎,View的模式總是最大化並且大小不能超過父容器的剩余空間。

三、View的工作流程

measure過程

View的measure過程

View的measure方法是一個final類型的方法,意味著子類不能重寫這個方法。View的measure方法中總會去調用View的onMeasure方法。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

我們只需要看這個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兩種情況,getDefaultSize返回的大小就是measureSpec中的specSize,而這個specSize就是View測量後的大小,View的最終大小是在layout階段確定的,幾乎所有情況下View的測量大小和最終大小是相等的。

UNSPECIFIED這種情況,一般用於系統內部的測量過程,View的大小就是getDefaultSize第一個參數size,即寬高分別為getSuggestedMinimumWidth()和
getSuggestedMinimumHeight()這兩個方法的返回值,源碼如下:

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

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

}

從上面方法可以看出,getSuggestedMinimumWidth和getSuggestedMinimumHeight方法實現原理一樣。從getSuggestedMinimumWidth方法裡面可以看出,如果沒有設置背景那麼view的寬度為mMinWidth,而mMinWidth對應於android:minWidth這個屬性所指定的值,因此view的寬度即為android:minWidth屬性所指定的值,如果沒有指定則默認為0;如果指定了背景,那麼View的寬度就是max(mMinWidth, mBackground.getMinimumWidth()),我們看一下mBackground.getMinimumWidth(),Drawable的getMinimumWidth方法,如下所示:

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

這個方法返回的就是Drawable的原始寬度,前提是有原始寬度,否則返回0。
getSuggestedMinimumWidth這個方法邏輯如下:如果view沒有設置了背景,那麼返回android:minWidth這個屬性所指定的值,這個值可以為0;如果View設置了背景,則返回android:minWidth和背景最小寬度中的最大值。getSuggestedMinimumWidth和getSuggestedMinimumHeight的返回值就是View在UNSPECIFIED情況下的測量寬高。

從getDefaultSize方法的實現來看,View的寬高由specSize決定,所以我們一般自定義控件的時候需要重寫onMeasure方法並設置wrap_content時的自身大小,否則在布局中使用wrap_content相當於match_parent。

ViewGroup的measure過程

ViewGroup除了完成自己的measure過程以外,還會遍歷去調用所有子元素的measure方法,各個子元素在遞歸去執行這個過程。ViewGroup是一個抽象類,提供了一個叫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);
        }
    }
}

上面方法會對每一個子元素進行measure,measureChild這個方法如下:

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

上面主要是取出子元素的LayoutParams,然後在通過getChildMeasureSpec來創建子元素的MeasureSpec,將MeasureSpec直接傳遞給View的measure方法來進行測量。在ViewGroup沒有定義測量的具體過程,不同的ViewGroup子類有不同的布局特性,需要各個子類去具體實現,下面分析LinearLayout的onMeasure的具體實現。

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

上述代碼主要針對不同方向實現不同的測量,看一下豎直方向上的布局:

// See how tall everyone is. Also remember max width.
    for (int i = 0; i < count; ++i) {
        final View child = getVirtualChildAt(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).
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                    heightMeasureSpec, usedHeight);
final int totalLength = mTotalLength;
            mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));

系統會遍歷子元素對每個子元素執行measureChildBeforeLayout這個方法,這個方法內部還是會調用子元素的measure方法,這樣各個子元素就依次開始進入measure過程,並且系統會通過mTotalLength這個變量來存儲LinearLayout在豎直方向上的初步高度。每測量一個元素,mTotalLength就會增加,增加的部分主要包括了子元素的高度以及子元素在豎直方向上的margin等。子元素測量完畢,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;
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            heightSizeAndState);

當子元素測量完畢後,LinearLayout會根據子元素的情況來測量自己的大小。如果在布局中高度采用的match_parent或者具體的數值,那麼則和View的測量過程一致,即高度為specSize;如果高度采用的是wrap_content,高度就是所有子元素所占用的高度總和,但是不能超過它的父容器的剩余空間,最終高度還要考慮其在豎直方向的padding,如下源碼所示:

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    final int result;
    switch (specMode) {
        case MeasureSpec.AT_MOST:
            if (specSize < size) {
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        case MeasureSpec.UNSPECIFIED:
        default:
            result = size;
    }
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}

View的measure完成以後,通過getMeasureWidth/Height方法就可以正確的獲取到view的測量寬高。但是在某些極端情況下,系統可能需要多次measure才能獲取到最終的測量寬高,最好在onLayout方法中獲取View的測量寬高或者最終寬高。

在Activity裡面獲取某一個View的寬高:

Activity/View#onWindowFocusChanged
View初始化完畢,可以獲取寬高,不過會被調用多次,在Activity的窗口得到焦點和失去焦點的時候均會被調用一次,當Activity繼續執行和暫停執行的時候,onWindowFocusChanged均會被調用。

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

view.post(runnable)
通過post可以將一個runnable投遞到消息隊列的尾部,然後等待Looper調用此runnabe的時候,View已經初始化好了。

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

ViewTreeObserver
使用ViewTreeObserver的眾多回調也可以完成這個功能,比如使用OnGlobalLayoutListener這個接口,隨著View樹的狀態發生改變,onGloballayout會被調用多次,如下:

ViewTreeObserver observer = view.getViewTreeObserver();
    observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    });

view.measure(int widthMeasureSpec, int heightMeasureSpec)
通過手動對view進行measure來得到具體View的寬高,需要根據LayoutParams來分:

match_parent
無法測出具體寬高,無法知道父容器的剩余空間。

具體數值(dp/px)
比如寬和高都是100px,如下measure

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

wrap_content
如下measure

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

注意(1 << 30)-1,View的尺寸使用30位二進制表示,最大是30個1(2^30-1),在最大化模式下,用View理論上能支持的最大值去構造MeasureSpec是合理的。

layout過程
Layout的作用是ViewGroup用來確定子元素的位置,當viewGroup的位置被確定後,在onLayout中會遍歷所有的子元素並調用其layout方法,在layout方法中onLayout又會被調用。layout確定view本身位置,onLayout方法確定所有子元素的位置,源碼如下:

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

    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 listenersCopy =
                    (ArrayList)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;
}

首先通過setFrame方法來設定View的四個頂點的位置,初始化mLeft、mRight、mTop、mBottom四個值,四個頂點確定,View在父容器中的位置也就確定了;接著調用onLayout方法,父容器用來確定子元素位置,onLayout的實現和布局有關,我們看一下LinearLayout的onLayout源碼:

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.getVisibility() != GONE) {
            final int childWidth = child.getMeasuredWidth();
            final int childHeight = child.getMeasuredHeight();

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

我們這裡還是分析在豎直方向上的布局,這裡會遍歷所有子元素並調用setChildFrame方法來為子元素指定對應的位置,其中childTop會逐漸增大,非常符合LinearLayout在豎直方向上的特性。setChildFrame調用子元素的layout方法而已,這樣當父元素在layout方法中完成自己的定位後,就通過onLayout方法去調用子元素的layout方法,子元素又會通過自己的layout方法來確定自己的位置,一層一層地傳遞下去就完成了整個view樹的layout過程。setChildFrame源碼如下:

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

方法中的width和height實際上就是子元素的測量寬高。
而在layout方法中會通過setFrame去設置子元素的四個頂點的位置,有如下賦值語句:

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

View的測量寬高和最終寬高有什麼不同?

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

從上面getWidth和getHeight的源碼結合mLeft、mRight、mTop、mBottom這四個變量的賦值過程來看,getWidth的返回值就是View的測量寬度,getHeight同理。在View的默認實現中,View的測量寬高和最終寬高是相等的,測量寬高是形成於View的measure過程,而最終寬高形成於View的layout過程,賦值時機不一樣,一般情況下兩者是相等的,但在有些情況下不一致,舉個例子:

protected void onLayout(boolean changed, int l, int t, int r, int b) {
    super.layout(l, t, r + 100, b + 100);
}

上述代碼則會導致在任何情況下View的最終寬高總是比測量寬高大100px;另外一種情況則是View需要多次measure才能確定自己的測量寬高,在前幾次的測量過程中,得出的測量寬高和最終寬高有可能不一致,但是最終來說,測量寬高和最終寬高還是一樣的。

draw過程
Draw的作用是將View繪制到屏幕上面,View的繪制過程遵循如下幾步:

繪制背景background.draw(canvas) 繪制自己(onDraw) 繪制children(dispatchDraw) 繪制裝飾(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 (!verticalEdges && !horizontalEdges) {
        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children
        dispatchDraw(canvas);

        // Overlay is part of the content and draws beneath Foreground
        if (mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }

        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);

        // we're done...
        return;
    }

View的繪制過程是通過dispatchDraw來實現的,會遍歷所有元素的draw方法,draw事件就一層一層傳遞下去。View有一個特殊的方法setWillNotDraw:

public void setWillNotDraw(boolean willNotDraw) {
    setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}

如果一個view不需要繪制任何內容,那麼設置這個標記為true後,系統會進行相應的優化。默認情況下,View沒有啟用這個標記位,但是ViewGroup會啟用這個標記位。一般我們的自定義控件繼承於ViewGroup本身並不具備繪制功能時,就可以開啟這個標記位便於系統進行後續優化。當知道ViewGroup需要通過onDraw來繪制內容時,我們需要顯式的關閉WILL_NOT_DRAW 這個標記位。

四、自定義View

自定義view的分類

繼承View重寫onDraw方法
重寫onDraw方法實現一些不顧則的圖形 繼承ViewGroup派生特殊的Layout
主要用於實現自定義布局 繼承特定的View(比如TextView)
一般用於擴展某種已有的View的功能 集成特定的ViewGroup(比如LinearLayout)
當某種效果看起來像幾種view組合在一起的時候,可以采用這種方法。

自定義view須知

讓View支持wrap_content 如果有必要,View支持padding
直接繼承View的控件,如果不在draw方法中處理padding,那麼padding屬性不起作用的;直接繼承ViewGroup的控件需要在onMeasure和onLayout中考慮padding和子元素的margin對其造成的影響,不然導致padding和子元素的margin失效。 盡量不要在View中使用Handler,沒必要
View內部本身提供了post系列的方法,完全可以替代Handler View中如果有線程或者動畫,需要及時停止,參考View#onDetachedFromWindow
有線程或者動畫需要停止時,那麼onDetachedFromWindow是一個很好的時機。當包含此View的Activity退出或者當前View被Remove時,View的onDetachedFromWindow方法會被調用;相對應的一個方法是onAttachedToWindow,當包含此View的Activity啟動時,View的onAttachedToWindow會被調用,當view變得不可見時我們也需要停止線程和動畫。 View帶有滑動嵌套情形時,需要處理好滑動沖突
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved