【Android】View的繪製原理
一、View繪製總入口
ActivityThread中,首先建立Activity,然後通過attach方法初始化對應的mWindow,然後將頂級檢視DecorView新增到Windows中,並建立ViewRootImpl物件,這個物件就是溝通WindowManager和DecorView之間的橋樑,也是View繪製的開始。
View的繪製流程首先開始於ViewRootImpl的performTraversals()方法。依次經過三大過程,measure、layout、draw,performTraversals會依次呼叫performMeasure、performLayout、performDraw方法。其中measure用來對View進行測量,layout來確定子元素在父元素中的位置即真實寬高以及四個頂點位置,draw負責將View繪製出來。
說的接地氣一點,measure就是給個建議值,View多大合適;layout就是去放View的外框,放在哪裡,具體多大;draw就是去畫View裡面的內容。
measure過程得到的測量寬高可以通過getMeasureWidth和getMeasureHeight得到,其值不一定就是實際寬高,實際寬高是layout之後,可以通過getWidth和getHeight獲得。
二、measure
1、MeasureSpec
測量過程需要提到一個類,叫MeasureSpec,“測量規格”。MeasureSpec是View定義的一個內部類。MeasureSpec代表一個32位的int,高兩位代表SpecMode,測量模式,第30位代表SpecSize,在某種測量模式下的規格大小。
MeasureSpec提供打包和解包的方法,可以將一組SpecMode和SpecSize通過makeMeasureSpec方法打包成MeasureSpec,也可以將一個MeasureSpec通過getMode和getSize進行解包獲得對應的值。
SpecMode測量模式包含三種,含義如下:
SpecMode值 | 含義 |
---|---|
UNSPECIFIED | 父容器沒有對View有任何限制,要多大給多大 |
EXACTLY | 父容器能得到View的精確的值,這時候View的測量大小就是SpecSize的值,對應於View的LayoutParams中為match_parent或具體的值的情況。 |
AT_MOST | 父容器指定了一個可用大小SpecSize,View的大小不定,但是不能大於這個值,這個對應於View的LayoutParams的wrap_content。 |
系統需要通過MeasureSpec對View進行測量。View的MeasureSpec需要由父容器的MeasureSpec和View的View的layoutParams一起決定,然後根據View的MeasureSpec確定View的寬和高。
由於DecorView是頂級檢視,所以它的測量方法比較特殊,具體下面一一看下DecorView和普通View的MeasureSpec的計算。
(1)DecorView的MeasureSpec值計算,DecorView是最先被測量的,可以從ViewRootImpl的performMeasure方法看出。
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
這裡的mView就是DecorView。往前搜傳入的兩個MeasureSpec的值。
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
可以看見呼叫了getRootMeasureSpec方法,傳入了兩個值,第一個值是螢幕尺寸,第二個值lp是LayoutParams 長寬的引數。所以,DecorView的MeasureSpec的值是由螢幕尺寸和它的LayoutParams 決定的。接下來看具體的關係getRootMeasureSpec。
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;
}
可以看見如果是MATCH_PARENT的話,就是精確的模式,大小就是視窗的大小,如果是WRAP_CONTENT,就是AT_MOST模式,即大小不定,但是最大為視窗大小。
(2)普通View的MeasureSpec值計算。
前面說到DecorView首先被測量,呼叫了mView.measure(childWidthMeasureSpec, childHeightMeasureSpec),由於DecorView繼承自FrameLayout繼承自ViewGroup繼承自View,可以看見View裡面的measure方法是一個final方法,即不可被重寫。
public final void measure(int widthMeasureSpec, int heightMeasureSpec)
這個方法其實是去呼叫了onMeasure方法,這個方法是各個子類可以重寫的。
跟蹤這個方法,可以看見在View的measure是由ViewGroup傳遞過來的,具體是在ViewGroup裡面的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);
}
可以看見,首先獲取View的LayoutParams,然後呼叫getChildMeasureSpec方法獲取View的MeasureSpec,再呼叫View的measure進行測量,測量後面說,先說View的MeasureSpec的計算。看getChildMeasureSpec方法。
先看傳入的三個引數:
- 第一個引數parentWidthMeasureSpec即父容器的MeasureSpec
- 第二個引數mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed,這些都是啥呢?這些分別是父容器設定的padding和View設定的margin和父容器已經被佔用的大小。所以這些值加起來就是父容器中不能被View使用的長度。可以看見原始碼中函式引數名字就是padding,雖然不很確切,但是也可以說明就是不能被View使用的部分。
第三個引數是View的LayoutParams。確切地說就是XML中layout_width和layout_height。
看下具體的getChildMeasureSpec方法。
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
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 >= 0) {
// 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 >= 0) {
// 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 ? 0 : 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 ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
一句句看下來,首先,獲取父容器的specMode和specSize,然後計算size = Math.max(0, specSize - padding),這裡size就是說是父容器留給View的最大長度,如果specSize < padding,說明沒有空間給View了,所以是0,否則能給View的剩餘空間就是specSize - padding,這個比較好理解。
然後判斷父容器specMode。根據父容器的specMode和View的LayoutParams值(match_parent、具體值、wrap_content)來決定。具體的邏輯非常簡單,程式碼容易看懂。
總結成如下表:
View的LayoutParams\父容器的測量模式specMode | EXACTLY | AT_MOST | UNSPECIFIED |
---|---|---|---|
具體的值 | EXACTLY childsize | EXACTLY childsize | EXACTLY childsize |
match_parent | EXACTLY parentsize | AT_MOST parentsize | UNSPECIFIED 0 |
wrap_content | AT_MOST parentsize | AT_MOST parentsize | UNSPECIFIED 0 |
這裡的parentsize就是父容器可用剩餘空間。
2、View的measure過程
首先,View的measure過程是由measure方法完成,看下View的measure方法。可以看見是個final方法,即不可被重寫。
public final void measure(int widthMeasureSpec, int heightMeasureSpec)
measure方法會呼叫onMeasure方法,看onMeasure方法的實現。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
setMeasuredDimension即設定View的測量寬高的值。計算方法是呼叫getDefaultSize,傳入兩個引數,先看這兩個引數。
第二個引數是MeasureSpec,即前面說的,通過父MeasureSpec和View的LayoutParams以及父容器已經佔用的空間進行計算得到。
看下第一個引數getSuggestedMinimumWidth()。
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
思路比較簡單,如果沒有背景,則直接返回mMinWidth,mMinWidth即View的android:minWidth的設定值,預設為0;如果有背景,則返回max(mMinWidth, mBackground.getMinimumWidth(),mBackground.getMinimumWidth()為背景的尺寸,預設也是0。
現在看下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;
}
思路也是比較簡單的,如果測量模式是UNSPECIFIED,返回第一個引數,如果是AT_MOST和EXACTLY,直接返回View的MeasureSpec的規格大小。
從上面的分析可以發現,直接繼承View的時候,需要重寫onMeasure方法,並設定wrap_content時候的大小,否則會和match_parent的時候效果一直。比如當View設定為wrap_content的時候,此時View的MeasureSpec為AT_MOST,parentsize。當View設定為match_parent的時候,其測量值最後的結果也為parentsize。
3、ViewGroup的measure過程
ViewGroup除了完成自己的measure過程外,還會去遍歷所有的子元素的measure方法,各個子元素再去遞迴這個過程。ViewGroup是個抽象類,其onMeasure方法是個抽象方法,需要子類去實現它,因為不同的ViewGroup有不一樣的佈局特性,所以導致他們的測量細節不一樣。
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);
}
}
}
遍歷所有的View,如果可見的話呼叫measureChild進行測量。getChildMeasureSpec的實現前面已經講過了。得到之後呼叫子元素的measure方法。
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);
}
4、measure結果的獲取
有的業務需要,需要獲取測量結果,但是有個問題,measure的測量和Activity的各個生命週期沒有關係,所以在什麼生命週期裡面都有可能得到的measure測量值為0,即還沒測量結束。從原始碼可以看見performTraversals是另開一個執行緒執行的。
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
如果保證能獲取到呢?有幾個方法:
(1)為View新增onWindowFocusChanged監聽,這個監聽會在每次Activity的視窗獲得和失去焦點的時候被呼叫,而View繪製成功的時候也是一次獲得焦點的過程,所以也會呼叫。
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
if (hasWindowFocus) {
int height = view.getMeasureHeight();
int width = view.getMeasureWidth();
}
}
(2)view.post
通過post將一個runnable投遞到訊息佇列尾部,然後呼叫的時候,說明View已經初始化好了。
view.post(new Runnable() {
@Override
public void run() {
int height = view.getMeasureHeight();
int width = view.getMeasureWidth();
}
});
(3)view.measure(int widthMeasureSpec, int heightMeasureSpec)
這個是最直接的方法,直接觸發計算。這個一般是用在具體的值上,因為parentsize不知道。
比如寬高都是100px的。
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);
二、layout
layout的作用是ViewGroup用來確定子元素的位置。首先呼叫setFrame初始化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);
...
}
onLayout由於不同的子類實現細節都不一樣,所以onLayout在View和ViewGroup中都沒有具體實現,都在子類中實現。以LinearLayout為例。
@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關鍵程式碼如下。
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呼叫子元素的layout。
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
這裡的width和height就是測量寬高。
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
看下最後真正的寬高的計算方法如下:
public final int getWidth() {
return mRight - mLeft;
}
public final int getHeight() {
return mBottom - mTop;
}
顯然這個地方得到的值就是width和height。所以說在View的預設實現中,View的measure的結果測量寬高和layout的結果最終寬高是相等的。除非重新View,使得兩者不一致。比如下面這樣,但是這個沒有什麼意義。
public void layout(int l, int t, int r, int b) {
super.layout(l, t, r + 100, b + 100);
}
三、draw
draw的過程就是將View繪製在螢幕上面。從Android原始碼的註釋也可以看見繪製分為四個步驟,在View的draw方法中。
(1)繪製背景
drawBackground(canvas)
(2)繪製自己的內容
onDraw(canvas)
(3)繪製children
dispatchDraw(canvas);
(4)繪製裝飾
onDrawForeground(canvas);
其中View的繪製通過dispatchDraw來遍歷子元素並呼叫其draw方法,將draw事件一層層傳遞下去。