@SystemService(Context.LAYOUT_INFLATER_SERVICE)
public abstract class LayoutInflater {
... ...
}
static {
... ...
registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
new CachedServiceFetcher<LayoutInflater>() {
@Override
public LayoutInflater createService(ContextImpl ctx) {
//创建LayoutInlater,具体类是PhoneLayoutInflater
return new PhoneLayoutInflater(ctx.getOuterContext());
}});
... ...
}
public class PhoneLayoutInflater extends LayoutInflater {
//内置View类型的前缀,如TextView的完整路径是android.widget.TextView
private static final String[] sClassPrefixList = {
"android.widget.",
"android.webkit.",
"android.app."
};
... ...
}
/** Override onCreateView to instantiate names that correspond to the
widgets known to the Widget factory. If we don't find a match,
call through to our super class.
重写onCreateView以 实例化 与之对应的名称 (Widget 工厂所了解的)。 如果我们找不到匹配,请通过父类。
*/
@Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
//在View名字的前面添加前缀来构造View的完整路径,例如,类名为TextView,那么TextVuiew完整的路径是android.widget.TextView
for (String prefix : sClassPrefixList) {
try {
View view = createView(name, prefix, attrs);
if (view != null) {
return view;
}
} catch (ClassNotFoundException e) {
// In this case we want to let the base class take a crack
// at it.
}
}
return super.onCreateView(name, attrs);
}
代码不多,核心是覆写了LayoutInflater的onCreateView方法,该方法就是在传递进来的View的名字上加上“android.widget.”或者"android.webkit."前缀用以得到该内置View类(如TextView、Button等都在android.widget包下)的完整路径。最后根据类的完整路径来构造对应的View对象。
具体是一个怎样的流程?以Activity 的setContentView为例:
public class Activity extends ContextThemeWrapper
implements LayoutInflater.Factory2,
Window.Callback, KeyEvent.Callback,
OnCreateContextMenuListener, ComponentCallbacks2,
Window.OnWindowDismissedCallback, WindowControllerCallback,
AutofillManager.AutofillClient {
... ...
}
/**
* Set the activity content from a layout resource. The resource will be
* inflated, adding all top-level views to the activity.
*
* @param layoutResID Resource ID to be inflated.
*
* @see #setContentView(android.view.View)
* @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
*/
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
/**
* Set the activity content to an explicit view. This view is placed
* directly into the activity's view hierarchy. It can itself be a complex
* view hierarchy. When calling this method, the layout parameters of the
* specified view are ignored. Both the width and the height of the view are
* set by default to {@link ViewGroup.LayoutParams#MATCH_PARENT}. To use
* your own layout parameters, invoke
* {@link #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)}
* instead.
*
* @param view The desired content to display.
*
* @see #setContentView(int)
* @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
*/
public void setContentView(View view) {
getWindow().setContentView(view);
initWindowDecorActionBar();
}
Activity的setContentView方法实际调用的是Window的setContentView,而Window是一个抽象类,上文提到的Window的具体实现类是PhoneWindow。
@Override
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) {
//1.当mContentparent为空时先构建DecorView
//
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 {
//解析layoutResID,通过inflate函数将指定的布局视图添加到mContentarent中
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
从上图发现:
我们发现mDecor中会加载一个系统定义好的布局,这个布局中又包裹了mContentParent,而这个mContentParent就是我们设置的布局,并添加到parent区域。
在PhoneWindow的setContentView中验证了这一点,首先会构建mContentParent对象,然后通过LayoutInflater的inflate函数将指定布局的视图添加到mContentParent中。
/**
* Inflate a new view hierarchy from the specified xml resource. Throws
* {@link InflateException} if there is an error.
*
* @param resource ID for an XML layout resource to load (e.g.,
* <code>R.layout.main_page</code>)
* @param root Optional view to be the parent of the generated hierarchy.
* @return The root View of the inflated hierarchy. If root was supplied,
* this is the root View; otherwise it is the root of the inflated
* XML file.
*/
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
/**
* Inflate a new view hierarchy from the specified xml node. Throws
* {@link InflateException} if there is an error. *
* <p>
* <em><strong>Important</strong></em> For performance
* reasons, view inflation relies heavily on pre-processing of XML files
* that is done at build time. Therefore, it is not currently possible to
* use LayoutInflater with an XmlPullParser over a plain XML file at runtime.
*
* @param parser XML dom node containing the description of the view
* hierarchy.
* @param root Optional view to be the parent of the generated hierarchy.
* @return The root View of the inflated hierarchy. If root was supplied,
* this is the root View; otherwise it is the root of the inflated
* XML file.
*/
public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {
return inflate(parser, root, root != null);
}
/**
* Inflate a new view hierarchy from the specified xml resource. Throws
* {@link InflateException} if there is an error.
*
* @param resource ID for an XML layout resource to load (e.g.,
* <code>R.layout.main_page</code>)
* @param root Optional view to be the parent of the generated hierarchy (if
* <em>attachToRoot</em> is true), or else simply an object that
* provides a set of LayoutParams values for root of the returned
* hierarchy (if <em>attachToRoot</em> is false.)
* @param attachToRoot Whether the inflated hierarchy should be attached to
* the root parameter? If false, root is only used to create the
* correct subclass of LayoutParams for the root view in the XML.
* @return The root View of the inflated hierarchy. If root was supplied and
* attachToRoot is true, this is root; otherwise it is the root of
* the inflated XML file.
*/
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
/**
* Inflate a new view hierarchy from the specified XML node. Throws
* {@link InflateException} if there is an error.
* <p>
* <em><strong>Important</strong></em> For performance
* reasons, view inflation relies heavily on pre-processing of XML files
* that is done at build time. Therefore, it is not currently possible to
* use LayoutInflater with an XmlPullParser over a plain XML file at runtime.
*
* @param parser XML dom node containing the description of the view
* hierarchy.
* @param root Optional view to be the parent of the generated hierarchy (if
* <em>attachToRoot</em> is true), or else simply an object that
* provides a set of LayoutParams values for root of the returned
* hierarchy (if <em>attachToRoot</em> is false.)
* @param attachToRoot Whether the inflated hierarchy should be attached to
* the root parameter? If false, root is only used to create the
* correct subclass of LayoutParams for the root view in the XML.
* @return The root View of the inflated hierarchy. If root was supplied and
* attachToRoot is true, this is root; otherwise it is the root of
* the inflated XML file.
*/
#LayoutInflater
//参数1 为xml解析器 参数2 为要解析布局的父视图 参数3为是否将要解析的视图添加到父视图中
//这里使用的是android的XmlPullParser解析
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
... ...
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
//Context对象
mConstructorArgs[0] = inflaterContext;
//存储父视图
View result = root;
try {
// Look for the root node.
int type;
//找到root元素
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
... ...
final String name = parser.getName();
... ...
//1. 解析merge标签
if (TAG_MERGE.equals(name)) {
... ...
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
//2.不是merge标签直接解析布局中的视图
//3.这里通过xml的tag来解析layout根视图
//name就是要解析的视图的类名,如RelativeLayout
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
... ...
// Create layout params that match root, if supplied
// 生成布局参数
params = root.generateLayoutParams(attrs);
//如果attachToRoot为false,那么给temp设置布局参数
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
// Inflate all children under temp against its context.
//解析temp视图下所有的子View
rInflateChildren(parser, temp, attrs, true);
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
//如果Root不为空,且attachToRoot为true,那么将temp添加到父视图中
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.
//如果root为空或者attachToRoot为false,那么返回结果就是temp
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
... ...
}
return result;
}
}
上述的inflate方法中,主要有以下几步:
1.解析xml中的根标签(第一个元素)
2.如果根标签是merge,那么调用rInflate进行解析,rInflate会将merge标签下的所有子VIew直接添加到根标签中。
3.如果标签是普通元素,那么运行到代码3,调用createViewFromTag对该元素进行解析。
4.调用rInflate解析temp根元素下的所有的子View,并且将这些子View都添加到temp下;
5.返回解析到的根视图。
解析单个元素的createViewFromTag方法
/**
* Convenience method for calling through to the five-arg createViewFromTag
* method. This method passes {@code false} for the {@code ignoreThemeAttr}
* argument and should be used for everything except {@code >include>}
* tag parsing.
*/
private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
return createViewFromTag(parent, name, context, attrs, false);
}
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
// Apply a theme wrapper, if allowed and one is specified.
if (!ignoreThemeAttr) {
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
if (themeResId != 0) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
}
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}
try {
View view;
//1.用户可以通过设置LayoutInflater的factory来自行解析View,默认这些Factory都为空,可以忽略这段
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);
}
//2.没有Factory的情况下通过onCreateView或者createView创建View
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
//3.内置View控件的解析
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
//4.自定义控件的解析
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
} catch (InflateException e) {
... ...
}
}
本程序的重点在于代码2,以及以后的代码,createViewFromTag将该元素的parent及名字传递过来。
区分内置View和自定义View的方式:
当这个tag的名字中没有包含“.”(在名字中查找“.”返回-1)时,LayoutInflater会认为这是一个内置的View。
例:
<FrameLayout
android:id="@+id/play_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
这里的FrameLayout就是xml元素的名字,因此在执行inflate时就会调用3处的onCreateView来解析这个FrameLayout标签。当我们使用自定义View时,在xml中必须写View的完整路径。
<com.duan.musicoco.view.ColorPickerView
android:id="@+id/theme_custom_color_primary"
/>
此时调用代码注释的4的createView来解析该View。
在上文的PhoneLayoutInflater中,PhoneLayoutInflater覆写了onCreateView方法,也就是代码3处的onCreateView方法,该方法就是在View的标签名的前面设置一个"android.widget."前缀,然后传递给createView进行解析。
也就是内置View 和 自定义 View最终都调用了createView进行解析,只是Google为了让开发者在xml中更方便定义View,只写View名称而不需要写完整的路径。
在LayoutInflater解析时,如果遇到只写类名的View,那么认为是内置的View控件,在onCreateView中将"android.widget."前缀传递给craeteView方法。
最后在crateView中构造View 的完整路径来解析。
如果是自定义控件,那么必须写完整路径,此时调用createView且前缀为null进行解析。
//createView相关代码
//根据完整路径的类名通过反射机制构造View对象
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
//1.从缓存中获取构造函数
Constructor<? extends View> constructor = sConstructorMap.get(name);
if (constructor != null && !verifyClassLoader(constructor)) {
constructor = null;
sConstructorMap.remove(name);
}
Class<? extends View> clazz = null;
try {
//2.没有缓存构造函数
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
//如果prefix不为空,那么构造完整的View路径,并且加载该类
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
}
//3.从Class对象获取构造函数
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
//4.将构造函数存入缓存中
sConstructorMap.put(name, constructor);
} else {
... ...
}
Object lastContext = mConstructorArgs[0];
if (mConstructorArgs[0] == null) {
// Fill in the context if not already within inflation.
mConstructorArgs[0] = mContext;
}
Object[] args = mConstructorArgs;
args[1] = attrs;
//5.通过反射构造View
final View view = constructor.newInstance(args);
if (view instanceof ViewStub) {
// Use the same context when inflating ViewStub later.
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
mConstructorArgs[0] = lastContext;
return view;
} catch (NoSuchMethodException e) {
... ...
}
}
createView相对比较简单,如果有前缀,那么构造View的完整路径,并且将该类加载到虚拟机中,然后获取该类的构造函数并缓存起来,再通过构造函数来创建View的对象,最后将View对象返回,这就是解析单个View的过程。
而我们的窗口是一棵视图树,LayoutInflater需要解析这棵树,这个功能就交给了rInflate方法。
/**
* Recursive method used to descend down the xml hierarchy and instantiate
* views, instantiate their children, and then call onFinishInflate().
* <p>
* <strong>Note:</strong> Default visibility so the BridgeInflater can
* override it.
*/
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
//1.获取树的深度,深度优先遍历
final int depth = parser.getDepth();
int type;
boolean pendingRequestFocus = false;
//挨个元素解析
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName();
if (TAG_REQUEST_FOCUS.equals(name)) {
pendingRequestFocus = true;
consumeChildElements(parser);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {//解析 include 标签
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {// 解析merge标签,抛出异常,因为merge标签必须为根视图。
throw new InflateException("<merge /> must be the root element");
} else {
//3.根据元素名进行解析
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
//递归调用进行解析,也就是深度优先遍历
rInflateChildren(parser, view, attrs, true);
//将解析到的View添加到ViewGroup中,也就是其parent
viewGroup.addView(view, params);
}
}
if (pendingRequestFocus) {
parent.restoreDefaultFocus();
}
if (finishInflate) {
parent.onFinishInflate();
}
}
rInflate 通过深度优先遍历,每解析一个View元素就会递归调用rInflate,直到这条路径下的最后一个元素,然后再回溯过来将每个View元素添加到它们的parent中。通过rInflate的解析之后,整棵视图树就构建完毕。当调用了activity的onResume()之后,我们通过steContentView设置的内容就会出现在视野中。
使用深度优先搜索来遍历这个图的具体过程是:
首先从一个未走到过的顶点作为起始顶点,比如1号顶点作为起点。
沿1号顶点的边去尝试访问其它未走到过的顶点,首先发现2号顶点还没有走到过,于是来到了2号顶点。
再以2号顶点作为出发点继续尝试访问其它未走到过的顶点,这样又来到了4号顶点。
再以4号顶点作为出发点继续尝试访问其它未走到过的顶点。
但是,此时沿4号顶点的边,已经不能访问到其它未走到过的顶点了,所以需要返回到2号顶点。
返回到2号顶点后,发现沿2号顶点的边也不能再访问到其它未走到过的顶点。此时又会来到3号顶点(2->1->3),再以3号顶点作为出发点继续访问其它未走到过的顶点,于是又来到了5号顶点。
至此,所有顶点我们都走到过了,遍历结束。
参考《Android源码设计模式》
深度优先遍历的主要思想是:
1.首先以一个未被访问过的顶点作为起始顶点,沿当前顶点的边走到未访问过的顶点;
2.当没有未访问过的顶点时,则回到上一个顶点,继续试探别的顶点,直到所有的顶点都被访问过。