介紹View的工作原理之前,先來介紹一些基礎的概念,以便后面詳細的介紹View的三大流程:measure、layout和draw。
1.ViewRoot和DecorView
ViewRoot具體對應的類是ViewRootImpl,該類是鏈接WindowManager與DecorView的紐帶,View的三大流程都是通過ViewRoot來完成的。ActivityThread中,Activity被創建之后,會把DecorView添加到Window中,同時創建ViewRootImpl對象,并給ViewRootImpl跟DecorView建立關聯。
View的繪制是從ViewRoot的performTraversals方法開始的,經過measure、layout和draw三個過程View才最終繪制出來。measure用來測量View的寬高,layout用來確定View在父容器中的位置,draw很明顯是把View繪制在屏幕上。
如圖所示,performTraversals會依次調用performMeasure、performLayout和performDraw三個方法,這三個方法分別完成頂級View的measure、layout、draw這個三個流程。其中:
1.perfromMeasure中會調用measure方法,在measure方法中又會調用onMeasure方法,在onMeasure方法中會對所有的子元素進行measure過程,這個時候measure流程就從父容器傳遞到子元素中了,這樣就完成了一次measure過程。接著子元素會重復父元素的measure過程,如此反復就完成整個View樹的遍歷。
2.performLayout的傳遞流程和performMeasure是一樣的。
3.performDraw的傳遞過程是在draw方法中通過dispathDraw來實現的,本質上并沒有區別。
measure過程決定了View的寬高,Measure完成以后,可以通過getMeasuredWidth和getMeasuredHeight方法來獲取到View測量后的寬高,在幾乎所有的情況下它都等于View的最終寬高,這僅僅是在代碼規范的前提之下。
layout最終決定了View的四個頂點的坐標和實際View的寬/高,完成以后,可以通過getTop、getBottom、getLeft、getRight來拿到View的四個頂點坐標位置,并可以通過getWidth和getHeight來得到View的最終寬高
draw過程決定了View的顯示,只有draw方法完成以后View的內容才會最終顯示在屏幕上。
簡單介紹了三大過程之后,我們來看下DecorView的結構:
DecorView作為頂級View一般會包含一個垂直的LinearLayout,通常在這上面會有上下兩部分(也需要根據具體情況來定),上面是標題欄,下面是內容欄。我們在Activity中setContentView就是添加在內容欄中,內容欄的id是content。大家也可以猜到,View層的時間都是先經過DecorView然后才會傳給其他的View。
2.MeasureSpec
為了更好的了解View的測量過程,我們下面介紹下MeasureSpec。MeasureSpec在很大程度上決定了一個View的尺寸規格,當然除了MeasureSpec還會受到父容器的影響。
MeasureSpec的中文意思是測量規格的意思,MeasureSpec代表一個32的int值,高2位代表SpecMode測量模式,低30位代表SpecSize測量規格大小。
經常使用的三個函數:
1.public static int makeMeasureSpec(int size,int mode)
構造一個MeasureSpec
2.public static int getMode(int measureSpec)
獲取MeasureSpec的測量模式
3.public static int getSize(int measureSpec)
獲取MeasureSpec的測量大小
SpecMode分為三類,每一類都沒有他們對應的約束。
1.UNSPECIFIED
父容器不對View有任何限制,要多大就給多大,這種情況,一般我們自定義View用不到。
2.Exactly
這個表示準確值,這個對應著LayoutParams中的matchparent和準確值,這個時候View的最終大小就是SpecSize所指定的數。
3.AT_MOST
父容器指定了一個可用大小即SpecSize,View的大小不能大于該值,具體是什么要看View中自己的處理,所有自定義View時,我們需要自己在measure里面處理設置布局的大小,它對應layoutparams中的wrap_content
這里還要提一下MeasureSpec跟LayoutParams的對應關系。
使用MeasureSpec來進行View的測量,但是正常情況下我們使用View指定MeasureSpec,盡管如此,但是我們可以給View設置layoutparams,在View測量的時候,系統會將LayoutParams在父容器的約束下自動轉化成對應的MeasureSpec,然后再根據MeasureSpec來確定View最終的寬高。
MeasureSpec不是由LayoutParams唯一決定的,子View的寬高由自身的layoutparams和父容器的MeasureSpec。
對于DecorView,其MeasureSpec由窗口的尺寸和自身的Layoutparams決定;
對于普通的View,其MeasureSpec由父容器的MeasureSpec和自身的Layoutparams共同決定;
遵循的規則:
1.LayoutParams.MATCH_PARENT:精確模式,大小就是窗口的大小
2.LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超過窗口的大小
3.固定大小(比如100dp):精確模式,大小為LayoutParams中指定的大小
**4.當view采用固定的寬高時,不管父容器的MeasureSpec是什么,View的MeasureSpec都是精確模式并且大小遵循LayoutParams的大小
5.當View的寬高時matchparent時,如果父容器是精確模式,那么View也是精確模式,并且View也是精確模式并且大小是父容器的剩余空間。如果父容器是最大模式,那么View也是最大模式但是大小不能超過剩余的空間。
6.當View是wrap_content,那么不管父容器的模式,View一定是最大模式,但是不能超過父容器的剩余空間。
3.View的工作流程
1.Measure
measure過程要分情況來看,如果只是一個原始的View,那么通過measure方法就完成了其測量過程,如果是一個ViewGroup,除了完成自己的測量過程外,還會遍歷去調用所有子元素的measure方法,各個子元素再遞歸去執行這個流程。下面我們再分別介紹下這兩種情況下的過程。
A.View的Measure過程,上源碼(源碼有方法的注釋,對理解方法有很大的幫助,有興趣可以看下):
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
上面代碼看起來很簡潔,但是我們繼續看里面的getDefaultSize方法。
/**
* Utility to return a default size. Uses the supplied size if the
* MeasureSpec imposed no constraints. Will get larger if allowed
* by the MeasureSpec.
*
* @param size Default size for this view
* @param measureSpec Constraints imposed by the parent
* @return The size this view should be.
*/
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;
}
這里我們看到了上面setMeasuredDimension設置寬高的測量值的具體方法,getDefaultSize方法邏輯也很簡單,我們重點看下AT_MOST跟EXACTLY這兩種情況。簡單地理解,其實getDefaultSize返回的大小就是measureSpec中的specSize,而這個specSize就是View測量后的大小,這里多次提到測量后的大小,是因為View最終的大小是在layout階段確定的,所以這里必須要加以區分,但是幾乎所有情況下View的測量大小和最終大小是相等的。
UNSPECIFIED這種情況取值是通過getSuggestedMinimumHeight跟getSuggestedMinimumWidth,我們看下源碼:
/**
* Returns the suggested minimum height that the view should use. This
* returns the maximum of the view's minimum height
* and the background's minimum height
* ({@link android.graphics.drawable.Drawable#getMinimumHeight()}).
* <p>
* When being used in {@link #onMeasure(int, int)}, the caller should still
* ensure the returned height is within the requirements of the parent.
*
* @return The suggested minimum height of the view.
*/
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
/**
* Returns the suggested minimum width that the view should use. This
* returns the maximum of the view's minimum width
* and the background's minimum width
* ({@link android.graphics.drawable.Drawable#getMinimumWidth()}).
* <p>
* When being used in {@link #onMeasure(int, int)}, the caller should still
* ensure the returned width is within the requirements of the parent.
*
* @return The suggested minimum width of the view.
*/
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
以getSuggestedMinimumHeight為例如果View沒有設置背景,那么View的高度為mMinHeight,而mMinWidth對應于android:minHeight這個屬性所指定的值,因此View的寬度即為android:minHeight屬性所指定的值。這個屬性如果不指定,那么mMinHeight則默認為0;如果View指定了背景,則View的寬度為max(mMinHeight,mBackground.getMinimumHeight())。mMinHeight的含義我們已經知道了,那么mBackground.getMinimumHeight()是什么呢?我們看一下Drawable的getMinimumHeight方法
/**
* Returns the minimum height suggested by this Drawable. If a View uses this
* Drawable as a background, it is suggested that the View use at least this
* value for its height. (There will be some scenarios where this will not be
* possible.) This value should INCLUDE any padding.
*
* @return The minimum height suggested by this Drawable. If this Drawable
* doesn't have a suggested minimum height, 0 is returned.
*/
public int getMinimumHeight() {
final int intrinsicHeight = getIntrinsicHeight();
return intrinsicHeight > 0 ? intrinsicHeight : 0;
}
可以看出,getMinimumWidth返回的就是Drawable的原始寬度,前提是這個Drawable有原始寬度,否則就返回0。那么Drawable在什么情況下會有原始寬度?這個是關于Drawable的問題,我們這里按下不表。
getSuggestedMinimumWidth的邏輯:如果View沒有設置背景,那么返回android:minWidth這個屬性所指定的值,這個值可以為0;如果View設置了背景,則返回android:minWidth和背景的最小寬度這兩者中的最大值,getSuggestedMinimumWidth和getSuggestedMinimumHeight的返回值就是View在UNSPECIFIED情況下的測量寬/高。
這里再說明一個問題,從getDefaultSize方法可以看出:直接繼承View的自定義控件需要重寫onMeasure方法并設置wrap_content時的自身大小,否則在布局中使用wrap_content就相當于使用match_parent。
Why??下面我們分析一下。
如果View在布局中使用wrap_content,那么它的specMode是AT_MOST模式,在這種模式下,它的寬/高等于specSize;根據上面普通View的MeasureSpec創建規則,這種情況下View的specSize是parentSize,而parentSize是父容器中目前可以使用的大小,也就是父容器當前剩余的空間大小。很顯然,View的寬/高就等于父容器當前剩余的空間大小,這種效果和在布局中使用match_parent完全一致。簡單點說就是你不處理,wrap_content跟match_parent就是一樣的效果。
那要怎么處理呢?我們先看下TextView是怎么做的。
......
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
......
這里的widthSize、heightSize是默認值也就是測量大小,width、height是最終值,我們繼續往下看。
if (widthMode == MeasureSpec.EXACTLY) {
// Parent has told us how big to be. So be it.
width = widthSize;
} else {
if (mLayout != null && mEllipsize == null) {
des = desired(mLayout);
}
if (des < 0) {
boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
if (boring != null) {
mBoring = boring;
}
} else {
fromexisting = true;
}
if (boring == null || boring == UNKNOWN_BORING) {
if (des < 0) {
des = (int) Math.ceil(Layout.getDesiredWidth(mTransformed, 0,
mTransformed.length(), mTextPaint, mTextDir));
}
width = des;
} else {
width = boring.width;
}
final Drawables dr = mDrawables;
if (dr != null) {
width = Math.max(width, dr.mDrawableWidthTop);
width = Math.max(width, dr.mDrawableWidthBottom);
}
if (mHint != null) {
int hintDes = -1;
int hintWidth;
if (mHintLayout != null && mEllipsize == null) {
hintDes = desired(mHintLayout);
}
if (hintDes < 0) {
hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir, mHintBoring);
if (hintBoring != null) {
mHintBoring = hintBoring;
}
}
if (hintBoring == null || hintBoring == UNKNOWN_BORING) {
if (hintDes < 0) {
hintDes = (int) Math.ceil(Layout.getDesiredWidth(mHint, 0, mHint.length(),
mTextPaint, mTextDir));
}
hintWidth = hintDes;
} else {
hintWidth = hintBoring.width;
}
if (hintWidth > width) {
width = hintWidth;
}
}
width += getCompoundPaddingLeft() + getCompoundPaddingRight();
if (mMaxWidthMode == EMS) {
width = Math.min(width, mMaxWidth * getLineHeight());
} else {
width = Math.min(width, mMaxWidth);
}
if (mMinWidthMode == EMS) {
width = Math.max(width, mMinWidth * getLineHeight());
} else {
width = Math.max(width, mMinWidth);
}
// Check against our minimum width
width = Math.max(width, getSuggestedMinimumWidth());
if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(widthSize, width);
}
}
這個if/else中間省略了一部分代碼,但是大家可以看到,wrap_content這種情況下,給了一個默認值,計算過程,大家有興趣可以具體看下,其實也就是說,解決這個問題只需要給一個默認值,但是這個默認值大小并沒有固定的依據,我覺得根據自己實際的View的使用情況來定即可。
B.ViewGroup的measure過程
ViewGroup除了完成自己的Measure之外還會遍歷子View的Measure方法,每個子View再遞歸的完成這個過程。ViewGroup是一個抽象類,因此并沒有重寫onMeasure方法,但是提供了measureChildren方法,源碼如下:
/**
* Ask all of the children of this view to measure themselves, taking into
* account both the MeasureSpec requirements for this view and its padding.
* We skip children that are in the GONE state The heavy lifting is done in
* getChildMeasureSpec.
*
* @param widthMeasureSpec The width requirements for this view
* @param heightMeasureSpec The height requirements for this view
*/
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);
}
}
}
/**
* Ask one of the children of this view to measure itself, taking into
* account both the MeasureSpec requirements for this view and its padding.
* The heavy lifting is done in getChildMeasureSpec.
*
* @param child The child to measure
* @param parentWidthMeasureSpec The width requirements for this view
* @param parentHeightMeasureSpec The height requirements for this view
*/
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);
}
可以看到,ViewGroup會對每個子View進行measure,具體可以看下measureChild方法,如上圖。很顯然,measureChild的思想就是取出子元素的LayoutParams,然后再通過getChildMeasureSpec來創建子元素的MeasureSpec,接著將MeasureSpec直接傳遞給View的measure方法來進行測量。getChildMeasureSpec的工作過程已經在上面進行了詳細分析。
ViewGroup的Measure方法可以看下LinearLayout
@Override
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);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == View.GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
nonSkippedChildCount++;
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
// Optimization: don't bother measuring children who are only
// laid out using excess space. These views will get measured
// later if we have space to distribute.
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;
} else {
if (useExcessSpace) {
// The heightMode is either UNSPECIFIED or AT_MOST, and
// this child is only laid out using excess space. Measure
// using WRAP_CONTENT so that we can find out the view's
// optimal height. We'll restore the original height of 0
// after measurement.
lp.height = LayoutParams.WRAP_CONTENT;
}
// 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 childHeight = child.getMeasuredHeight();
if (useExcessSpace) {
// Restore the original height and record how much space
// we've allocated to excess-only children so that we can
// match the behavior of EXACTLY measurement.
lp.height = 0;
consumedExcessSpace += childHeight;
}
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
if (useLargestChild) {
largestChildHeight = Math.max(childHeight, largestChildHeight);
}
}
......
從上面這段代碼可以看出,系統會遍歷子元素并對每個子元素執行measureChild-BeforeLayout方法,這個方法內部會調用子元素的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;
// Either expand children with weight to take up available space or
// shrink them if they extend beyond our current bounds. If we skipped
// measurement on any children, we need to measure them now.
int remainingExcess = heightSize - mTotalLength
+ (mAllowInconsistentMeasurement ? 0 : consumedExcessSpace);
......
if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
maxWidth = alternativeMaxWidth;
}
maxWidth += mPaddingLeft + mPaddingRight;
// Check against our minimum width
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
if (matchWidth) {
forceUniformWidth(count, heightMeasureSpec);
}
......
從上面可以看出,當子元素測量完畢后,LinearLayout會根據子元素的情況來測量自己的大小。針對豎直的LinearLayout而言,它在水平方向的測量過程遵循View的測量過程,在豎直方向的測量過程則和View有所不同。具體來說是指,如果它的布局中高度采用的是match_parent或者具體數值,那么它的測量過程和View一致,即高度為specSize;如果它的布局中高度采用的是wrap_content,那么它的高度是所有子元素所占用的高度總和,但是仍然不能超過它的父容器的剩余空間,當然它的最終高度還需要考慮其在豎直方向的padding,這個過程可以進一步參看如下源碼:
/**
* Utility to reconcile a desired size and state, with constraints imposed
* by a MeasureSpec. Will take the desired size, unless a different size
* is imposed by the constraints. The returned value is a compound integer,
* with the resolved size in the {@link #MEASURED_SIZE_MASK} bits and
* optionally the bit {@link #MEASURED_STATE_TOO_SMALL} set if the
* resulting size is smaller than the size the view wants to be.
*
* @param size How big the view wants to be.
* @param measureSpec Constraints imposed by the parent.
* @param childMeasuredState Size information bit mask for the view's
* children.
* @return Size information bit mask as defined by
* {@link #MEASURED_SIZE_MASK} and
* {@link #MEASURED_STATE_TOO_SMALL}.
*/
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過程是三大流程中最復雜的一個,measure完成以后,通過getMeasured-Width/Height方法就可以正確地獲取到View的測量寬/高。需要注意的是,在某些極端情況下,系統可能需要多次measure才能確定最終的測量寬/高,在這種情形下,在onMeasure方法中拿到的測量寬/高很可能是不準確的。一個比較好的習慣是在onLayout方法中去獲取View的測量寬/高或者最終寬/高。
這里引申一個問題,Activity啟動之后,某個任務需要View的寬高,如何獲取View的寬高呢?
這個場景還是很常見的,我們這里先分析下上面這個問題,我們知道,onCreate、onStart、onResume中均無法正確得到某個View的寬/高信息,因為View的Measure過程跟Activity的生命周期不是同步執行的,有沒有辦法解決這個問題呢?肯定是有的,下面我們就介紹幾種方法。
(1)Activity/View#onWindowFocusChanged。
onWindowFocusChanged這個方法的含義是:View已經初始化完畢了,寬/高已經準備好了,這個時候去獲取寬/高是沒問題的。需要注意的是,onWindowFocusChanged會被調用多次,當Activity的窗口得到焦點和失去焦點時均會被調用一次。具體來說,當Activity繼續執行和暫停執行時,onWindowFocusChanged均會被調用,如果頻繁地進行onResume和onPause,那么onWindowFocusChanged也會被頻繁地調用。
2)view.post(runnable)。通過post可以將一個runnable投遞到消息隊列的尾部,然后等待Looper調用此runnable的時候,View也已經初始化好了。
3)ViewTreeObserver。使用ViewTreeObserver的眾多回調可以完成這個功能,比如使用OnGlobalLayoutListener這個接口,當View樹的狀態發生改變或者View樹內部的View的可見性發現改變時,onGlobalLayout方法將被回調,因此這是獲取View的寬/高一個很好的時機。需要注意的是,伴隨著View樹的狀態改變等,onGlobalLayout會被調用多次。
(4)view.measure。通過手動對View進行measure來得到View的寬/高。這種方法比較復雜,這里要分情況處理,根據View的LayoutParams來分。
上面四個方法具體可以參考 任玉剛.Android開發藝術探索.電子工業出版社 4.3.1
2.Layout
Layout的作用是ViewGroup用來確定子元素的位置,當ViewGroup的位置被確定后,它在onLayout中會遍歷所有的子元素并調用其layout方法,在layout方法中onLayout方法又會被調用。Layout過程和measure過程相比就簡單多了,layout方法確定View本身的位置,而onLayout方法則會確定所有子元素的位置,先看View的layout方法,如下所示。
/**
* Assign a size and position to a view and all of its
* descendants
*
* <p>This is the second phase of the layout mechanism.
* (The first is measuring). In this phase, each parent calls
* layout on all of its children to position them.
* This is typically done using the child measurements
* that were stored in the measure pass().</p>
*
* <p>Derived classes should not override this method.
* Derived classes with children should override
* onLayout. In that method, they should
* call layout on each of their children.</p>
*
* @param l Left position, relative to parent
* @param t Top position, relative to parent
* @param r Right position, relative to parent
* @param b Bottom position, relative to parent
*/
@SuppressWarnings({"unchecked"})
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);
if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
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;
if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
}
layout方法的大致流程如下:
(1)通過setFrame方法來設定View的四個頂點的位置,即初始化mLeft、mRight、mTop和mBottom這四個值,View的四個頂點一旦確定,那么View在父容器中的位置也就確定了。
(2)接著會調用onLayout方法,這個方法的用途是父容器確定子元素的位置,和onMeasure方法類似,onLayout的具體實現同樣和具體的布局有關,所以View和ViewGroup均沒有真正實現onLayout方法。
接下來,我們可以看一下LinearLayout的onLayout方法,如下所示。
@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);
}
}
我們具體看下layoutVertical:
/**
* Position the children during a layout pass if the orientation of this
* LinearLayout is set to {@link #VERTICAL}.
* @see #getOrientation()
* @see #setOrientation(int)
* @see #onLayout(boolean, int, int, int, int)
* @param left
* @param top
* @param right
* @param bottom
*/
void layoutVertical(int left, int top, int right, int bottom) {
final int paddingLeft = mPaddingLeft;
int childTop;
int childLeft;
// Where right end of child should go
final int width = right - left;
int childRight = width - mPaddingRight;
// Space available for child
int childSpace = width - paddingLeft - mPaddingRight;
final int count = getVirtualChildCount();
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
switch (majorGravity) {
case Gravity.BOTTOM:
// mTotalLength contains the padding already
childTop = mPaddingTop + bottom - top - mTotalLength;
break;
// mTotalLength contains the padding already
case Gravity.CENTER_VERTICAL:
childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
break;
case Gravity.TOP:
default:
childTop = mPaddingTop;
break;
}
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();
int gravity = lp.gravity;
if (gravity < 0) {
gravity = minorGravity;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
childLeft = childRight - childWidth - lp.rightMargin;
break;
case Gravity.LEFT:
default:
childLeft = paddingLeft + lp.leftMargin;
break;
}
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);
}
引申一個問題View的測量寬/高和最終/寬高有什么區別?
這個問題實際上是:View的getMeasuredWidth和getWidth這兩個方法有什么區別?回答這個問題我們還是上源碼,看一下getwidth和getHeight這兩個方法的具體實現:
/**
* Return the width of the your view.
*
* @return The width of your view, in pixels.
*/
@ViewDebug.ExportedProperty(category = "layout")
public final int getWidth() {
return mRight - mLeft;
}
/**
* Return the height of your view.
*
* @return The height of your view, in pixels.
*/
@ViewDebug.ExportedProperty(category = "layout")
public final int getHeight() {
return mBottom - mTop;
}
其實看出,在View的默認實現中,View的測量寬/高和最終寬/高是相等的,只不過測量寬/高形成于View的measure過程,而最終寬/高形成于View的layout過程,即兩者的賦值時機不同,測量寬/高的賦值時機稍微早一些。因此,在日常開發中,我們可以認為View的測量寬/高就等于最終寬/高,但是的確存在某些特殊情況會導致兩者不一致,也就是在layout過程中改變了寬高會導致最終的寬高發生變化。
3.Draw
Draw過程就比較簡單了,很明顯它的作用是將View繪制到屏幕上面。先上源碼:
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;
}
可以看到注釋里有View的繪制過程概括下遵循如下幾步:
(1)繪制背景background.draw(canvas)。
(2)繪制自己(onDraw)。
(3)繪制children(dispatchDraw)。
(4)繪制裝飾(onDrawScrollBars)。
View繪制過程的傳遞是通過dispatchDraw來實現的,dispatchDraw會遍歷調用所有子元素的draw方法,如此draw事件就一層層地傳遞了下去。View有一個特殊的方法setWillNotDraw。
/**
* If this view doesn't do any drawing on its own, set this flag to
* allow further optimizations. By default, this flag is not set on
* View, but could be set on some View subclasses such as ViewGroup.
*
* Typically, if you override {@link #onDraw(android.graphics.Canvas)}
* you should clear this flag.
*
* @param willNotDraw whether or not this View draw on its own
*/
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
...
//ViewGroup
private void initViewGroup() {
// ViewGroup doesn't draw by default
if (!debugDraw()) {
setFlags(WILL_NOT_DRAW, DRAW_MASK);
}
...
從源碼可以看出,如果一個View不需要繪制任何內容,那么設置這個標記位為true以后,系統會進行相應的優化。從上面View跟ViewGroup的代碼也可以看到,View默認是沒有啟用這個優化的,而ViewGroup是默認啟用了。這里就引申出一個問題:當我們的自定義控件繼承于ViewGroup并且本身不具備繪制功能時,就可以開啟這個標記位從而便于系統進行后續的優化。同樣,當明確知道一個ViewGroup需要通過onDraw來繪制內容時,我們需要顯式地關閉WILL_NOT_DRAW這個標記位。
擴展:View的重繪與更新
我們知道View的重繪與更新,可以調用invalidate方法或者是requestLayout方法,兩者有什么區別呢?我們看下源碼注釋:
/**
* Invalidate the whole view. If the view is visible,
* {@link #onDraw(android.graphics.Canvas)} will be called at some point in
* the future.
* <p>
* This must be called from a UI thread. To call from a non-UI thread, call
* {@link #postInvalidate()}.
*/
public void invalidate() {
invalidate(true);
}
/**
* This is where the invalidate() work actually happens. A full invalidate()
* causes the drawing cache to be invalidated, but this function can be
* called with invalidateCache set to false to skip that invalidation step
* for cases that do not need it (for example, a component that remains at
* the same dimensions with the same content).
*
* @param invalidateCache Whether the drawing cache for this view should be
* invalidated as well. This is usually true for a full
* invalidate, but may be set to false if the View's contents or
* dimensions have not changed.
* @hide
*/
public void invalidate(boolean invalidateCache) {
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}
......
/**
* Call this when something has changed which has invalidated the
* layout of this view. This will schedule a layout pass of the view
* tree. This should not be called while the view hierarchy is currently in a layout
* pass ({@link #isInLayout()}. If layout is happening, the request may be honored at the
* end of the current layout pass (and then layout will run again) or after the current
* frame is drawn and the next layout occurs.
*
* <p>Subclasses which override this method should call the superclass method to
* handle possible request-during-layout errors correctly.</p>
*/
@CallSuper
public void requestLayout() {
......
從上面源碼的注釋中可以看到,invalidate方法是重新繪制更新View,重新調用draw方法。requestLayout方法則是只調用Measure跟Layout方法。invalidate的注釋也寫了,其只能是UI線程調用,非UI線程需要用postInvalidate方法。一般的,View如果appearance發生改變,重新調用invalidate方法就可以,如果View是位置、大小發生變化則需要調用requestLayout。兩者都有變化,一般先調用requestLayout再調用invalidate。
到這里View的工作原理就介紹完了,如果大家想自定義自己的View相信會從上面的介紹中有心得體會。
參考:任玉剛.Android開發藝術探索.電子工業出版社