setContentView()
一般我们使用 setContentView() 来给 Activity 设置布局,初学者可能认为执行 setContentView() 之后就马上开始执行绘制流程了,其实并没有,我们来看看 setContentView() 做了什么,本文源码基于 Android 11.0。
public class Activity{
private Window mWindow;
final void attach(...) {
...
mWindow = new PhoneWindow(this, window, activityConfigCallback);
...
}
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
...
}
public Window getWindow() {
return mWindow;
}
}
setContentView() 调用了 PhoneWindow 的 setContentView() 方法:
public class PhoneWindow extends Window{
// This is the view in which the window contents are placed. It is either
// mDecor itself, or a child of mDecor where the contents go.
ViewGroup mContentParent;
@Override
public void setContentView(int layoutResID) {
...
// 第一次 mContentParent 为 null
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
// hasFeature() 方法默认返回 false
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
// 把 layoutResID 对应的布局添加到 mContentParent 中
mLayoutInflater.inflate(layoutResID, mContentParent);
}
...
}
}
第一次进入 setContentView() 方法,mContentParent 为 null,此时调用 installDecor() 方法:
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
private void installDecor() {
// mDecor 即 DecorView
if (mDecor == null) {
// 创建 mDecor
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
// setWindow() 为当前 PhoneWindow
mDecor.setWindow(this);
}
if (mContentParent == null) {
// mContentParent 来自 generateLayout()
mContentParent = generateLayout(mDecor);
}
...
}
在 installDecor() 方法中,判断如果 mDecor 为 null,则创建 mDecor;如果 mDecor 不为 null,直接与当前 PhoneWindow 关联上。mDecor 是什么,mDecor 是 DecorView,它是这个 PhoneWindow 对应的布局的根布局。接下来调用 generateLayout() 方法:
public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
protected ViewGroup generateLayout(DecorView decor) {
...
int layoutResource;
int features = getLocalFeatures();
if ((features & (1 << FEATURE_NO_TITLE)) == 0)){
...
}else if ((features & (1 << FEATURE_ACTION_BAR)) != 0)){
...
}else{
// Embedded, so no decoration is needed.
layoutResource = R.layout.screen_simple;
}
mDecor.startChanging();
// 将 layoutResource 对应的布局添加到 DecorView 中
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
//mContentParent 对应的布局 id 为 com.android.internal.R.id.content;
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
...
return contentParent;
}
从名字也可以看出来,generateLayout() 方法是用来生成布局的,mContentParent 就是通过 generateLayout() 生成的,这里会根据 features 来确定 layoutResource,然后调用 mDecor.onResourcesLoaded() 把 layoutResource 添加到 mDecor 中,mContentParent 对应的就是 layoutResource 中 id 为 com.android.internal.R.id.content 的部分。最后回到前面,把 layoutResID 对应的布局添加到 mContentParent 中。
其中 R.layout.screen_simple 的布局如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
<!-- 这个就是上面的 mContentParent -->
<FrameLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundInsidePadding="false"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
可以看到,setContentView(int layoutResID) 只是把 layoutResID 对应的布局添加到 DecorView 中 id 为 content 的 FrameLayout 里面,还没有开始执行绘制流程。
View 的绘制流程
那么执行绘制流程是从什么时候开始的呢?真正执行绘制流程在 Activity 的 onResume() 生命周期方法执行之后,我们来看看 ActivityThread 中的代码:
public final class ActivityThread{
@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason) {
...
// performResumeActivity() 里面调用了 Activity 的 onResume() 方法
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
...
final Activity a = r.activity;
...
// decor 即 DecorView, 每个 PhoneWindow 对应一个 DecorView
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
...
wm.addView(decor, l);
...
}
}
performResumeActivity() 里面调用了 Activity 的 onResume() 方法, 在 performResumeActivity() 之后执行了 wm.addView(decor, l),wm 来源于 Activity 的 getWindowManager() 方法:
public class Activity{
private WindowManager mWindowManager;
final void attach(...){
...
mWindowManager = mWindow.getWindowManager();
...
}
/** Retrieve the window manager for showing custom windows. */
public WindowManager getWindowManager() {
return mWindowManager;
}
}
最终追溯到 Window 的 getWindowManager() 方法:
public abstract class Window{
private WindowManager mWindowManager;
// 设置该 Window 的 WindowManager
public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
boolean hardwareAccelerated) {
...
if (wm == null) {
wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
}
mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this); // 1
}
public WindowManager getWindowManager() {
return mWindowManager;
}
}
从代码第 12 行可以看到,mWindowManager 来自 WindowManagerImpl 的 createLocalWindowManager() 方法,createLocalWindowManager() 方法返回的是 WindowManagerImpl 对象,所以实际调用的是 WindowManagerImpl 的 addView() 方法:
public final class WindowManagerImpl implements WindowManager {
// mGlobal 是 WindowManagerGlobal 的单例
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
...
mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
mContext.getUserId());
}
}
接着执行 WindowManagerGlobal 的 addView() 方法:
public final class WindowManagerGlobal {
// 存储的是所有 Window 所对应的根 View
private final ArrayList<View> mViews = new ArrayList<View>();
// 存储的是所有 Window 所对应的 ViewRootImpl
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
// 存储的是所有 Window 所对应的布局参数
private final ArrayList<WindowManager.LayoutParams> mParams = new ArrayList<WindowManager.LayoutParams>();
// 存储的是那些正在被删除的 View 对象,或者说是那些已经
// 调用 remoView() 方法但是删除操作还未完成的 Window 对象
private final ArraySet<View> mDyingViews = new ArraySet<View>();
private static WindowManagerGlobal sDefaultWindowManager;
public static WindowManagerGlobal getInstance() {
synchronized (WindowManagerGlobal.class) {
if (sDefaultWindowManager == null) {
sDefaultWindowManager = new WindowManagerGlobal();
}
return sDefaultWindowManager;
}
}
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
// 检查参数的合法性
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (display == null) {
throw new IllegalArgumentException("display must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}
...
// 如果是子 Window 还需要调整布局参数
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
}
ViewRootImpl root;
...
// 创建 ViewRootImpl 实例
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, userId);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
addView() 方法里面执行了 ViewRootImpl 的 setView() 方法:
public final class ViewRootImpl{
final IWindowSession mWindowSession;
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
int userId) {
...
// 发起绘制流程
requestLayout();
...
// 通过 WindowSession 最终完成 Window 的添加
res = mWindowSession.addToDisplayAsUser(...)
...
}
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
// 检查线程
checkThread();
mLayoutRequested = true;
// 绘制的入口
scheduleTraversals();
}
}
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
void scheduleTraversals() {
// 添加了 mTraversalScheduled 标志位,所以连续两次 setText() 只会执行一次绘制流程
if (!mTraversalScheduled) {
mTraversalScheduled = true;
// 添加同步屏障
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 提交给编舞者,会在下一帧绘制时调用 mTraversalRunnable ,运行其 run() 方法
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
...
}
}
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
// 移除同步屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
...
performTraversals();
...
}
}
private void performTraversals() {
......
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
...
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); // 测量
...
performLayout(lp, mWidth, mHeight); // 布局
...
performDraw(); // 绘制
...
}
}
requestLayout() 方法会发起绘制流程,并最终调用 performTraversals() 方法。在 performTraversals() 方法中,首先通过 getRootMeasureSpec() 方法获取最外层根视图的 MeasureSpec 赋值给 childWidthMeasureSpec 和 childHeightMeasureSpec。
MeasureSpec
什么是 MeasureSpec? MeasureSpec 是一个 32 位 int 值,高 2 位表示 SpecMode ,低 30 位表示 SpecSize,SpecMode 是指测量模式,而 SpecSize 是指在某种测量模式下的规格大小。确切来说,MeasureSpec 在很大程度上决定了一个 View 的尺寸规格,之所以说是很大程度上,因为这个过程还受父容器的影响,父容器影响 View 的 MeasureSpec 的创建过程,下面先来看看 MeasureSpec 内部的代码:
public class View{
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0X3 << MODE_SHIFT;
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;
// 根据传入的 size 和 mode 来确定 MeasureSpec 的值
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
// MODE_MASK为:11000000 00000000 00000000 00000000
// ~MODE_MASK为:00111111 11111111 11111111 11111111
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
}
MeasureSpec 通过将 SpecMode 和 SpecSize 打包成一个 int 值来避免过多的对象内存分配,为了方便操作,其提供了打包和解包方法。SpecMode 和 SpecSize 也是一个 int 值,一组 SpecMode 和 SpecSize 可以打包为一个 MeasureSpec,而一个 MeasureSpec 可以通过解包的形式来得出其原始的 SpecMode 和 SpecSize,需要注意的是这里提到的 MeasureSpec 是指 MeasureSpec 所代表的 int 值,而并非 MeasureSpec 本身。
SpecMode有三类:
- UNSPECIFIED,父容器不对 View 有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。
- EXACTLY,父容器已经检测出 View 所需要的精确大小,这个时候 View 的最终大小就是 SpecSize 所指定的值。它对应于 LayoutParams 中的 match_parent 和具体的数值这两种模式。
- AT_MOST,父容器指定了一个可用大小即 SpecSize,View 的大小不能大于这个值,具体是什么值要看不同 View 的具体实现。它对应于 LayoutParams 中的 wrap_content。
前面通过 getRootMeasureSpec() 方法来获取 DecorView 的 MeasureSpec,代码如下:
public final class ViewRootImpl{
// DecorView 的 MeasureSpec
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;
}
}
通过上述代码,DecorView 的 MeasureSpec 的产生过程就很明确了,具体来说其遵守如下规则,根据它的 LayoutParams 中的宽/高的参数来划分:
- LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小;
- LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小:
- 固定大小(比如100dp):精确模式,大小为 LayoutParams 中指定的大小。
由此可知,DecorView 的 MeasureSpec 由窗口尺寸(windowSize)和自身的 LayoutParams 共同决定。
measure 过程
接下来把 DecorView 的 MeasureSpec 传给了 performMeasure() 方法,代码如下:
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
...
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
接下来执行根 View 的 measure() 方法:
public class View{
// final 方法,子类不可重写
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
}
measure() 方法里面调用了 onMeasure() 方法,由于 DecorView 是 FrameLayout 的子类,接下来执行 FrameLayout 的 onMeasure() 方法(其他比如 LinearLayout、RelativeLayout 等 ViewGroup 的子类都会根据自己的特性重写 onMeasure() 方法):
public class FrameLayout extends ViewGroup {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
int maxHeight = 0;
int maxWidth = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
// 先测量子 View 的宽高
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
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());
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
...
// 测量自己的宽高
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT));
}
}
可以看到,这里会遍历子视图并执行 measureChildWithMargins() 方法,在这个方法内部测量子视图的宽高,最后调用 setMeasuredDimension() 方法测量自己的宽高。measureChildWithMargins() 在 ViewGroup 中:
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
// 获取子视图的 LayoutParams
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// 通过父视图的测量规格以及子视图本身的 LayoutParams 来共同决定子视图的测量规格
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);
// 执行子 View 的 measure() 方法
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
在这里会获取子视图的 MeasureSepc 并调用子视图的 measure() 方法。可以看到,子视图的 MeasureSpec 由父容器的 MeasureSpec 和自己的 LayoutParams 共同决定。
其中 childWidthMeasureSpec 是通过 getChildMeasureSpec() 方法获取的,代码如下:
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
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);
}
}
上述方法不难理解,它的主要作用是根据父容器的 MeasureSpec 同时结合 View 本身的 LayoutParams 来确定子元素的 MeasureSpec,参数 padding 是指父容器中已占用的空间大小,因此子元素可用的大小为父容器的尺寸减去 padding,具体代码如下所示:
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
getChildMeasureSpec() 清楚展示了普通 View 的 MeasureSpec 的创建规则,为了更清晰地理解 getChildMeasureSpec() 的逻辑,这里提供一个表,表中对 getChildMeasureSpec() 的工作原理进行了梳理。注意,表中的 parentSize 是指父容器中目前可使用的大小。
针对不同的父容器和 View 本身不同的 LayoutParams,View 就可以有多种 MeasureSpec。这里简单说一下,当 View 采用固定宽/高的时候,不管父容器的 MeasureSpec 是什么,View 的 MeasureSpec 都是精确模式并且其大小遵循 Layoutparams 中的大小。当 View 的宽/高是 match_parent 时,如果父容器的模式是精准模式,那么 View 也是精准模式并且其大小是父容器的剩余空间;如果父容器是最大模式,那么 View 也是最大模式并且其大小不会超过父容器的剩余空间。当 View 的宽/高是 wrap_content 时,不管父容器的模式是精准还是最大化,View 的模式总是最大化并且大小不能超过父容器的剩余空间。至于 UNSPECIFIED 模式,该模式主要用于系统内部多次 Measure 的情形,一般来说,我们不需要关注此模式。
接下来继续执行子 View 的 measure() 方法,如果子 View 为普通的 View ,则执行 View 的 onMeasure() 方法,代码如下:
public class View{
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
...
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
}
setMeasuredDimension() 方法最终调用了 setMeasuredDimensionRaw() 对 mMeasuredWidth 和 mMeasuredHeight 赋值。
上面的代码中我们看到 View 的 mMeasuredWidth 和 mMeasuredHeight 是通过 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;
}
在 getDefaultSize() 方法中,SpecMode 为 MeasureSpec.AT_MOST 和 MeasureSpec.EXACTLY 时都返回了 specSize,这个 specSize 就是 View 测量后的大小。这里多次提到测量后的大小,是因为 View 最终的大小是在 layout 阶段确定的,所以这里必须要加以区分,但是几乎所有的情况下,View 的测量大小和最终大小是相等的。
如果你通过直接继承 View 来实现自定义 View ,并在 xml 中配置宽高为 wrap_content 时,实际效果却是该自定义 View 把剩下的空间都占满了,跟配置宽高为 match_parent 没什么区别。为什么呢?因为如果在 xml 中使用 wrap_content,则通过 getChildMeasureSpec() 拿到的该 View 的 specMode 就是 AT_MOST 模式,specSize 为父容器中目前可使用的大小 parentSize,再结合 getDefaultSize() 方法拿到 View 的宽/高就等于父容器中目前可使用的大小。
如何解决这个问题呢,也很简单,代码如下:
public class MyView extends View {
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 你可以自己给 mWidth 和 mHeight 赋值为实际包裹内容的宽高
int mWidth = 100;
int mHeight = 100;
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, mHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, mHeight);
}
}
}
我们只需要给 View 指定一个默认的内部宽/高(mWidth 和 mHeight),并在 wrap_content 时设置此宽/高即可。对于非 wrap_content 情形,我们沿用系统的测量值即可,至于这个默认的内部宽/高的大小如何指定,这个没有固定的依据,根据需要灵活指定即可。如果查看 TextView、ImageView 等的源码就可以知道,针对 wrap_content 情形,它们的 onMeasure() 方法均做了特殊处理。
至于前面 getDefaultSize() 方法中 UNSPECIFIED 这种情况,一般用于系统内部的测量过程,在这种情况下,View 的大小为 getDefaultSize() 的第一个参数 size,即宽/高分别为 getSuggestedMinimumWidth() 和 getSuggestedMinimumHeight() 这两个方法的返回值,看一下它们的源码:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
从 getSuggestedMinimumWidth() 的代码可以看出,如果 View 没有设置背景,那么 View 的宽度为 mMinWidth,而 mMinWidth 对应于 android:minWidth 这个属性所指定的值。这个属性如果不指定,那么 mMinWidth 则默认为 0;如果 View 指定了背景,则 View 的宽度为 max(mMinWidth,mBackground.getMinimumWidth())。那么 mBackground.getMinimumWidth() 是什么呢?我们看一下 Drawable 的 getMinimumWidth() 方法,如下所示:
public int getMinimumwidth(){
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
可以看出,getMinimumWidth() 返回的就是 Drawable 的原始宽度,前提是这个 Drawable 有原始宽度,否则就返回 0。那么 Drawable 在什么情况下有原始宽度呢?举例说明一下,ShapeDrawable 无原始宽/高,而 BitmapDrawable 有原始宽/高(图片的尺寸)。
View 的 measure 过程是三大流程中最复杂的一个,measure 完成以后,通过 getMeasuredWidth() 或 getMeasuredHeight() 方法就可以正确地获取到 View 的测量宽/高。需要注意的是,在某些极端情况下,系统可能需要多次 measure 才能确定最终的测量宽/高,在这种情形下,在 onMeasure() 方法中拿到的测量宽/高很可能是不准确的。一个比较好的习惯是在 onLayout() 方法中去获取 View 的测量宽/高或者最终宽/高。
总结一下,整个 View 视图是一个树结构的视图:
measure 过程就是从 DecorView 开始自顶向下遍历递归调用 View 的 measure() 方法( measure() 又调用 onMeasure() 方法)并最终对 mMeasuredWidth 和 mMeasuredHeight 赋值的过程。
layout 过程
Layout 的作用是 ViewGroup 用来确定子元素的位置的,当 ViewGroup 的位置被确定后,它在 onLayout() 中会遍历所有的子元素并调用其 layout 方法。
接下来分析 performLayout() 方法,如下:
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {
...
final View host = mView;
...
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
...
}
ViewGroup 的 layout() 方法为 final 类型,不可以重写:
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
@Override
public final void layout(int l, int t, int r, int b) {
...
super.layout(l, t, r, b);
...
}
}
接着调用 View 的 layout() 方法:
public class View{
public void layout(int l, int t, int r, int b) {
......
// 实质都是调用 setFrame() 方法把参数分别赋值给
// mLeft、mTop、mRight 和 mBottom 这几个变量,
// 判断 View 的位置是否发生过变化,以确定有没有必要对当前的 View 进行重新 layout
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
// 需要重新 layout
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
// 调用 onLayout() 方法
onLayout(changed, l, t, r, b);
...
}
...
}
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;
...
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
...
}
return changed;
}
}
layout() 方法中根据 isLayoutModeOptical(mParent) 来判断执行 setOpticalFrame() 方法还是 setFrame() 方法,点进 setOpticalFrame() 方法,发现最终还是调用的 setFrame() 方法来初始化 mLeft、mTop、mRight 和 mBottom 这四个值,View 的四个顶点值一旦确定,那么 View 在父容器中的位置也就确定了。
接着执行 onLayout() 方法, View 的 onLayout() 方法是一个空方法, ViewGroup 的 onLayout() 方法是一个抽象方法,意味着 ViewGroup 的子类都要重写这个方法。 DecorView 是 FrameLayout 的子类,接下来调用 FrameLayout 的 onLayout() 方法:
public class FrameLayout extends ViewGroup {
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
final int count = getChildCount();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
...
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
这里 onLayout() 方法又调用了 layoutChildren() 方法,在这里会遍历所有的子视图,并调用子视图的 layout() 方法来确定自己的位置,这样一层一层地传递下去,就完成了整个 View 树的 layout 过程。
那么 View 的测量宽/高和最终宽/高有什么区别呢?这个问题可以具体为:View 的 getMeasuredWidth() 与 getWidth() 有什么区别。我们来看一下 getWidth() 、 getHeight() 和 getMeasuredWidth() 、 getMeasuredHeight() 这两对方法在 View 源码中的具体实现:
public class View{
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
public final int getMeasuredHeight() {
return mMeasuredHeight & MEASURED_SIZE_MASK;
}
public final int getWidth() {
return mRight - mLeft;
}
public final int getHeight() {
return mBottom - mTop;
}
}
前面看到在 performLayout() 方法中,给 layout() 方法传递的参数是 host.getMeasuredWidth() 和 host.getMeasuredHeight(),再结合 mLeft、mRight、mTop、mBottom 这四个变量的赋值过程,可以发现:getWidth() 的返回值刚好就是 View 的测量宽度,而 getHeight() 的返回值也刚好就是 View 的测量高度。经过上述分析,现在我们可以回答这个问题了:在 View 的默认实现中,View 的测量宽/高和最终宽/高是相等的,只不过测量宽/高形成于 View 的 measure 过程,而最终宽/高形成于 View 的 layout 过程,即两者的赋值时机不同,测量宽/高的赋值时机稍微早一些。因此,在日常开发中,我们可以认为 View的测量宽/高就等于最终宽/高,但是的确存在某些特殊情况会导致两者不一致,下面举例说明。
如果重写 View 的 layout() 方法,代码如下:
public final void layout(int l, int t, int r, int b) {
super.layout(l, t, r + 100, b + 100);
}
上述代码会导致在任何情况下 View 的最终宽/高总是比测量宽/高大 100px,虽然这样做会导致 View 显示不正常并且也没有实际意义,但是这证明了测量宽/高的确可以不等于最终宽/高。另外一种情况是在某些情况下,View 需要多次 measure 才能确定自己的测量宽/高,在前几次的测量过程中,其得出的测量宽/高有可能和最终宽/高不一致,但最终来说,测量宽/高还是和最终宽/高相同。
draw 过程
接下来分析 performDraw() 方法:
public final class ViewRootImpl{
private void performDraw() {
...
boolean canUseAsync = draw(fullRedrawNeeded);
...
}
private boolean draw(boolean fullRedrawNeeded) {
...
if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
...
mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this); //硬件加速绘制
}else{
...
// 软件绘制
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);
...
}
}
接下来调用 View 的 draw() 方法:
public class View{
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)
* 7. If necessary, draw the default focus highlight
*/
// Step 1, draw the background, if needed
// 步骤 1. 绘制背景
int saveCount;
drawBackground(canvas);
// skip step 2 & 5 if possible (common case)
// 步骤 2. 如果需要的话,保存 canvas 的图层,为 fading 做准备
// 步骤 5. 如果需要的话,绘制 fading edges 和恢复图层
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
// 步骤 3. 绘制 View 的内容
onDraw(canvas);
// Step 4, draw the children
// 步骤 4. 绘制 children
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
// 步骤 6. 绘制装饰 (foreground, scrollbars)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
// 步骤 7. 绘制默认的焦点高亮显示
drawDefaultFocusHighlight(canvas);
if (isShowingLayoutBounds()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
...
}
}
draw 过程的作用是将 View 绘制到屏幕上,主要分为以下几步:
- 绘制背景 background.draw(canvas)。
- 绘制自己(onDraw)。
- 绘制 children(dispatchDraw)。
- 绘制装饰(onDrawForeground)。
View 的 onDraw() 方法是一个空方法,说明 View 默认不会绘制任何内容,真正的绘制都需要自己在子类中实现。View 的绘制过程的传递是通过 dispatchDraw() 来实现的,在 ViewGroup 中对 dispatchDraw() 方法进行了重写,代码如下:
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
protected void dispatchDraw(Canvas canvas) {
...
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
...
for (int i = 0; i < childrenCount; i++) {
while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
final View transientChild = mTransientViews.get(transientIndex);
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
// 调用 drawChild() 方法
more |= drawChild(canvas, transientChild, drawingTime);
}
}
}
...
// Draw any disappearing views that have animations
if (mDisappearingChildren != null) {
...
for (int i = disappearingCount; i >= 0; i--) {
final View child = disappearingChildren.get(i);
more |= drawChild(canvas, child, drawingTime);
}
}
...
}
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
}
dispatchDraw() 方法里面遍历每个子 View,调用子 View 的 draw() 方法,如此 draw 事件就一层一层地传递了下去。
View 中有一个特殊的方法 setWillNotDraw(),其源码如下所示:
/**
* If this view doesn't do any drawing on its own, set this flag to
* allow further optimizations. By default, this flag is not set on
* View, but could be set on some View subclasses such as ViewGroup.
*
* Typically, if you override {@link #onDraw(android.graphics.Canvas)}
* you should clear this flag.
*
* @param willNotDraw whether or not this View draw on its own
*/
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
从注释可以看出,如果一个 View 不需要绘制任何内容,那么设置这个标记位为 tue 以后,系统会进行相应的优化。默认情况下,View 没有启用这个优化标记位,但是 ViewGroup 会默认启用这个优化标记位。这个标记位对实际开发的意义是:当我们的自定义控件继承于 ViewGroup 并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化。当然,当明确知道一个 ViewGroup 需要通过 onDraw() 来绘制内容时,我们需要显式地关闭 WILL_NOT_DRAW 个标记位。