在 Android 的知識體系中,View
扮演者很重要的角色。View
是 Android 在視覺上的呈現。本文結合 android-28
的原始碼來分析 View
的繪製過程。
ViewRootImpl
ViewRootImpl
類是連線 WindowManager
和 DecorView
的紐帶,View
的繪製流程均是通過 ViewRootImpl
來完成的。
// 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
中。
// 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
中
// 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
繪製出來
- meausre: 用來測量
View
的寬和高 - layout: 用來確定
View
在父容器中的放置位置 - draw: 負責將
View
繪製在螢幕上
如圖所示,
performTraversals
會依次呼叫performMeasure
、performLayout
和performDraw
三個方法,這三個方法分別完成頂級View
的measure
、layout
和draw
這三大流程。其中在performMeasure
中會呼叫measure
方法,在measure
方法中又會去呼叫onMeasure
方法,在onMeasure
方法中又會對所有的子元素進行measure
過程,這個時候measure
流程就從父容器傳遞到了子元素中了,這樣就完成了一次measure
過程。接著子元素會重複父容器的measure
過程,如此反覆就完成了整個View
樹的遍歷。通過performLayout
和performDraw
的傳遞流程跟performMeasure
類似
MeasureSpec
為了更好地理解 View
的測量過程,我們還需要理解 MeasureSpec
。MeasureSpec
參與了 View
的 measure
過程,在很大程度上決定了一個 View
的尺寸規格,但父容器也會影響 View
的 MeasureSpec
的建立過程。在測量過程中,系統會將 View
的 LayoutParams
根據父容器所試駕的規則轉換成對應的 MeasureSpec
,然後再根據這個 MeasureSpec
來測量出 View
的寬和高。
// 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
- SpecMode: 測量模式
- SpecSize: 在某種測量模式下的規格大小
MeasureSpec
通過將 SpecMode
和 SpecSize
打包成一個 int
值來避免過多的物件記憶體分配。makeMeasureSpec
是打包方法,getMode
和 getSize
則為解包方法。
SpecMode
有三類,每一類都標識特殊的含義
UNSPECIFIED
父容器不對 View
有任何限制,要多大給多大,這種情況一般用於系統內部,標識一種測量的狀態
EXACTLY
父容器已經檢測出 View
所需要的精確大小,這個時候 View
的最終大小就是 SpecSize
所指定的值。它對應於 LayoutParams
中的 match_parent
和具體的數值這兩種模式
AT_MOST
父容器指定了一個可用大小即 SpecSize
,View
的大小不能大於這個值,具體是什麼值要看不同 View
的具體實現。它對應於 LayoutParams
中的 wrap_content
MeasupreSpec 和 LayoutParams
MeasureSpec
不是唯一由 LayoutParams
決定的,LayoutParams
需要和父容器一起才能決定 View
的 MeasureSpec
,從而進一步決定 View
的寬和高。
對於 DecorView
,其 MeasureSpec
由視窗的尺寸和其自身的 LayoutParams
來共同確定;對於普通的 View
,其 MeasureSpec
由父容器的 MeasureSpec
和自身的 LayoutParams
來共同局誒的那個,MeasureSpec
一旦確定後, onMeasure
中就可以確定 View
的測量寬和高
DecorView 建立 MeasureSpec
對於 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
中的寬和高引數來劃分
- LayoutParams.MATCH_PARENT: 精確模式,大小就是視窗大小
- LayoutParams.WRAP_CONTENT: 最大模式,大小不定,但是不能超過視窗的大小
- 固定大小(如 100dp): 精確模式,大小為 LayoutParams 中指定的大小
ViewRootImpl
在 performTraversals
方法中呼叫 getRootMeasureSpec
獲取到 childWidthMeasureSpec
和 childHeightMeasureSpec
後,會傳給 performMeasure
方法,最終呼叫 DecorView
的 measure
方法
普通 View 建立 MeasureSpec
對於普通 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
的模式總是最大化並且大小不能超過父容器的剩餘空間。