好記性不如爛筆頭。生活中多做筆記,不僅可以方便自己,還可以方便他人。
(下面的源碼大部分是來(lái)自API 28)
緊接著上一篇文章重溫View繪制原理(一),繼續(xù)看view繪制原理。
1. View繪制流程
view的繪制是從根視圖 ViewRoot 的 performTraversals() 方法開(kāi)始,從上到下遍歷整個(gè)視圖樹(shù),每個(gè) View 控制負(fù)責(zé)繪制自己,而 ViewGroup 還需要負(fù)責(zé)通知自己的子 View 進(jìn)行繪制操作。視圖操作的過(guò)程可以分為三個(gè)步驟,分別是測(cè)量(Measure)、布局(Layout)和繪制(Draw)。performTraversals 方法在viewRoot的實(shí)現(xiàn)類 ViewRootImpl 里面:
- measure方法用于測(cè)量View的寬高
- layout方法用于確定View在父容器中的位置
- draw方法負(fù)責(zé)將View繪制在屏幕上
view繪制流程圖:


看看performTraversals方法源碼:
private void performTraversals() {
...
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
...
// 測(cè)量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
// 布局
performLayout(lp, mWidth, mHeight);
...
// 繪制
performDraw();
...
}
依次調(diào)用performMeasure、performLayout、performDraw方法,分別完成頂級(jí)View的measure、layout、draw流程。performMeasure會(huì)調(diào)用measure方法,而measure又會(huì)調(diào)用onMeasure方法,在onMeasure方法中又會(huì)對(duì)子元素進(jìn)行measure,這樣重復(fù)下去就完成了整個(gè)View樹(shù)的遍歷。
performLayout、performDraw傳遞過(guò)程也非常類似,不過(guò)performDraw是在draw方法中通過(guò)dispatchDraw方法實(shí)現(xiàn)的。
measure過(guò)程決定了View的寬高,而Layout方法則確定了四個(gè)頂點(diǎn)的坐標(biāo)和實(shí)際的寬高(往往等于measure中計(jì)算的寬高),draw方法則決定了View的顯示。只有完成了draw方法才能正確顯示在屏幕上。
2. MeasureSpec
MeasureSpec是measure的重要參數(shù)。
MeasureSpec 表示的是一個(gè) 32 位的整數(shù)值,它的高 2 位表示測(cè)量模式 SpecMode,低 30 位表示某種測(cè)量模式下的規(guī)格大小 SpecSize(PS:這里用到了位運(yùn)算進(jìn)行狀態(tài)壓縮來(lái)節(jié)省內(nèi)存)。MeasureSpec 是 View 類的一個(gè)靜態(tài)內(nèi)部類,用來(lái)說(shuō)明應(yīng)該如何測(cè)量這個(gè)View,有三種模式:
UNSPECIFIED:不指定測(cè)量模式,父視圖沒(méi)有限制子視圖的大小,子視圖可以是想要的任何尺寸,通常用于系統(tǒng)內(nèi)部,應(yīng)用開(kāi)發(fā)中很少使用到。
EXACTLY:精確測(cè)量模式,當(dāng)該視圖的 layout_width 或者 layout_height 指定為具體數(shù)值或者 match_parent 時(shí)生效,表示父視圖已經(jīng)決定了子視圖的精確大小,這種模式下 View 的測(cè)量值就是 SpecSize 的值。
AT_MOST:最大值模式,當(dāng)前視圖的 layout_width 或者 layout_height 指定為 wrap_content 時(shí)生效,此時(shí)子視圖的尺寸可以是不超過(guò)父視圖運(yùn)行的最大尺寸的任何尺寸。
下表是普通View的MeasureSpec的創(chuàng)建規(guī)則對(duì)應(yīng)表:
| childLayoutParams/parentSpecParams | EXACTLY | AT_MOST | UNSPECIFIED |
|---|---|---|---|
| dp/px | 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 |
3. Measure
View的繪制從測(cè)量開(kāi)始,看看performMeasure()方法:
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
具體操作是分發(fā)給 ViewGroup 的,由 ViewGroup 在它的 measureChild 方法中傳遞給子 View。ViewGroup 通過(guò)遍歷自身所有的子 View,并逐個(gè)調(diào)用子 View 的 measure 方法實(shí)現(xiàn)測(cè)量操作。
3.1 View的measure過(guò)程
View的measure過(guò)程由measure方法來(lái)完成,measure方法是一個(gè)final方法,不能重寫(xiě),它會(huì)調(diào)用VIew的onMeasure方法。onMeasure方法中會(huì)調(diào)用getDefaultSize方法,而getDefault方法中又會(huì)調(diào)用getSuggestedWidth和getSuggestedHeight方法。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
/**
* 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;
}
getDefaultSize方法所返回的就是測(cè)量后的View的大小。
接著看getSuggestedWidth和getSuggestedHeight方法:
/**
* 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());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
它在沒(méi)有指定background的情況下,返回的是minSize這一屬性對(duì)應(yīng)的值,而在指定了背景的情況下,返回的是背景drawable的getMinimumWidth / getMinimumHeight方法對(duì)應(yīng)的值
這兩個(gè)方法在Drawable有原始寬度的情況下返回原始寬度,否則返回0
從getDefaultSize方法可以看出,View的寬高由specSize決定。
3.2 ViewGroup的measure過(guò)程
ViewGroup除了完成自己的measure過(guò)程,還會(huì)遍歷調(diào)用子元素的measure方法,然后子元素再次遞歸執(zhí)行,ViewGroup是一個(gè)抽象類,因此沒(méi)有重寫(xiě)View的onMeasure方法。但它提供了一個(gè)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執(zhí)行measure時(shí),會(huì)遍歷子元素,調(diào)用measureChild方法對(duì)子元素進(jìn)行measure。
在measureChild方法中,取出子元素的LayoutParams,通過(guò)getChildMeasureSpec方法創(chuàng)建子元素MeasureSpec,然后傳遞給View的measure方法進(jìn)行測(cè)量。
ViewGroup沒(méi)有定義測(cè)量具體過(guò)程,因?yàn)樗莻€(gè)抽象類。具體的測(cè)量過(guò)程的onMeasure方法需要子類來(lái)實(shí)現(xiàn),由于它的子類的特性可能會(huì)很大不同,所以沒(méi)法做統(tǒng)一處理(如LinearLayout和RelativeLayout)。
4. Layout
Layout流程的作用是ViewGroup確定子元素的位置。當(dāng)ViewGroup被確定后,在onLayout中會(huì)遍歷所有子元素并調(diào)用layout方法,在layout方法中會(huì)調(diào)用onLayout方法。layout方法確定View的位置,而onLayout方法則確定所有子元素的位置。
ViewRootImpl 的 performLayout 如下:
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
mLayoutRequested = false;
mScrollMayChange = true;
mInLayout = true;
final View host = mView;
if (host == null) {
return;
}
if (DEBUG_ORIENTATION || DEBUG_LAYOUT) {
Log.v(mTag, "Laying out " + host + " to (" +
host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")");
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
try {
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
...
}
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
mInLayout = false;
}
先看View的layout方法:
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);
}
}
}
final boolean wasLayoutValid = isLayoutValid();
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
if (!wasLayoutValid && isFocused()) {
mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
if (canTakeFocus()) {
// We have a robust focus, so parents should no longer be wanting focus.
clearParentsWantFocus();
} else if (getViewRootImpl() == null || !getViewRootImpl().isInLayout()) {
// This is a weird case. Most-likely the user, rather than ViewRootImpl, called
// layout. In this case, there's no guarantee that parent layouts will be evaluated
// and thus the safest action is to clear focus here.
clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
clearParentsWantFocus();
} else if (!hasParentWantsFocus()) {
// original requestFocus was likely on this view directly, so just clear focus
clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
}
// otherwise, we let parents handle re-assigning focus during their layout passes.
} else if ((mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) {
mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
View focused = findFocus();
if (focused != null) {
// Try to restore focus as close as possible to our starting focus.
if (!restoreDefaultFocus() && !hasParentWantsFocus()) {
// Give up and clear focus once we've reached the top-most parent which wants
// focus.
focused.clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
}
}
}
if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
}
首先,通過(guò)setFrame方法設(shè)定View四個(gè)頂點(diǎn)的位置(初始化mLeft,mRight,mTop,mBottom)。四個(gè)頂點(diǎn)一旦確定,則在父容器中的位置也確定了,接著便會(huì)調(diào)用onLayout方法,來(lái)讓父容器確定子容器的位置。onLayout同樣和具體布局有關(guān),因此View和ViewGroup均沒(méi)有實(shí)現(xiàn)onLayout方法。
5. Draw
draw流程是將View繪制到屏幕上。先看看performDraw 方法:
private void performDraw() {
....
try {
boolean canUseAsync = draw(fullRedrawNeeded);
....
} finally {
mIsDrawing = false;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
...
}
private boolean draw(boolean fullRedrawNeeded) {
...
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
scalingRequired, dirty, surfaceInsets)) {
return false;
}
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
...
mView.draw(canvas);
...
}
最終調(diào)用到每個(gè) View 的 draw 方法繪制每個(gè)具體的 View,繪制基本上可以分為六個(gè)步驟:
public void draw(Canvas canvas) {
...
// Step 1, draw the background, if needed
if (!dirtyOpaque) {
drawBackground(canvas);
}
...
// Step 2, save the canvas' layers
saveCount = canvas.getSaveCount();
...
// 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
canvas.drawRect(left, top, right, top + length, p);
...
canvas.restoreToCount(saveCount);
...
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
}
View繪制過(guò)程的傳遞是通過(guò)dispatchDraw實(shí)現(xiàn)的。dispatchDraw會(huì)遍歷調(diào)用所有子元素的draw方法。這樣draw事件就一層層傳遞了下來(lái)。
它有個(gè)比較特殊的setWillNotDraw方法。如果一個(gè)View不需要繪制任何內(nèi)容,在我們?cè)O(shè)定這個(gè)標(biāo)記為true后,系統(tǒng)就會(huì)對(duì)其進(jìn)行相應(yīng)優(yōu)化。一般View沒(méi)有啟用這個(gè)標(biāo)記位。但ViewGroup是默認(rèn)啟用的。
它對(duì)實(shí)際開(kāi)發(fā)的意義在于:我們的自定義控件繼承于ViewGroup并且不具備繪制功能時(shí),可以開(kāi)啟這個(gè)標(biāo)記位方便系統(tǒng)進(jìn)行后續(xù)優(yōu)化。
6. 結(jié)束語(yǔ)
關(guān)于view繪制的原理在網(wǎng)上也特別多,時(shí)間久了也容易忘記,看一遍別人的,還不如順便把他們的記錄下來(lái),方便自己以后溫習(xí)。
重要參考: