概要
Activity
界面的根布局其實是一個名為
DecorView
的
FrameLayout
, 在建立
DecorView
的時候,會相應的建立一個
ViewRootImpl
類來控制 UI 的繪制,其實也就是控制
DecorView
。繪制的過程分為了
measure
,
layout
,
draw
三個過程。 以下分别對
View
和
ViewGroup
三個過程進行說明。
View 繪制
measure
public final void measure(int widthMeasureSpec, int heightMeasureSpec)
兩個參數
widthMeasureSpec
,
heightMeasureSpec
是
parent
提供過來的,
parent
用這兩個參數告訴
View
,我希望你的寬高是多大。這兩個 參數可以由
View
中的一個靜态類
MeasureSpec
來解析,
MeasureSpec
是由
size
和
mode
組成,可由下面方法獲得。
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
size
很好了解 ,就是
parent
給
Child View
的大小。 而
mode
呢,有三種形式。
-
:UNSPECIFIED
對parent
沒有任何限制,Child View
想多大就多大。Child View
-
:EXACTLY
對parent
說,我隻希望是這個固定的大小,也就是上面代碼解析出來的child view
。specSize
-
:AT_MOST
告訴parent
,你最大繪制範圍就是child view
。specSize
那麼
View.onMeasure()
就是利用
paren
t 傳過來的關于寬高限制的參數來決定自己的寬度 ,然而可以看到這個方法是
final
,不允許重寫的。 當然,實際的測量是在
View.onMeasure()
方法中。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
系統調用了
setMeasuredDimension()
,這是測量之後必須要調用的方法,它用來存儲的測量的寬高,也就是
getMeasuredWidth()
和
getMeasuredHeight()
的值。
系統的實作中還用到了
getSuggestedMinimumWidth()
和
getSuggestedMinimumHeight()
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
為何要調用這個?有時候,我可能設定了
android:backgound
或者
android:minWidth
,
android:minHeight
,是以我們自定義的
View
不應該小于這個值吧。
當然我們還想知道,系統的預設實作中設定的測量寬高是什麼呢,這個是要看
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;
}
如果解析出來的
mode
為
UNSPECIFIED
,也就說
parent
對
Child View
沒有限制,那就用
getSuggestedMinimumXXX()
的值。
如果解析出來的
mode
為
AT_MOST
或
EXACTLY
, 就用
parent
提供過來的大小。 那也就是說,如果調用系統的方法
super.onMeasure()
進行測試,預設就是填充整個父布局。 我們自定義
View
或
ViewGroup
的時候,有時候并不想這樣,是以需要自己測量,最後調用
setMeasuredDimension()
方法。
draw
@CallSuper
public void draw(Canvas canvas) {
// ...
/*
* 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);
}
// Step 2, save the canvas' layers
// ...
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
// ...
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
}
View.draw()
方法并不是
final
的,是以我們可以重寫它,不過一定記得調用
super.draw(canvas)
, 因為系統幫我們繪制了
background
,
scrollBar
,而且最重要的是,如果是
ViewGroup
的話,會調用
dispatchDraw()
來讓
Child View
進行繪制。這也正是
@CallSuper
注解的意思。
從代碼中的
step 3
可以看出,
onDraw()
才是繪制自己的内容區域。而
View
的
onDraw()
方法是一個空方法
/**
* Implement this to do your drawing.
*
* @param canvas the canvas on which the background will be drawn
*/
protected void onDraw(Canvas canvas) {
}
layout
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != ) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
}
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);
}
}
View
的
layout()
是
parent
(
ViewGroup
) 調用,用來确定
Child View
的位置,順便也确定了實際大小。如果是自定義 View 的話,完全沒必要在意這個方法,當然也不用在意在
View.layout()
調用的
View.onLayout()
方法。而在實際中,隻需要注意
onSizeChange()
方法,這個方法告訴你,你的
View
的大小發生了改變。而
onSizeChange()
就是在
setOpticalFrame()
或者
setFrame()
中調用的,而這個方法是
layout
過程的關鍵所在 ,它們決定了
View
的實際寬高,我們取其中一個方法看看樣子
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
// 檢測大小是否已經改變
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
// Invalidate our old position
invalidate(sizeChanged);
// 更新坐标值
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
if (sizeChanged) {
// 調用了 onSizeChanged()
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
}
return changed;
}
setFrame()
檢測了
View
的大小是否改變了,然後更新了坐标值。 坐标值的改變也意味着
getWidth()
和
getHeight()
的值可以确定了。
public final int getWidth() {
return mRight - mLeft;
}
public final int getHeight() {
return mBottom - mTop;
}
如果大小改變了,就調用
sizeChange()
方法
private void sizeChange(int newWidth, int newHeight, int oldWidth, int oldHeight) {
onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);
// ...
}
在
sizeChange()
方法中首先就調用了
onSizeChanged()
方法。
那麼我們梳理下
layout
過程,
layout()
->
onSizeChanged()
->
onLayout()
。
ViewGroup 繪制
measure
View
的
measure()
方法為
final
類型,是以子類不能複寫這個方法,隻能複寫
onMeasure()
方法,那麼
ViewGroup
作為
View
的子類,也不例外。
我們還可以知道,
onMeasure()
是要完成自身的測量,并通過調用
setMeasuredDimension()
方法來存儲測量的結果。 然而對于
ViewGroup
來說,它需要提供一個
MeasureSpec
給它的每個
Child View
用來完成測量,然後根據
Child View
測量結果來決定自身的測量值。
ViewGroup
用來測量
Child View
的方法有三個,分别是
measureChild()
,
measureChildren()
,
measureChildWithMargins()
。
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);
}
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(, specSize - padding);
int resultSize = ;
int resultMode = ;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= ) {
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 >= ) {
// 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 >= ) {
// 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 = View.sUseZeroUnspecifiedMeasureSpec ? : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
首先整體觀看
measureChild()
方法,它隻考慮了
padding
,而并沒有考慮經常會出現的
margin
,是以,如果自定義
View
的時候沒有
margin
可以考慮用這個方法完成
Child View
的測量。
measureChild()
是調用
getChildMeasureSpec()
來為
Child View
生成相應的
MeasureSpec
,它的第二個參數雖然名為
padding
,但是在實際使用的時候,它其實在更嚴格的意義上講,是指已經使用的長度或寬度。
mesureChildren()
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = ; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
measureChildren()
在原理還是調用了
measureChild()
方法,隻是有一點需要注意,如果
Child View
的可見性為
GONE
,是不會測量的。
measureChildWithMargins()
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);
}
measureChildWithMargins()
這個方法就比較全面,首先考慮了
padding
和
margin
,其次它需要傳入兩個參數,分為代表已經使用過的寬度和高度,這樣我們就很友善計算并排的
Child View
的寬高。
FrameLayout.onMeasure()
說了這麼多很空泛,舉個例子。 我們知道,根布局其實就是一個名為
DecorView
的
FrameLayout
,它的
onMeasure()
方法的部分源碼如下
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
mMatchParentChildren.clear();
int maxHeight = ;
int maxWidth = ;
int childState = ;
for (int i = ; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, , heightMeasureSpec, );
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
//...
}
}
// ...
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
// ...
}
在第
17
行,它使用的是
measureChildWithMargins()
方法來完成
Child View
的測量,由于
FrameLayout
設計為層層覆寫的特性,是以
measureChildWithMargins()
的第三個參數和第五個參數都為0,代表永遠沒有使用過的寬度來高度,隻考慮
padding
和
margin
即可。
layout
@Override
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {
mTransition.layoutChange(this);
}
super.layout(l, t, r, b);
} else {
// record the fact that we noop'd it; request layout when transition finishes
mLayoutCalledWhileSuppressed = true;
}
}
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
ViewGroup
的
layout()
方法其實也是調用
View
的
layout()
完成布局的。 然而我們注意到
ViewGroup
的
onLayout
是一個抽象方法,子類必須實作,這也是提示我們,自定義
ViewGroup
就有責任為
Child View
布局。
draw
ViewGroup
如果自身沒有特别的繪制需要,就不需要複寫
draw()
或者
onDraw()
方法,它的重點在布局。
draw()
方法會繪制一些基本的東西,例如背景,滾動條等等,是以如果想複寫
draw()
方法記得調用
super.draw()
,而如果複寫
onDraw()
隻需要完成自己的繪制即可 。
參考文章
https://developer.android.google.cn/training/custom-views/create-view.html
https://realm.io/news/360andev-huyen-tue-dao-measure-layout-draw-repeat-custom-views-and-viewgroups-android/
http://blog.csdn.net/yanbober/article/details/46128379