編輯:關於Android編程
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和LayoutParams的對應關系
對於DecorView,其MeasureSpec由其窗口的尺寸和其自身的LayoutParams來共同確定;對於普通view,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams來共同決定,MeasureSpec一旦確定後,onMeasure就可以確定View的測量寬/高。
DecorView的MeasureSpec產生過程根據LayoutParams中的寬和高來劃分:
普通View來說,針對不同的父容器和View本身不同的LayoutParams,View就可以有多種MeasureSpec。
當View采用固定寬高的時候,不管父容器的MeasureSpec是什麼,View的MeasureSpec都是精確模式並且其大小遵循LayoutParams的大小。 當View的寬高是match_parent時,如果父容器的模式是精確模式,那麼view也是精確模式並且其大小是父容器的剩余空間;如果父容器是最大模式,那麼View也是最大模式並且其大小不會超過父容器的剩余空間。 當view的寬高是wrap_content時,不管父容器的模式是精確還是最大化嗎,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過程以外,還會遍歷去調用所有子元素的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#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來分:
具體數值(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) { ArrayListlistenersCopy = (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;
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的繪制過程遵循如下幾步:
可以通過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重寫onDraw方法自定義view須知
讓View支持wrap_content 如果有必要,View支持paddingManifest 文件 詳解 本文地址: http://blog.csdn.net/caroline_wendy/article/details/20899281
在Activity中覆寫下面兩個方法:復制代碼 代碼如下: // 創建菜單 @Override public boo
一、網絡爬蟲的基本知識網絡爬蟲通過遍歷互聯網絡,把網絡中的相關網頁全部抓取過來,這體現了爬的概念。爬蟲如何遍歷網絡呢,互聯網可以看做是一張大圖,每個頁面看做其中的一個節點
具體情況是使用刷機精靈安裝驅動的時候提示如下錯誤 解決辦法如下: 1、下載安裝豌豆莢,用豌豆莢查找安裝手機驅動,連上後退出豌豆莢,用刷機精靈刷機。 2