編輯:關於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) {
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;
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支持padding
Android - Manifest 文件 詳解
Manifest 文件 詳解 本文地址: http://blog.csdn.net/caroline_wendy/article/details/20899281
在Android中創建菜單項Menu以及獲取手機分辨率的解決方法
在Activity中覆寫下面兩個方法:復制代碼 代碼如下: // 創建菜單 @Override public boo
Android編寫簡單的網絡爬蟲
一、網絡爬蟲的基本知識網絡爬蟲通過遍歷互聯網絡,把網絡中的相關網頁全部抓取過來,這體現了爬的概念。爬蟲如何遍歷網絡呢,互聯網可以看做是一張大圖,每個頁面看做其中的一個節點
刷機精靈一鍵刷機驅動安裝失敗怎麼辦|刷機精靈無法安裝驅動怎麼辦
具體情況是使用刷機精靈安裝驅動的時候提示如下錯誤 解決辦法如下: 1、下載安裝豌豆莢,用豌豆莢查找安裝手機驅動,連上後退出豌豆莢,用刷機精靈刷機。 2