自定義View基礎之View的繪制流程

DecorView

在了解view的繪制流程之前,首先我們要知道一個DecorView的概念,什么是DecorView?

DecorView是整個界面的最頂層View,它的尺寸通常就是屏幕尺寸,也就是說,DecorView是充滿屏幕的,它實際上是一個FrameLayout,又包含了一個子元素,LinearLayout,這個LinearLayout又包含兩個FrameLayout,一個用來顯示標題,一個用來顯示內容。顯示內容的FrameLayout,其ID為 android.R.id.content

我們在 Activity 中設置 Layout 時,用的方法是setContentView,指的就是這個content。參考下圖

DecorView

View的繪制流程是從ViewRootperformTraversals開始的,它經過measure,layout,draw三個過程最終將View繪制出來。

performTraversals會依次調用performMeasureperformLayoutperformDraw三個方法,他們會依次調用measure,layout,draw方法,然后又調用了onMeasureonLayoutdispatchDrawonMeasure方法中,父容器會對所有的子View進行Measure,子元素又會作為父容器,重復對它自己的子元素進行Measure,這樣Measure過程就從DecorView一級一級傳遞下去了。Layout和Draw方法也是如此。

我們關注的重點是onMeasure方法,它決定了所有View的尺寸。

MeasureSpec

MeasureSpec是一個32位 int 數值,它包含了兩組信息。高兩位代表SpecMode,低30位代表SpecSize

SpecMode指測量模式,有以下三個值:

  • EXACTLY
    表示父容器已經確定好子view的大小值,這時候view的大小就是SpecSize的值。它對應了LayoutParams中設置 match_parent 和指定具體數值的情況
  • AT_MOST
    表示父容器并不確定子View的具體大小,但是確定了一個上限,就是SpecSize,子View不能超過這個大小。具體值是多少,要看View 的具體實現。
  • UNSPECIFIED
    父容器不對子View進行限制,可以按照自己的意愿隨意設置,比較少見,通常用于系統內部。

說完這三種模式可能大家還是非常模糊,我們看一下MeasureSpec的源碼,就一目了然了。

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

MeasureSpec 內部封裝了makeMeasureSpecgetModegetSize三個方法,方便我們對MeasureSpec數據進行處理。
那么MeasureSpec到底是怎樣使用的呢?接下來我們就要看onMeasure方法了。

View的onMeasure方法以及MeasureSpec的獲取

首先我們回顧一下View 的繪制流程,在上文中有一句黑體顯示的話,意思就是所有的View測量都是從最頂層的DecorView開始的,我們就先看一下DecorView的Measure過程,它的MeasureSpec是怎樣得到的。

ViewRootperformTraversals方法中可以看到:

childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);  
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); 

DecorViewMeasureSpec是通過getRootMeasureSpec來得到的,它傳入了兩個參數,lp.width和lp.height在創建ViewGroup實例的時候就被賦值了,它們都等于MATCH_PARENT。
然后來看一下getRootMeasureSpec的代碼:

private int getRootMeasureSpec(int windowSize, int rootDimension) {  
    int measureSpec;  
    switch (rootDimension) {  
    case ViewGroup.LayoutParams.MATCH_PARENT:  
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);  
        break;  
    case ViewGroup.LayoutParams.WRAP_CONTENT:  
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);  
        break;  
    default:  
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);  
        break;  
    }  
    return measureSpec;  
} 

到這里,我們就知道了,DecorView作為最頂級的根View,它的MeasureSpec就是EXACTLY+WindowSize,也就是說他總是充滿全屏的。

最頂層的DecorView尺寸確定好之后,下一步就是各個子View的Measure過程了,系統會從ViewGroup開始一級一級向下Measure。但是,我們又知道,一個View的大小同時還要受到父View的限制,它的大小是由本身的LayoutParams,和父View 的MeasureSpec共同決定的。

對于普通的View(非ViewGroup),它的Measure過程是由ViewGroup傳遞過來的,看一下ViewGroup的measureChildWithMargin方法就清楚了:

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

可以看到,子View的Measure方法,實際上就是在這里調用的。在Measure之前,先獲得子View的MeasureSpec,然后調用了child.measure。

子View的MeasureSpec又是通過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);
    }

這一段代碼有點長,但是不難理解,該方法在調用時傳入了三個參數:父View的MeasureSpecPadding(父View已經被占用的空間),和子View的LayoutParams(match_parent,wrap_content,或者精確的數值)

然后根據父view的SpecMode進行判斷,用表格的方式表示如下,橫排表示父View的SpecMode,豎排表示子View的SpecMode(LayoutParams),內容表示View最終的SpecMode

EXACTLY(match_parent給定數值) AT_MOST(wrap_content) UNSPECIFIED
dp/px( >0) EXACTLY+給定值 EXACTLY+給定值 EXACTLY+給定值
MATCH_PARENT EXACTLY+父View剩余空間(或0) AT_MOST+父View剩余空間(或0) UNSPECIFIED + 0
WRAP_CONTENT AT_MOST+父View剩余空間(或0) AT_MOST+父View剩余空間(或0) UNSPECIFIED + 0

到這里就很清楚了,子View的大小是由父View的MeasureSpce和它本身的LayoutParams共同決定的:

  • View是具體數值:最終結果一定是EXACTLY+·給定值·
  • 子·View·是MATCH_PARENT,它的大小就是父View的剩余空間,mode和父View相同(不考慮UNSPECIFIED )
  • ViewWRAP_CONTENT,它的大小是父View的剩余空間,mode是AT_MOST

Measure的流程到這里基本已經清楚了:從頂級View開始,先調用MeasureChild將父View的Spec傳入,通過getChildMeasureSpec方法,獲取到子View的Spec,然后通過child.measure(widhSpec,heightSpec)將得到的子View Spec傳遞過去。

View的Measure過程

通過上邊的介紹,我們已經確定了View的MeasureSpec,接下來就是具體的Measure方法了,因為View的Measure最終調用的是onMeasure,我們只要看onMeasure方法就可以了:

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

代碼很簡單,就是一個setMeasuredDimension,用來設置View的尺寸,傳入的參數,在上文已經介紹的很清楚了,從ViewGroup傳遞過來。但是他還調用了一個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;
    }

這個方法邏輯也很簡單,不考慮UNSPECIFIED的情況下,最終返回的結果一定是在ViewGroup里傳遞來的MeasureSpec

到這里,整個Measure過程就已經結束了。View的最終尺寸大小,遵循的就是上面我們得出的三條結論。要注意一點,當我們直接繼承View時:

子View是WRAP_CONTENT,它的大小是父View的剩余空間,mode是AT_MOST

前兩個都是沒有問題的,但是WRAP_CONTENT,它的大小和MATCH_PARENT是一樣的,也就是說WRAP_CONTENT會不起作用。解決這個問題也很簡單,必須重寫onMeasure方法,然后自定義一個默認的width和height,當傳遞過來的SpecModeAT_MOST時,設置尺寸為定義的寬高。

ViewGroup的Measure過程

相對于單一View來說,ViewGroupMeasure方法要復雜一些,因為它不僅僅是確定自己的尺寸,還要測量每一個子View,并得到他們的MeasureSpecViewGroup并沒有重寫Measure(final的)方法,也沒有重寫onMeasure方法,因為ViewGroup是抽象類,onMeasure方法需要在具體的實現類中去重寫。

ViewGroup只是為測量子View添加了兩個新的方法: measureChildren()measureChild()。代碼如下

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

首先去遍歷子View,調用MeasureChild方法,在該方法中獲取各個子View的MeasureSpec,然后調用子View的Measure方法,傳遞各個子View的大小。

Layout過程

Layout的作用,是ViewGroup為自己的子View指定位置,現在看一下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;
        }
        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;
    }

關鍵在第11行,它最終調用了onLayout(changed, l, t, r, b)方法,來完成最終的Layout過程。
那么我們就看一下onLayout方法,你會發現是這樣的:
View:

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

ViewGroup

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

View中是一個空方法,ViewGroup中是一個抽象方法。為什么呢?其實答案很簡單。Layout過程是確定子元素在自己布局中的位置,view是不存在子元素的,所以是空方法,它只需要通過setFrame來決定自身的位置。而ViewGroup本身就是一個抽象方法,它的不同實現會有不同的Layout方式,所以在繼承ViewGroup的布局中,必須指定自己的Layout方式,因此,ViewGroup中是一個抽象方法。

layout方法的大致流程如下:

首先通過setFrame來設置View的四個頂點位置,并保存起來。在Layout時,會先用setFrame方法來保存四個頂點的坐標,并進行判斷,值如果發生了變化,就會調用onLayout方法來重新定位子元素。
如果是單一View,調用setFrame方法之后,其實就已經結束了,因為他沒有子元素,onLayout方法是空的,調用onlayout方法沒有任何意義。而對于ViewGroup來說,他是包含子元素的,需要在onLayout方法中繼續確定子元素的位置。

借用一下LinearLayout的源碼來分析:

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

可以看見,VERTICALHORIZONTAL的方法是不一樣的。看一下layoutVertical的代碼:

    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方法來確定各個子元素的位置,因為是Vertical的排列方式,所以childTop 的值不斷增大,子元素會依次向下排列。
setChildFrame代碼如下:

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

只是調用子View的Layout方法而已。View的Layout方法上邊已經介紹過了,如果是單一View,調用setFrame結束,如果是ViewGroup,會繼續調用他的onlayout方法,這樣一層一層向下確定各個View的位置。整個流程結束以后,所有的Layout過程就結束了。

可能大家也注意到一個問題,layout方法會接收四個參數,分別代表四個頂點的位置,而該方法是通過child.layout(left, top, left + width, top + height);來調用的,實際上,只有left和top兩個值是確定的
另外兩個頂點是通過寬和高來確定的,寬和高是這樣獲取的:

                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();

得到四個頂點的值之后,最終會傳遞到setFrame方法,setFrame的部分代碼:

 protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;

這樣看起來,view最終的尺寸就是Measure過程中確定的尺寸
如果我們改動一下代碼,改變一下ViewGroup的setChildFrame或者view的layout方法:

child.layout(left, top, left + width+100, top + height+100);
或者
public void layout(int l, int t, int r, int b) {
super.layout(l, t, r+100, b+100);
}

這樣最終得到的View就會比Measure出來的View大了100的尺寸,在setFrame的時候,得到的四個坐標值也會大100,
看一下View的getwidth和getHeight代碼:

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

此時獲取到的View最終寬和高就會比Measure出來的寬和高(getMeasuredWidth,getMeasuredHeight)要大100px了。
所以,Measure出來的尺寸,通常情況下是View的最終尺寸,實際上View的最終尺寸是在Layout階段來決定的,它并不一定等于MeasureSpec的大小。

獲取View的尺寸

上邊我們提到了兩個方法:getMeasuredWidthgetWidth,用來獲取View的尺寸,我們知道,View的尺寸是通過MeasureLayout共同決定的,那么獲取View的尺寸就很簡單了,調用這兩個方法就可以了。實際上并不是這么簡單的。因為Activity的生命周期和View的繪制不是同步的。在onCreateonStartonResume里簡單的調用這兩個方法,得到的結果是不確定的,因為View可能還沒有繪制完成。那么怎樣才能得到正確的View尺寸?

1.重寫View的onFocusChanged方法。
在View得到或者失去焦點的時候,該方法都會被調用。可以這么理解,既然View已經得到焦點了,那么它的寬和高必定已經準備好了。所以獲取尺寸是完全可以的。所以我們可以重寫該方法,在此處調用getMeasuredWith方法。

2.利用view.post(runnable)
View在處理post的消息時,肯定已經初始化好了,所以利用此方法也能正確的獲取到View尺寸。

3.ViewTreeObserver
它是view事件的一個觀察者,用來監聽ViewTree的各種事件,針對不同的事件它定義了許多不同的接口,可以利用onGlobalLayout方法來獲取View尺寸。

4.Activity的onPostCreate()

Draw過程

measure和layout的過程都結束后,接下來就進入到draw的過程了,源碼如下:

public void draw(Canvas canvas) {  
    if (ViewDebug.TRACE_HIERARCHY) {  
        ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);  
    }  
    final int privateFlags = mPrivateFlags;  
    final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&  
            (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);  
    mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;  
    // Step 1, draw the background, if needed  
    int saveCount;  
    if (!dirtyOpaque) {  
        final Drawable background = mBGDrawable;  
        if (background != null) {  
            final int scrollX = mScrollX;  
            final int scrollY = mScrollY;  
            if (mBackgroundSizeChanged) {  
                background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);  
                mBackgroundSizeChanged = false;  
            }  
            if ((scrollX | scrollY) == 0) {  
                background.draw(canvas);  
            } else {  
                canvas.translate(scrollX, scrollY);  
                background.draw(canvas);  
                canvas.translate(-scrollX, -scrollY);  
            }  
        }  
    }  
    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);  
        // Step 6, draw decorations (scrollbars)  
        onDrawScrollBars(canvas);  
        // we're done...  
        return;  
    }  
}  

可以看到,第一步是從第9行代碼開始的,這一步的作用是對視圖的背景進行繪制。這里會先得到一個mBGDrawable對象,然后根據layout過程確定的視圖位置來設置背景的繪制區域,之后再調用Drawabledraw()方法來完成背景的繪制工作。那么這個mBGDrawable對象是從哪里來的呢?其實就是在XML中通過android:background屬性設置的圖片或顏色。當然你也可以在代碼中通過setBackgroundColor()setBackgroundResource()等方法進行賦值。

接下來的第三步是對視圖的內容進行繪制。可以看到,這里去調用了一下onDraw()方法,那么onDraw()方法里又寫了什么代碼呢?進去一看你會發現,原來又是個空方法啊。其實也可以理解,因為每個視圖的內容部分肯定都是各不相同的,這部分的功能交給子類來去實現也是理所當然的。

第三步完成之后緊接著會執行第四步,這一步的作用是對當前視圖的所有子視圖進行繪制。但如果當前的視圖沒有子視圖,那么也就不需要進行繪制了。因此你會發現View中的dispatchDraw()方法又是一個空方法,而ViewGroupdispatchDraw()方法中就會有具體的繪制代碼。

以上都執行完后就會進入到第六步,也是最后一步,這一步的作用是對視圖的滾動條進行繪制。那么你可能會奇怪,當前的視圖又不一定是ListView或者ScrollView,為什么要繪制滾動條呢?其實不管是Button也好,TextView也好,任何一個視圖都是有滾動條的,只是一般情況下我們都沒有讓它顯示出來而已。

通過以上流程分析,相信大家已經知道,View是不會幫我們繪制內容部分的,因此需要每個視圖根據想要展示的內容來自行繪制。如果你去觀察TextViewImageView等類的源碼,你會發現它們都有重寫onDraw()這個方法,并且在里面執行了相當不少的繪制邏輯。繪制的方式主要是借助Canvas這個類,它會作為參數傳入到onDraw()方法中,供給每個視圖使用。

另外,view還有一個特殊方法:setWillNotDraw。默認情況下,view是不啟用這個參數的,而viewGroup會啟用。因為單一View是肯定需要繪制自身的,而ViewGroup只是作為單一View的載體,draw工作是交給子View的,它不需要繪制自身,也就是默認ViewGroup是透明的,如果想讓ViewGroup繪制自身,需要調用setWillNotDraw(false),來啟用draw功能,然后重寫的onDraw方法才會起作用。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容