在 Activity 中实现 getContentView 操作
2017/9/8 17:17:03
前言
最近接到个需要优化Android原生系统设置APK的任务。这个任务里面有一个更换应用背景图片的需求。我手里的这个设备是一个平板设备,使用了一下这个原生设置APK,感觉它有点像是一个主Activity,通过更换Fragment的方式来切换不同的展示内容。这样一来就好办了,想着直接找到这个Activity,看看它是 set 了哪一个 layout 进去,然后再直接在这个 layout 中添加个背景图片就好了。但后来跟踪了一下源码,发现并没有这么简单。这个主Activity是继承自Android打包好的 PreferenceActivity 来实现的,而设置布局的工作,已经由这个父Activity完成了(如下图所示)。完全没有子Activity什么事,并且PreferenceActivity还没有暴露任何接口能让子类取得布局。而且像这种设置方式,想反射都不好反射。
./frameworks/base/core/java/android/preference/PreferenceActivity.java
那这样可如何是好呢?如果能有一个 getContentView() 方法就好了。既然Android官方不提供,那我们干脆跟踪跟踪源码,看看能不能自己造一个出来。
开发环境
操作系统:Android4.4.2
硬件设备:智能音箱中的平板设备。
编译需求:有完整源代码,能够正常编译大包。
追本溯源-跟踪源代码
首先来看看Activity.java中的 setContentView() 是如何处理的。
./frameworks/base/core/java/android/app/Activity.java
原来与 Window 有关啊。再看看 getWindow() 中是如何提供 Window 对象的。
到这里,我们就知道了,Activity中使用的 Window 就是 PhoneWindow 类的对象了。那么,我们直接去 PhoneWindow 类中看看它的 setContentView() 作了什么操作。
./frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindow.java
由上图所示代码第290行,我们知道了原来 set 进去的 layout 就是被加载到了这个 mContentParent 容器中去了啊。这个 mContentParent 是一个 ViewGroup 类的对象。那这个 mContentParent 又是怎么来的呢?
注意到上图第285行的条件判断语句里,似乎这个 mContentParent 对象还与 installDecor() 方法有关呢!去看看。
看来刚才猜想的没错,这个 mContentParent 确实和这个 Decor 有着很密切的关系。先去 generateDecor() 方法里看看这个 mDecor 是个什么来头。
简单粗暴,我喜欢!!!DecorView是一个定义在 PhoneWindow 内部的内部类,它是 FrameLayout 的子类,如下图所示。
然后我们再去看看 mContentParent 是如何由 mDecor 生成的。追踪 generateLayout() 方法。
generateLayout() 方法代码量较大,我们不需要看懂每一行代码的含义,只看我们需要的就好。
protected ViewGroup generateLayout(DecorView decor) {
// 为后续的加载作前期准备工作,读取属性值,选择布局文件等。
// ...
mDecor.startChanging();
View in = mLayoutInflater.inflate(layoutResource, null);
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);// ID_ANDROID_CONTENT = com.android.internal.R.id.content;
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
if (getContainer() == null) {
Drawable drawable = mBackgroundDrawable;
if (mBackgroundResource != 0) {
drawable = getContext().getResources().getDrawable(mBackgroundResource);
}
mDecor.setWindowBackground(drawable);
drawable = null;
if (mFrameResource != 0) {
drawable = getContext().getResources().getDrawable(mFrameResource);
}
mDecor.setWindowFrame(drawable);
}
mDecor.finishChanging();
return contentParent;
}
由上述代码第7行可知,mDecor在这里添加了一个View,准确说,它添加的肯定是一个 ViewGroup 类对象。这次添加也是 mDecor 首次添加,即在 mDecor 的 child 中,它是第0个。
然后第9行的 ViewGroup contentParent 就是最终要返回的容器对象。这行代码后面的 findViewById() 方法是定义在 Window.java 中的方法。它其实就是通过 mDecor 来根据 ID 查找 View 。
./frameworks/base/core/java/android/view/Window.java
到这里,整个加载布局的流程就很清楚了。我们把它总结一下。
那么,借此设置流程图,创造 getContentView() 方法的步骤也清晰了。如下图所示:
这里有一个很重要的、也是关键的一点,就是Activity提供了 getWindow() 方法用于取得 Window 对象,且这个 Window 对象又提供了取得 mDecorView 对象的接口。
代码实操
更改Android原生系统设置APK中的背景图片。
./packages/apps/Settings/src/.../Settings.java
至此,我们不仅完成了更换不是由自己设置布局的Activity的背景图片,还了解到了系统设置布局的流程的知识。
<wiz_tmp_tag id="wiz-table-range-border" contenteditable="false" style="display: none;">
+++