View绘制体系(一)——从setContentView聊起
前言
对于Android开发者来说,View的绘制是非常基础且重要的部分,而Activity绘制View的流程,我们都是从setContentView开始去设置我们自定义的布局的,所以我准备从setContentView为起点来聊下View的绘制流程。
setContentView
在Activity中,我们经常会调用到setContentView这个方法来设置对应的布局文件,在这里我想从源码的角度去分析下setContentView内部是如何实现的。需要注意的是,setContentView并没有绘制View,只是创建了View。
我们先来看下Activity
类中的setContentView
方法:
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
public void setContentView(View view) {
getWindow().setContentView(view);
initWindowDecorActionBar();
}
public void setContentView(View view, ViewGroup.LayoutParams params) {
getWindow().setContentView(view, params);
initWindowDecorActionBar();
}
可以看到setContentView的三个重载其内部都是调用的getWindow().setContentView
对应参数的三个重载方法,然后调用initWindowDecorActionBar()
来初始化标题栏,那么我们就看看getWindow()
方法。
public Window getWindow() {
return mWindow;
}
根据上面代码,可以看到mWindow
是Activity
中的一个变量,保存与Activity
对应的Window
对象,Window
是个抽象类,所以我们要找到该对象初始化的地方,在Activity
中的attach
方法里面:
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback) {
attachBaseContext(context);
//...
mWindow = new PhoneWindow(this, window, activityConfigCallback);
//...
}
由此可以看到PhoneWindow
是Window
的实现子类,而Activity
的setContentView
实质是调用了PhoneWindow
的setContentView
方法,那么就来看下这个类,我们先来看下PhoneWindow.setContentView(int layoutResID)
这个方法:
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
//FEATURE_CONTENT_TRANSITIONS表示开启了动画Transition效果
//移除mContentParent中所有的子View
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
//开启Transiton后做相应的处理,不做具体分析
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
//一般情况来到这里,加载布局
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
需要注意的变量是mContentParent
这个,我们来看下这个变量是什么:
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
// 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;
从注释可以看出mContentParent
是放置Window
内容的一个容器,它是mDecor
自身或者mDecor
的子View
,而mDecor
是Window
对象的顶层View
,放置Window
的所有装饰元素,DecorView
继承FrameLayout
,是一个容器。
继续回到setContentView
,首先先判断mContentParent
是否为空,如果为空的话就调用installDecor()
方法去执行初始化操作,否则判断是否开启了Transition
效果,如果开启了就移除mContentParent
的所有子View
。我们先来分析下installDecor()
方法的具体实现:
private void installDecor() {
//省略了一些与无关分析代码
if (mDecor == null) {
mDecor = generateDecor(-1);
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
R.id.decor_content_parent);
if (decorContentParent != null) {
mDecorContentParent = decorContentParent;
if (mDecorContentParent.getTitle() == null) {
mDecorContentParent.setWindowTitle(mTitle);
} else {
mTitleView = findViewById(R.id.title);
if (mTitleView != null) {
if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {
final View titleContainer = findViewById(R.id.title_container);
if (titleContainer != null) {
titleContainer.setVisibility(View.GONE);
} else {
mTitleView.setVisibility(View.GONE);
}
mContentParent.setForeground(null);
} else {
mTitleView.setText(mTitle);
}
}
}
// Only inflate or create a new TransitionManager if the caller hasn't
// already set a custom one.
if (hasFeature(FEATURE_ACTIVITY_TRANSITIONS)) {
//省略跟Transition有关的代码
}
}
}
在installDecor
方法中我们需要关注的是mDecor
和mContentParent
的初始化,先来看看generateDecor(-1)
的具体实现:
protected DecorView generateDecor(int featureId) {
//省略context的设置
return new DecorView(context, featureId, this, getAttributes());
}
可见在installDecor()
方法中调用DecorView
的构造方法初始化了一个DecorView
,再来看下mContentParent
的初始化方法generateLayout(mDecor)
:
protected ViewGroup generateLayout(DecorView decor) {
// 获取manifest文件中Activity的theme设置
TypedArray a = getWindowStyle();
//通过获取到的theme配置设置对应的flag
mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR)
& (~getForcedWindowFlags());
if (mIsFloating) {
setLayout(WRAP_CONTENT, WRAP_CONTENT);
setFlags(0, flagsToUpdate);
} else {
setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);
}
//...
if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
requestFeature(FEATURE_NO_TITLE);
} else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
// Don't allow an action bar if there is no title.
requestFeature(FEATURE_ACTION_BAR);
}
//...
// 获取代码requestWindowFeature()中指定的Features, 并设置对应的布局文件
int layoutResource;
int features = getLocalFeatures();
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
layoutResource = R.layout.screen_swipe_dismiss;
} else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
//...
} else {
// 无任何修饰时的布局文件
layoutResource = R.layout.screen_simple;
}
mDecor.startChanging();
//加载对应布局,并添加到mDecorView中
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
//加载对应contentParent布局
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
// Remaining setup -- of background and title -- that only applies
// to top-level windows.
if (getContainer() == null) {
final Drawable background;
if (mBackgroundResource != 0) {
background = getContext().getDrawable(mBackgroundResource);
} else {
background = mBackgroundDrawable;
}
mDecor.setWindowBackground(background);
final Drawable frame;
if (mFrameResource != 0) {
frame = getContext().getDrawable(mFrameResource);
} else {
frame = null;
}
mDecor.setWindowFrame(frame);
mDecor.setElevation(mElevation);
mDecor.setClipToOutline(mClipToOutline);
if (mTitle != null) {
setTitle(mTitle);
}
if (mTitleColor == 0) {
mTitleColor = mTextColor;
}
setTitleColor(mTitleColor);
}
mDecor.finishChanging();
return contentParent;
}
上面代码省略了一些重复性设置的部分,可以分成以下三步来分析:
getWindowStyle()
:获取在manifest
文件中设置的Activity
的theme
属性,并通过setFlags
或者requestFeature
进行相对应的设置getLocalFeatures()
:获取代码中所有通过requestFeature
设置的属性,并通过Feature
的不同给layoutResource
设置不同的布局文件- 加载
mDecor
和contentParent
:mDecor.onResourcesLoaded
方法内部调用了addView
方法向mDecor
中添加了layoutResource
对应的布局。然后在mDecorView
通过ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT)
找到对应的cotentParent
视图,并对其进行相关的background
,title
等设置后,返回contentParent
从上述分析,我们可以得到installDecor
的作用是给mDecor
设置布局文件,并获取到其中id
为ID_ANDROID_CONTENT
,即content
的View
,赋值给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" />
<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>
可以看到有id为content的FrameLayout
容器,即mContentParent
,上面的ViewStub
从id可以看出是ActionMode
菜单视图。这是最简单的布局screen_simple
,其它的一些布局与其类似,只是多了一些其他的控件。
通过上面的分析,我们可以知道在installDecor
方法中调用generateDecor
对DecorView
进行了初始化,然后调用generateLayout
方法,获取了manifest文件中的android:theme
属性和代码中requestFeature
的设置(所以requestFeature需要在setContentView之前调用),选择对应的布局文件,加载mDecor
和mContentParent
两个视图。
再回到setContentView
中来:
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
//FEATURE_CONTENT_TRANSITIONS表示开启了动画Transition效果
//移除mContentParent中所有的子View
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
//开启Transiton后做相应的处理,不做具体分析
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
//一般情况来到这里,加载布局
mLayoutInflater.inflate(layoutResID, mContentParent);
}
//通知调用onApplyWindowInsets分发insets,该方法与状态栏相关
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
//设置用户显示设置content view的布局
mContentParentExplicitlySet = true;
}
在installDecor
初始化后,调用mLayoutInflater.inflate(layoutResID, mContentParent);
将我们写的Activity的布局加载到mContentParent
容器中。
继续往下看,getCallBack()
返回的是Window
对应的Activity
,因为在Activity的attach
方法中,调用了mWindow.setCallback(this)
。后续就是判断Activity不为空且未被销毁时,调用其onContentChanged()
方法通知Activity
内容发生改变(在Activity类中该方法是个空实现)。
对于setContentView
的另外两个重载方法,如下:
public void setContentView(View view) {
setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
public void setContentView(View view, ViewGroup.LayoutParams params) {
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
view.setLayoutParams(params);
final Scene newScene = new Scene(mContentParent, view);
transitionTo(newScene);
} else {
mContentParent.addView(view, params);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
这两个重载跟setContentView(int)
的流程几乎是一模一样的,只是由于传入的参数中有View
对象,所以不需要加载,而是直接调用addView
添加到mContentParent
中。
总结
setContentView的总流程图如下(省略了无关的部分):
ps:关于setContentView的绘制流程,就分析到这里了,后续将会介绍inflate方法是如何将我们自定义的布局加载到mContentParent中的,敬请关注!