在 Android 的知識體系中,View
扮演者很重要的角色。View
是 Android 在視覺上的呈現。本文結合 android-28
的源碼來分析 View
的繪製過程。java
ViewRootImpl
類是鏈接 WindowManager
和 DecorView
的紐帶,View
的繪製流程均是經過 ViewRootImpl
來完成的。android
// ActivityThread.java
@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason) {
...
if (r.window == null && !a.mFinished && willBeVisible) {
// 獲取 WindowManager 及 DecorView
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (r.mPreserveWindow) {
a.mWindowAdded = true;
r.mPreserveWindow = false;
// Normally the ViewRoot sets up callbacks with the Activity
// in addView->ViewRootImpl#setView. If we are instead reusing
// the decor view we have to notify the view root that the
// callbacks may have changed.
ViewRootImpl impl = decor.getViewRootImpl();
if (impl != null) {
impl.notifyChildRebuilt();
}
}
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
// 將 DecorView 添加到當前 Window 中
wm.addView(decor, l);
} else {
// The activity will get a callback for this {@link LayoutParams} change
// earlier. However, at that time the decor will not be set (this is set
// in this method), so no action will be taken. This call ensures the
// callback occurs with the decor set.
a.onWindowAttributesChanged(l);
}
}
// If the window has already been added, but during resume
// we started another activity, then don't yet make the
// window visible.
} else if (!willBeVisible) {
if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
r.hideForNow = true;
}
...
}
複製代碼
上面的代碼說明,在 ActivityThread
中,當 Activity
對象被建立完畢後,會將 DecorView
經過 WindowManager
添加到 Window
中。app
// WindowManagerImpl.java
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
複製代碼
能夠知道最終是經過 WindowManagerGlobal
的 addView
方法來將 DecorView
添加到 Window
中ide
// WindowManagerGlobal
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
...
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
...
// 初始化 ViewRootImpl 並將 ViewRootImpl 對象和 DecorView 創建關聯
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
複製代碼
上述代碼建立了 ViewRootImpl
對象,並將 ViewRootImpl
對象和 DecorView
創建關聯。最終在 setView
方法中,會執行 ViewRootImpl
的 requestLayout
方法來執行 View
的繪製流程函數
// ViewRootImpl.java
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
複製代碼
scheduleTraversals
方法最終會調用 performTraversals
方法,通過 measure
、layout
和 draw
三個過程才能最終將一個 View
繪製出來佈局
View
的寬和高View
在父容器中的放置位置View
繪製在屏幕上如圖所示,
performTraversals
會依次調用performMeasure
、performLayout
和performDraw
三個方法,這三個方法分別完成頂級View
的measure
、layout
和draw
這三大流程。其中在performMeasure
中會調用measure
方法,在measure
方法中又會去調用onMeasure
方法,在onMeasure
方法中又會對全部的子元素進行measure
過程,這個時候measure
流程就從父容器傳遞到了子元素中了,這樣就完成了一次measure
過程。接着子元素會重複父容器的measure
過程,如此反覆就完成了整個View
樹的遍歷。經過performLayout
和performDraw
的傳遞流程跟performMeasure
相似ui
爲了更好地理解 View
的測量過程,咱們還須要理解 MeasureSpec
。MeasureSpec
參與了 View
的 measure
過程,在很大程度上決定了一個 View
的尺寸規格,但父容器也會影響 View
的 MeasureSpec
的建立過程。在測量過程當中,系統會將 View
的 LayoutParams
根據父容器所設置的規則轉換成對應的 MeasureSpec
,而後再根據這個 MeasureSpec
來測量出 View
的寬和高。this
// View.java
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/** @hide */
@IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
@Retention(RetentionPolicy.SOURCE)
public @interface MeasureSpecMode {}
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size, @MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int makeSafeMeasureSpec(int size, int mode) {
if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
return 0;
}
return makeMeasureSpec(size, mode);
}
@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
static int adjust(int measureSpec, int delta) {
final int mode = getMode(measureSpec);
int size = getSize(measureSpec);
if (mode == UNSPECIFIED) {
// No need to adjust size for UNSPECIFIED mode.
return makeMeasureSpec(size, UNSPECIFIED);
}
size += delta;
if (size < 0) {
Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
") spec: " + toString(measureSpec) + " delta: " + delta);
size = 0;
}
return makeMeasureSpec(size, mode);
}
public static String toString(int measureSpec) {
int mode = getMode(measureSpec);
int size = getSize(measureSpec);
StringBuilder sb = new StringBuilder("MeasureSpec: ");
if (mode == UNSPECIFIED)
sb.append("UNSPECIFIED ");
else if (mode == EXACTLY)
sb.append("EXACTLY ");
else if (mode == AT_MOST)
sb.append("AT_MOST ");
else
sb.append(mode).append(" ");
sb.append(size);
return sb.toString();
}
}
複製代碼
MeasureSpec
表明一個 32 位的 int
值,高 2 位表明 SpecMode
, 低 30 位表明 SpecSize
spa
MeasureSpec
經過將 SpecMode
和 SpecSize
打包成一個 int
值來避免過多的對象內存分配。makeMeasureSpec
是打包方法,getMode
和 getSize
則爲解包方法。code
SpecMode
有三類,每一類都標識特殊的含義
父容器不對 View
有任何限制,要多大給多大,這種狀況通常用於系統內部,標識一種測量的狀態
父容器已經檢測出 View
所須要的精確大小,這個時候 View
的最終大小就是 SpecSize
所指定的值。它對應於 LayoutParams
中的 match_parent
和具體的數值這兩種模式
父容器指定了一個可用大小即 SpecSize
,View
的大小不能大於這個值,具體是什麼值要看不一樣 View
的具體實現。它對應於 LayoutParams
中的 wrap_content
MeasureSpec
不是惟一由 LayoutParams
決定的,LayoutParams
須要和父容器一塊兒才能決定 View
的 MeasureSpec
,從而進一步決定 View
的寬和高。
對於 DecorView
,其 MeasureSpec
由窗口的尺寸和其自身的 LayoutParams
來共同肯定;對於普通的 View
,其 MeasureSpec
由父容器的 MeasureSpec
和自身的 LayoutParams
來共同局誒的那個,MeasureSpec
一旦肯定後, onMeasure
中就能夠肯定 View
的測量寬和高
對於 DecorView
來講,它的 MeasureSpec
建立過程是由 getRootMeasureSpec
方法來完成的
// ViewRootImpl.java
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: // 精確模式,大小爲 LayoutParams 中指定的大小
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
複製代碼
經過上述代碼,DecorView
的 MeasureSpec
的產生過程就很明確了,具體來講其遵照以下規則,根據它的 LayoutParams
中的寬和高參數來劃分
ViewRootImpl
在 performTraversals
方法中調用 getRootMeasureSpec
獲取到 childWidthMeasureSpec
和 childHeightMeasureSpec
後,會傳給 performMeasure
方法,最終調用 DecorView
的 measure
方法
對於普通 View
來講,即佈局中的 View
,View
的 measure
過程由 ViewGroup
傳遞而來,在 ViewGroup
的 measureChildWithMargins
方法
// ViewGroup.java
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
方法通常會在自定義 Layout
組件的 onMeasure
方法中調用(如 FrameLayout, LinearLayout),來測量子元素的規格。在調用子元素的 measure
方法以前會先經過 getChildMeasureSpec
方法來獲得子元素的 MeasureSpec
。經過上面代碼可知,子元素的 MeasureSpec
的建立和父容器的 MeasureSpec
和子元素自己的 LayoutParams
有關,此外還和 View
的 margin
及 padding
有關
// ViewGroup.java
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
// 父容器的 mode 和 size
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);
}
複製代碼
getChildMeasureSpec
函數主要的做用是根據父容器的 MeasureSpec
同時結合 View
自己的 LayoutParams
來肯定子元素的 MeasureSpec
。
當
View
採用固定寬和高的時候,無論父容器的MeasureSpec
是什麼,View
的MeasureSpec
都是精確模式而且其大小遵循LayoutParamas
中的大小。當View
的寬和高是match_parent
時,若是父容器的模式是精確模式,那麼View
也是精確模式而且其大小是父容器的剩餘空間;若是父容器是最大模式,那麼View
也是最大模式而且其大小不會超過父容器的剩餘空間。若是父容器是最大模式,那麼View
也是最大模式而且其大小不會超過父容器的剩餘空間。當View
的寬和高是wrap_content
時,無論父容器的模式是精準仍是最大化,View
的模式老是最大化而且大小不能超過父容器的剩餘空間。