1.ViewRoot和DecorView
ViewRoot對應ViewRootImpl,實現了DecorView和WindowManager之間的交互。
View的繪製流程從ViewRoot#performTraversals開始,經過measure、layout、draw最終將一個View繪製出來:
例,measur過程:performMeasure->measure->onMeasure->子View的measure。
measure決定了View的寬高,measure完成後可以通過getMeasureWidth/getMeasureHeight獲取測量後的寬高;layout決定了View四個頂點的座標和實際View的寬高,可調用getLeft/getTop/getRight/getBottom/getWidth/getHeight獲取對應屬性;draw決定了View的顯示,draw完成後View的內容才顯示在屏幕上。
Activity中通過setContentView設置的view位於DecorView的content部分,可以通過android.R.id.content索引到該View的父容器,然後通過getChildAt(0)定位到該View:
2.MeasureSpec
- 用於parent向child傳遞layout要求。真正傳遞的實際上是一個32位int存儲,高2位代表mode,低30位代表size,MeasureSpec只是一個工具類,幫助拼裝和拆解這個int。
mode:- UNSPECIFIED
parent不對child強加任何限制。child想要多大就多大。 - EXACTLY
parent已經決定了child準確的大小,child要依據這個大小。 - AT_MOST
child可以想多大就多大,但是被指定了一個上限。
- UNSPECIFIED
- 與LayoutParams的關係
對於一個View,可以設置LayoutParams來指定寬高,系統會綜合該LayoutParams和parent施加的MeasureSpec,得出最後應用於該View的MeasureSpec;而對於DecorView,因爲其沒有parent,所以取而代之的是Window的size,結合自己的LayoutParams得出最後的MeasureSpec。MeasureSpec一旦確定,onMeasure中就可以確定View的寬高。- DecorView的MeasureSpec計算過程:
在ViewRootImpl的measureHierarchy中,計算了DecorView的MeasureSpec。desiredWindow*爲window的size:
getRootMeasureSpec中根據window size和DecorView的LayoutParams計算出MeasureSpec。規則很簡單,如果是MATCH_PARENT或者固定的值,則spec mode爲EXACTLY,同時size設置爲相應的值;如果是WRAP_CONTENT,則spec mode爲AT_MOST,size爲window size:childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
private static int getRootMeasureSpec(int windowSize, int rootDimension) { int measureSpec; switch (rootDimension) { case ViewGroup.LayoutParams.MATCH_PARENT: // Window can't resize. Force root view to be windowSize. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY); break; case ViewGroup.LayoutParams.WRAP_CONTENT: // Window can resize. Set max size for root view. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST); break; default: // Window wants to be an exact size. Force root view to be that size. measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY); break; } return measureSpec; }
- 普通View的MeasureSpec計算過程:
以ViewGroup的measureChildWithMargins爲例,在該方法中會計算child的MeasureSpec。計算完成後,會直接對該view進行measure。計算時也會考慮parent的padding,child的margin:
具體的計算過程在getChildMeasureSpec中進行: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); }
- child指定確定的size,則遵從child的這個size設置。
- child指定match_parent,如果parent表示可以exactly,則其size爲parent size;如果parent表示atmost,即其size也不確定,則其atmost爲parent size。
- child指定wrap_content,則此時size由child自己決定,所以只限制其atmost爲parent size。
- DecorView的MeasureSpec計算過程:
3.View的渲染流程
measure確定測量寬高->layout確定最終寬高和四個頂點位置->draw繪製到屏幕上
- measure
- View的measure
View的measure過程由其measure方法執行,其中會調用onMeasure,具體的計算在onMeasure中進行。measure用final修飾,所以只有onMeasure可以而且必須被子類複寫。onMeasure的默認實現,是通過setMeasuredDimension(onMeasure中一定要調用該方法設置measure出的值)設置測量值爲view默認的size:
getSuggestedMinimumWidth中會根據android:minWidth和是否設置了background得出minwidth。getSuggestedMinimumHeight原理一樣:protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
默認的size在getDefaultSize中計算。對於parent的spec mode爲UNSPECIFIED的情況,最終的size即爲minwidth;在AT_MOST和EXACTLY的情況下默認的size就是parent中指定的size:protected int getSuggestedMinimumHeight() { return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight()); } protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); }
所以,onMeasure的默認實現都會將measure size設置爲parent size。對於child使用wrap_content的情況,這通常不是符合預期的設置。所以在自定義View的時候,需要重寫onMeasure方法,將View的measure size設置爲預期的默認值,一般是該View的默認最小值。(這也是爲什麼一定要重寫onMeasure)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; }
具體的處理方式,在onMeasure中,針對AT_MOST的情況,將對應的size(width或者height)設置爲默認最小值。因爲在ViewGroup的getChildMeasureSpec方法中,針對child爲wrap_content和child爲match_parent+parent爲wrap_content這兩種情況,最終的spec mode都會是AT_MOST,即針對無法由parent決定child的情況,最終都會是AT_MOST。 - ViewGroup的measure
ViewGroup除了完成自身的measure之外,還要遍歷子View去執行其measure方法。因爲ViewGroup不同的派生類具有不同佈局特性,所以測量方式也不同,故沒有提供默認的onMeasure方法。但是ViewGroup中提供了簡單的measure其child的方法,提供給其派生類使用(在其onMeasure中調用);複雜的情況下,其派生類一般是自己實現。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); } protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { ... }
- 獲取View的寬高的tips:
- 因爲系統可能多次measure之後,才能確定最終的寬高,所以measure之後的measureWidth和measureHeight可能是不準確的,這個時候就要在onLayout之後去獲取寬高。
- 在Activity的生命週期回調中無法直接獲取view的寬高,因爲View的渲染過程和其聲明週期回調不是同步執行的,可以通過如下方法:
- Activity/View#onWindowFocusChanged
該方法會在Activity的窗口獲得和失去焦點的時候被調用;伴隨着焦點的變化,該方法會被調用多次:@Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (hasFocus) { int width = view.getMeasuredWidth(); int height = view.getMeasuredHeight(); } }
- view.post(Runnable)
當View初始化完畢之後,Looper就開始執行各個post進去的Runnable:@Override protected void onStart() { super.onStart(); view.post(new Runnable() { @Override public void run() { int width = view.getMeasuredWidth(); int height = view.getMeasuredHeight(); } }); }
- ViewTreeObserver
如果一個View的view tree的layout狀態或者view的可見性發生了變化,onGlobalLayout就會被回調;伴隨着view tree的變化,該方法會被調用多次: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
手動調用view.measure去測量,然後得到寬高。對於該View的LayoutParams爲match_parent的情況,無法使用該方法,因爲此時parent的MeasureSpec是不確定的:// dp/px int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY); int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY); view.measure(widthMeasureSpec, heightMeasureSpec);
// wrap_content int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST); int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1 < 30) - 1, MeasureSpec.AT_MOST); view.measure(widthMeasureSpec, heightMeasureSpec);
- Activity/View#onWindowFocusChanged
- View的measure
- layout
該步驟用來確定View的位置,依據就是measure之後存儲的measure值。由View的layout方法處理:首先會通過setFrame設置自身的l/t/r/b(位置/寬高被確定)然後調用onLayout,在onLayout中需要遍歷其所有的子View,計算其layout數據,然後調用其layout方法,直至所有View都layout完成。所以,對於ViewGroup一定要實現onLayout方法。
ViewGroup和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);
以LinearLayout的layout過程爲例來看。首先LinearLayout的layout的起點也是其layout方法,被parent調用之後,設置了l/t/r/b;之後調用自己的onLayout向子View發起layout。根據佈局方式的不同,水平和垂直的layout也不同:
對於垂直的情況:childTop會隨着child一個個的layout逐漸增大,其表現就是後面child會被放置在更下面;拿到child的measureWidth和measureHeight之後,調用setChildFrame將layout工作傳遞給該child。@Override 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); } }
setChildFrame則直接調用了child的layout方法。而這裏的layout方法傳遞的r和b參數,對應的是l+measureWidth和t+measureHeight計算出來的。所以通常情況下,調用View的getWidth方法(返回的是r-l)和getMeasureWidth方法,其返回值是一致的,即measureWidth的值。而在一些情況下,多次進行measure會導致layout階段與measure階段的width不同,但總的來說兩者基本上是相等的。height也是一樣的情況。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); } } }
private void setChildFrame(View child, int left, int top, int width, int height) { child.layout(left, top, left + width, top + height); }
- draw
- 流程:
- 繪製背景:background.draw(canvas)
- 繪製自己:onDraw
- 繪製children:dispatchDraw->child.draw
- 繪製裝飾:onDrawScrollBars
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); drawAutofilledHighlight(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); // Step 7, draw the default focus highlight drawDefaultFocusHighlight(canvas); if (debugDraw()) { debugDrawFocus(canvas); } // we're done... return; }
- tips
setWillNotDraw方法:該方法用於設置表示其是否會進行draw,以便系統進行優化。ViewGroup會默認設置爲true,所以如果一個自定義VIewGroup有draw的需求,要將其設置爲false。
- 流程:
4.自定義View
- 分類
- 繼承View
實現一些特殊的效果,需要重寫onDraw。同時需要處理wrap_content和padding。 - 繼承ViewGroup
實現自定義佈局,實現組合的效果。需要處理自身的measure、layout,以及children的measure、layout。 - 繼承特定View
擴展某個已有View的功能。wrap_content和padding不需要自己處理。 - 繼承特定ViewGroup
實現組合的效果。不需要處理measure、layout。
- 繼承View
- 自定義View須知
- 處理wrap_content
直接繼承View或ViewGroup,默認的onMeasure無法正確處理wrap_content。 - 處理padding
直接繼承View需要在draw方法中處理padding;直接繼承ViewGroup需要在onMeasure和onLayout中考慮padding和margin的影響。 - 儘量不要在View中使用Handler
View內部提供的post系列方法可以滿足需求。 - View中的線程和動畫,需要及時停止
當View不可見時,需要及時停止,否則可能造成內存泄漏。可以根據回調方法去處理。當View被remove或者VIew所在的Activity退出時,View#onDetachedFromWindow會被調用;當View的Activity啓動時,View#onAttactedToWindow會被調用。 - VIew嵌套滾動時,處理好滾動衝突
- 處理wrap_content