一、前言
在 Android 应用开发的世界里,setContentView
几乎是每个开发者都会接触到的方法。它的作用至关重要------负责将视图(View
)或布局(Layout
)展示在屏幕上。尽管这看起来是一个简单直接的操作,但其背后实际上隐藏着 Android
系统中复杂而精妙的窗口管理和视图渲染机制。最近,在与同行的交流中,我发现我几乎将这一块忘得一干二净。因此,我决定从 Activity
的 setContentView
方法入手,重新梳理并深入探讨 Android 的窗口管理和视图展示原理,希望能够为大家带来新的理解和启发。阿弥陀佛。
吨吨吨,喝一杯冰美式。要睡着了。
PS:因为代码中有大量的样式、动画、配置相关的代码,我会选择性的省略,如果你想看完整的,我使用的是"appcompact1.3.1"、SDK31。
如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏
二、AppCompatActivity
一般而言你都会如此使用setConentView
:
less
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(mContentLayoutId);
}
当然你也可以在这么使用:
kotlin
class TestActivity : AppCompatActivity(R.layout.activity_test)
我们看看AppCompatActivity
的setContentView
藏着什么:
less
@Override
public void setContentView(@LayoutRes int layoutResID) {
//**
getDelegate().setContentView(layoutResID);
}
可以看到在AppCompatActivity
中,使用的getDelegate
,这看起来像是委托啊!点下去看看:
kotlin
@NonNull
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, this);
}
return mDelegate;
}
我们找到了全新的AppCompatDelegate
!官方如是说:
此类表示一个委托,您可以使用该委托将 AppCompat 的支持扩展到任何 Activity.只能 Activity 与一个 AppCompatDelegate 实例链接,因此应保留从 create(Activity, AppCompatCallback) 返回的实例,直到 Activity 被销毁。
但是AppCompatDelegate
是一个抽象类,我们可以很轻松的找到它的实现类AppCompatDelegateImpl
。
我们可以在这里看到setContentView
scss
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}
可以看到,我们提供的LayoutId,最后会添加到contentParent这个View上,那么mSubDecor是从哪儿来的?看到调用方法的名字,ensure sub decor ,子装饰视图,想必在这里。我们继续往下看:
perl
private void ensureSubDecor() {
// 检查子装饰(sub decor)是否已经设置
if (!mSubDecorInstalled) {
// 创建子装饰视图
mSubDecor = createSubDecor();
//**
}
}
看样子藏在createSubDecor
中, gogogo:
scss
private ViewGroup createSubDecor() {
TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
//**省略 获取当前上下文的主题属性,设置对应的样式
a.recycle();
// 确保窗口已安装其装饰
ensureWindow();
mWindow.getDecorView();
// 获取布局填充器
final LayoutInflater inflater = LayoutInflater.from(mContext);
ViewGroup subDecor = null;
// 根据是否有标题和是否为浮动窗口来决定使用哪个布局
if (!mWindowNoTitle) {
if (mIsFloating) {
// 如果是浮动窗口,则使用对话框标题装饰
subDecor = (ViewGroup) inflater.inflate(
R.layout.abc_dialog_title_material, null);
//**
} else if (mHasActionBar) {
// 如果有动作栏,则使用特定主题创建布局
// 使用主题化上下文填充视图并设置为内容视图
subDecor = (ViewGroup) LayoutInflater.from(themedContext)
.inflate(R.layout.abc_screen_toolbar, null);
//**
}
} else {
// 根据是否覆盖动作模式选择不同布局
if (mOverlayActionMode) {
subDecor = (ViewGroup) inflater.inflate(
R.layout.abc_screen_simple_overlay_action_mode, null);
//**
} else {
subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
//**
}
}
//**
// 将窗口的内容视图设置为subDecor
mWindow.setContentView(subDecor);
//**
return subDecor;
}
可以看到这个方法返回一个配置好的 subDecor
,subDecor
使用的是系统的布局,根据配置的不同,使用了不同的xml
。返回给我们用于添加LayoutId
,但是它是如何显示的还是不清楚,但是注意到mWindow.setContentView(subDecor);
。
我们点下去一看,回来到Window类:
java
/**
* Convenience for
* {@link #setContentView(View, android.view.ViewGroup.LayoutParams)}
* 将屏幕内容设置为显式视图。此视图直接放置在屏幕的视图层次结构中。它本身可以是一个复杂的视图层次结构。
* @param view The desired content to display.
* @see #setContentView(View, android.view.ViewGroup.LayoutParams)
*/
public abstract void setContentView(View view);
显然这是整个视图显示过程中非常核心的一步。但是它是抽象的!不过根据我们小学二年级就学过的
The only existing implementation of this abstract class is android.view.PhoneWindow, which you should instantiate when needing a Window.
是的,Window
只有一个实现类-PhoneWindow
。
三、PhoneWindow
我们找到PhoneWindow
:
csharp
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
if (mContentParent == null) {
// 如果内容父视图还未创建,则进行安装
installDecor();
}
//**
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
//**
} else {
// 将视图添加到内容父视图中
mContentParent.addView(view, params);
}
//**
}
我们看看如何初始化mContentParent
,走进installDecor()
的内心。
csharp
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
// 创建窗口装饰视图
mDecor = generateDecor(-1);
//**
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
// 生成并设置内容布局
mContentParent = generateLayout(mDecor);
//**
} else {
//**
}
// 省略涉及到过渡管理器和动画的配置
}
}
在这里我们可以看到两个,generateDecor
,但是在createSubDecor
中我们已经创建过了:
ini
protected ViewGroup generateLayout(DecorView decor) {
// ... 省略了一部分属性设置代码 ...
// 根据窗口特性选择布局资源
int layoutResource;
int features = getLocalFeatures();
// 根据不同的特性标志选择不同的布局资源
if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
if (mIsFloating) {
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_title_icons;
}
} else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) !=
&& (features & (1 << FEATURE_ACTION_BAR)) == 0) {
layoutResource = R.layout.screen_progress;
} else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
if (mIsFloating) {
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_custom_title;
}
} else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
if (mIsFloating) {
layoutResource = res.resourceId;
} else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
layoutResource = a.getResourceId(
R.styleable.Window_windowActionBarFullscreenDecorLayout,
R.layout.screen_action_bar);
} else {
layoutResource = R.layout.screen_title;
}
} else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
layoutResource = R.layout.screen_simple_overlay_action_mode;
} else {
layoutResource = R.layout.screen_simple;
}
// 装饰视图开始变化
mDecor.startChanging();
// 使用LayoutInflater加载布局资源
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
// 获取内容父视图
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
// ... 省略了其他设置代码 ...
// 装饰视图完成变化
mDecor.finishChanging();
return contentParent;
}
在这个方法中,会根据不同的特性标志选择不同的布局资源,但是这些布局都有一个显著的特点。他们都有一个id为content的FrameLayout。就是那个老生常谈的android.R.id.content
~。
ini
<FrameLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent" />
四、倒着回去
至此,我们知道了PhoneWindow
的setContentView
中的contentParent
来自哪里:
那我们也是知道了AppCompatDelegate
中的setContentView
的contentParent
来自哪里:
捋一下:
- 在活动的
onCreate
方法中调用setContentView
,传入布局资源ID或者直接传入一个视图(View)对象。 - 从
AppCompatDelegate
中调用Window.setContentView
。 PhoneWindow
对象负责创建和管理顶层视图容器,DecorView
。如果DecorView
还未创建,Window
会通过调用generateDecor
方法来创建它。DecorView中一定有一个ID为android.R.id.content
的FrameLayout。AppCompatDelegateImpl
将视图添加到android.R.id.content
。
五、Activity
为什么没有提及Activity呢?细心的大家应该发现了,AppCompatActivity的setContentView是一个重写方法,它完全重写了父类。
Overrides method in Activity
我们往上看一下:
less
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
显然,最后Activity也是回到了PhoneWindow,至于为什么要这样呢?你猜~~~~嘻嘻。
六、LayoutInflater
不知道大家有没有注意到PhoneWindow和AppCompatDelegate中的LayoutInflater,那么DecorView和我们的Layout是如何渲染到屏幕的呢?请看LayoutInflater。
用Google话来说:
将布局 XML 文件实例化到其相应的 View 对象中
请见下回分解。
七、总结
啊,有点困。冰美式压不住我的睡意。刀了。下一篇我们来说说LayoutInflater。
如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏