一.目标
【framework代码可以直接用Android Studio打开SDK目录下面的resouce,这里也推荐一个网址:http://androidxref.com/6.0.1_r10/xref/这个会比较完整,当然如果不涉及NDK内容SDK下的resouce还是够用】
首先讨论这个话题重点不是为了解析源码而解析源码,主要的目标有两个:
1)了解setContentView的源码
2)利用源码中的原理来实现我们的效果,主要为了引出
2.1.《SnackBar的源码分析》
2.2.《利用decorView机制实现底部弹出框》.
2.3.《小红书欢迎页的视差效果实现》
所以我会在代码的关键地方进行提醒!提醒!提醒!
二.setContentView源码分析
1.Activity setContentView
首先在分析之前我们明确一下在进入Activity的onCreate我们会调用setContentView(layoutResID)方法,所以我们第一步就是跳入到Acitivity的setContentView方法:
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
可以看到里面获取了window,然后调用了window的setcontentView方法,那么这个window到底是什么呢?
2.getWindow()
说起这个window是什么就比较复杂,需要从Acitivity的启动过程进行分析,这边就简单分析下,首先我们知道Activity最终都会在ActivityThread这个类中的performLaunchActivity进行启动。这个方法如下:
Activity activity = null;
try {
//利用类加载器来实例化Acitivity对象
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state != null) {
r.state.setClassLoader(cl);
}
} catch (Exception e) {
if (!mInstrumentation.onException(activity, e)) {
throw new RuntimeException(
"Unable to instantiate activity " + component
+ ": " + e.toString(), e);
}
}
try {
...
if (activity != null) {
Context appContext = createBaseContextForActivity(r, activity);
CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
Configuration config = new Configuration(mCompatConfiguration);
if (r.overrideConfig != null) {
config.updateFrom(r.overrideConfig);
}
if (DEBUG_CONFIGURATION) Slog.v(TAG, "Launching activity "
+ r.activityInfo.name + " with config " + config);
Window window = null;
if (r.mPendingRemoveWindow != null && r.mPreserveWindow) {
window = r.mPendingRemoveWindow;
r.mPendingRemoveWindow = null;
r.mPendingRemoveWindowManager = null;
}
//调用其attach方法为其关联运行过程中所依赖的一系列上下文环境变量
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window);
在Activity的attach方法里,系统会创建Activity所属的Window对象并为其设置回调接口,具体方法如下:
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
//从这里可以知道Activity的window是PhoneWindow
mWindow = new PhoneWindow(this, window);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
这样我们就知道我们getWindow()方法得到的是PhoneWindow。
3.PhoneWindow setContentView
这样我们回到源代码getWindow().setContentView(layoutResID);
进入我们PhoneWindow的setContentView代码:
// 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)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
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;
1).从代码中我们看到首先会判断mContentParent是否为空,如果为空则调用installDecor方法,如果不为空则removeAllViews即清楚所有子Views,然后通过 mLayoutInflater.inflate(layoutResID, mContentParent);方法将我们的布局放进mContentParent 中,所以我们猜想installDecor就是为了初始化我们的mContentParent,接下来我们会进行分析。
2).然后程序会通过getCallBack()方法获取到一个CallBack对象,这里的CallBack对象又是什么呢?其实我们上面已经在Acitivity的attach方法有mWindow.setCallback(this),所以这个Callback对象其实就是Acitivity自己。
3).当PhoneWindow接收到系统分发给它的触摸、IO、菜单等相关的事件时,可以回调相应的Activity进行处理。至于Callback可以回调哪些方法,自己看下这个接口的声明方法即可。当然了这里不是我们的关键,因为我们的setContentView里面只是回调了onContentChanged,而onContentChanged在Activity中是空实现。
4.PhoneWindow installDecor()
接下来我们就可以查看installDecor方法了:
mForceDecorInstall = false;
if (mDecor == null) {
//生成decorView对象
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
//初始化mContentParent 对象
mContentParent = generateLayout(mDecor);
// Set up decor part of UI to ignore fitsSystemWindows if appropriate.
mDecor.makeOptionalFitsSystemWindows();
final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
R.id.decor_content_parent);
if (decorContentParent != null) {
mDecorContentParent = decorContentParent;
mDecorContentParent.setWindowCallback(getCallback());
......
} else {
mTitleView = (TextView) findViewById(R.id.title);
if (mTitleView != null) {
......
}
}
从代码可以看到mDecor为空的话会先生成一个DecorView对象,然后会通过DecorView对象初始化我们的mCntentParent,那这些过程到底是什么样的呢?接下来我们再来一一分析。
5.PhoneWindow generateDecor
首先我们知道这个方法是初始化一个DecorView对象,那具体到底是什么样的呢?方法如下:
protected DecorView generateDecor(int featureId) {
// System process doesn't have application context and in that case we need to directly use
// the context we have. Otherwise we want the application context, so we don't cling to the
// activity.
Context context;
.......
return new DecorView(context, featureId, this, getAttributes());
}
代码很简单,只是new出了一个DecorView对象,我们跟进DecorView类发现
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks{
}
其实DecorView只是一个FrameLayout布局(这个DecorView是在WindowManager中attach到Window里的)。
6.PhoneWindow generateLayout
那么我们接下来就看DecorView这个对象传进generateLayout方法到底做了什么呢?
protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
//首先通过WindowStyle中设置的各种属性,对Window进行requestFeature或者setFlags
TypedArray a = getWindowStyle();
mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
//中间省略一些代码
.................
// Inflate the window decor.
int layoutResource;
int features = getLocalFeatures();
// System.out.println("Features: 0x" + Integer.toHexString(features));
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) {
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleIconsDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_title_icons;
}
// XXX Remove this once action bar supports these features.
removeFeature(FEATURE_ACTION_BAR);
// System.out.println("Title Icons!");
} else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
&& (features & (1 << FEATURE_ACTION_BAR)) == 0) {
// Special case for a window with only a progress bar (and title).
// XXX Need to have a no-title version of embedded windows.
layoutResource = R.layout.screen_progress;
// System.out.println("Progress!");
} else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
// Special case for a window with a custom title.
// If the window is floating, we need a dialog layout
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogCustomTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_custom_title;
}
// XXX Remove this once action bar supports these features.
removeFeature(FEATURE_ACTION_BAR);
} else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
// If no other features and not embedded, only need a title.
// If the window is floating, we need a dialog layout
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleDecorLayout, res, true);
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;
}
// System.out.println("Title!");
} else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
layoutResource = R.layout.screen_simple_overlay_action_mode;
} else {
// Embedded, so no decoration is needed.
layoutResource = R.layout.screen_simple;
// System.out.println("Simple!");
}
//前方高能请注意!!!!!!!!!!!!!
mDecor.startChanging();
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
..............
mDecor.finishChanging();
return contentParent;
}
从这个方法我们可以看出根据一些feature我们代码会去加载不同的layoutResource ,我们挑一个最简单的layoutResource 来进行查看下即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" />
//这个地方就是我们set进来布局存放的地方
<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>
我们猜想一下这个地方这个布局就是后面add到我们DecorView里面的布局,所以我们接下来进行验证下。
7.DecorView onResourcesLoaded
这个方法就是把我们的layout设置进decorview中,具体代码如下:
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
mStackId = getStackId();
if (mBackdropFrameRenderer != null) {
loadBackgroundDrawablesIfNeeded();
mBackdropFrameRenderer.onResourcesLoaded(
this, mResizingBackgroundDrawable, mCaptionBackgroundDrawable,
mUserCaptionBackgroundDrawable, getCurrentColor(mStatusColorViewState),
getCurrentColor(mNavigationColorViewState));
}
mDecorCaptionView = createDecorCaptionView(inflater);
//看重点,这句就是inflate出我们的布局
final View root = inflater.inflate(layoutResource, null);
if (mDecorCaptionView != null) {
if (mDecorCaptionView.getParent() == null) {
addView(mDecorCaptionView,
new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mDecorCaptionView.addView(root,
new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
} else {
// Put it below the color views.
addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mContentRoot = (ViewGroup) root;
initializeElevation();
}
从代码可以看出首先会在这句final View root = inflater.inflate(layoutResource, null);把我们的布局创建出来,然后后面会加到mDecorCaptionView中,这样我们的setContentView就完成了,我们通过图片来说明刚才我们的分析过程:
最终我们的真正的布局其实是在id为Content的framework下面的。
这个地方要提高警惕,这个知识点我们会在2.1.SnakeBar的源码分析
2.2.利用decorView机制实现dialog中用到,注意!注意!注意!
故事到这里还没有结束,我们刚才看到了在创建我们布局的时候用到final View root = inflater.inflate(layoutResource, null),那我们这里又要分析一下inflater这个对象到底是怎么inflate出我们布局的,这个地方有个知识点到时会在小红书视差动画实现案例中用到。
8.LayoutInflater.from(context) inflate
上面的inflater对象我们看源码都知道是从PhoneWindow中的generateLayout时传进onResourcesLoaded方法中的,所以我们直接在PhoneWindow这个类中找出这个对象,我们知道这个inflater对象会在构造函数调用的时候就进行初始化:
public PhoneWindow(Context context) {
super(context);
mLayoutInflater = LayoutInflater.from(context);
}
接下来我们跟进from方法:
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}
所以他其实就是从上下文对象中获取系统的一个服务,具体getSystemService方法做了些神马这个地方就不深究,我们直接看重点。直接跟进inflate方法:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
//解析XML的根标签。
try {
// Look for the root node.
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
final String name = parser.getName();
if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}
//如果是merge,调用rInflate进行解析。它会把merge所有子view添加到根标签中。
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
//如果是普通标签,调用createViewFromTag进行解析。
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
//用rInflate解析temp根元素下的子view。并添加到temp中。
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
final InflateException ie = new InflateException(e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(parser.getPositionDescription()
+ ": " + e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
//最后返回root
return result;
}
}
从源码中添加的注释我们可以看到,如果xml布局中的标签是普通标签则会调用createViewFromTag方法进行解析,那么我们看看createViewFromTag方法到底做了些啥:
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
//此处省略一些代码,减少眼疲劳
............
try {
View view;
//前方高能!高能!高能!
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
} catch (InflateException e) {
throw e;
} catch (ClassNotFoundException e) {
..........
}
}
我们在高能区域看到首先会判断mFactory2 ,mFactory 来进行createView即创建view,那么两个又是什么东西呢?心累60秒。。。。。
心痛完继续前行,这两个玩意可谓是作用很大(这两个东西会用来做拦截view的创建过程,从而可以做一些换肤和之后要说的小红书视差效果实现)是不是感觉顿时有了激情。
其实mFactory2 ,mFactory 这两个东西基本是一样的,mFactory2 是SDK>=11后引入的。v4包下有个类LayoutInflaterCompat帮我们完成了兼容性的操作。这里又有一个知识点要说为什么我们的support包可以支持在低版本下使用高版本的控件,其实也是用的这个功能,我们从support包进去有个类AppCompatDelegateImplV9,在这里面会默认设置一个factory来拦截视图创建过程,从而达到可以使用高版本控件的能力
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory(layoutInflater, this);
} else {
if (!(LayoutInflaterCompat.getFactory(layoutInflater)
instanceof AppCompatDelegateImplV9)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
这里如果有需要以后会分析一下support包的源码。所以我们知道我们也可以在LayoutInflater中调用setFactory设置我们的factory来进行拦截和自定义解析xml内容,我们看下fatory接口的一个说明:
public interface Factory {
//当我们使用LayoutInflater渲染View的时候便会回调该Hook方法,从而我们可以自定义自己tag的名称
/**
* Hook you can supply that is called when inflating from a LayoutInflater.
* You can use this to customize the tag names available in your XML
* layout files.
*
* <p>
* Note that it is good practice to prefix these custom names with your
* package (i.e., com.coolcompany.apps) to avoid conflicts with system
* names.
*
* @param name Tag name to be inflated.
* @param context The context the view is being created in.
* @param attrs Inflation attributes as specified in XML file.
*
* @return View Newly created view. Return null for the default
* behavior.
*/
public View onCreateView(String name, Context context, AttributeSet attrs);
}
到这我们的setContentView就算是最终完成了。本篇文章主要是一个引子,希望在接下来的例子里面,大家能融会贯通!!!!