Android-LayoutInflater布局文件解析过程分析
备注:
本篇文章所引用的源码版本:android-sdk-21
转载请注明出处:http://blog.csdn.net/a740169405/article/details/54580347
简述:
简单的说,LayoutInflater就是是一个用来解析xml布局文件的类。该篇文章将对LayoutInflater类进行分析,内容包括:
1. LayoutInflater在哪里创建
2. 如何获取LayoutInflater对象
3. 视图的创建过程(xml转换成View的过程)
4. inflate方法的两个重要参数(root、attachToRoot)分析
LayoutInflater的来源:
LayoutInflater和其他系统服务一样,也是在ContextImpl类中进行注册的,ContextImpl类中有一个静态代码块,应用程序用到的系统服务都在这进行注册:
class ContextImpl extends Context {
static {
// ...
// 注册ActivityManager服务
registerService(ACTIVITY_SERVICE, new ServiceFetcher() {
public Object createService(ContextImpl ctx) {
return new ActivityManager(ctx.getOuterContext(), ctx.mMainThread.getHandler());
}});
// 注册WindowManager服务
registerService(WINDOW_SERVICE, new ServiceFetcher() {
Display mDefaultDisplay;
public Object getService(ContextImpl ctx) {
Display display = ctx.mDisplay;
if (display == null) {
if (mDefaultDisplay == null) {
DisplayManager dm = (DisplayManager)ctx.getOuterContext().
getSystemService(Context.DISPLAY_SERVICE);
mDefaultDisplay = dm.getDisplay(Display.DEFAULT_DISPLAY);
}
display = mDefaultDisplay;
}
return new WindowManagerImpl(display);
}});
// ....
// 注册LayoutInflater服务
registerService(LAYOUT_INFLATER_SERVICE, new ServiceFetcher() {
public Object createService(ContextImpl ctx) {
return PolicyManager.makeNewLayoutInflater(ctx.getOuterContext());
}});
// ...其他服务的注册,不一一列举,有兴趣可以自己看源码
}
// ...其他代码
// 存储所有服务的ServiceFetcher集合
private static final HashMap<String, ServiceFetcher> SYSTEM_SERVICE_MAP =
new HashMap<String, ServiceFetcher>();
private static void registerService(String serviceName, ServiceFetcher fetcher) {
if (!(fetcher instanceof StaticServiceFetcher)) {
fetcher.mContextCacheIndex = sNextPerContextServiceCacheIndex++;
}
SYSTEM_SERVICE_MAP.put(serviceName, fetcher);
}
}
从代码中可以发现,除了LayoutInflater的注册,还有我们常见的WindowManager、ActivityManager等的注册。所有的注册都调用了静态方法:registerService,这里所有的服务并不是在静态代码块中直接创建,而是采用饥渴式方法,只创建了对应服务的获取器ServiceFetcher对象。在真正使用特定服务的时候才创建,SYSTEM_SERVICE_MAP是一个静态的集合对象,存储了所有服务的获取器(ServiceFetcher)对象,map的键是对应服务的名称。只需要调用获取器(ServiceFetcher)的getService(Context context)方法既可以获取对应的系统服务。
我们只关注LayoutInflater的获取器(ServiceFetcher)是如何实现的,其getService(Context context);方法调用了com.android.internal.policy.PolicyManager#makeNewLayoutInflater(Context context)
public static LayoutInflater makeNewLayoutInflater(Context context) {
return new BridgeInflater(context, RenderAction.getCurrentContext().getProjectCallback());
}
这里提一下,上面代码是android-sdk-21版本的源码,创建了一个BridgeInflater对象,如果是android-sdk-19及以下的源码,PolicyManager#makeNewLayoutInflater方法应该是:
public static LayoutInflater makeNewLayoutInflater(Context context) {
return sPolicy.makeNewLayoutInflater(context);
}
接着调用了com.android.internal.policy.impl.Policy#makeNewLayoutInflater(Context context)方法:
public LayoutInflater makeNewLayoutInflater(Context context) {
return new PhoneLayoutInflater(context);
}
也就是说android-sdk-19及以下的版本是创建一个PhoneLayoutInflater对象。
BridgeInflate和PhoneLayoutInflater都是继承自LayoutInflater,实现了解析xml布局的API,将会在后面分析xml布局文件解析过程时用上。这里不讨论两者的实现以及区别。
获取LayoutInflater对象:
按照上面的逻辑,LayoutInflater不需要我们自己new,framework层已经帮我们创建好,自然也会也会提供API供开发者获取LayoutInflater对象。
方式一:
既然LayoutInflater是在ContextImpl中注册的,Context也提供了接口来获取LayoutInflater服务,也就是Context#getSystemService(String name);方法:
@Override
public Object getSystemService(String name) {
ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name);
return fetcher == null ? null : fetcher.getService(this);
}
该方法从SYSTEM_SERVICE_MAP集合内取出对应服务的获取器ServiceFetcher,并调用其getService方法来获取服务,首次调用的时候,将会调用到ServiceFetcher类的createService方法来创建一个LayoutInflater对象,之后将会返回已经创建好的对象。
所有的其他获取LayoutInflater对象的方式,都将调用到Context#getSystemService(String name);方法,我们继续往下看看其他方式是如何获取的。
方式二:
通过LayoutInflater#from(context)方法来获取:
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;
}
最终该方式还是调用了方式一中说到的Context#getSystemService(String name);方法,并将LayoutInflater服务名称传递进去。
方式三:
如果在Activity内,可以通过Activity#getLayoutInflater();方法获取LayoutInflater,该方法是Activity封装的一个方法:
@NonNull
public LayoutInflater getLayoutInflater() {
return getWindow().getLayoutInflater();
}
Activity里的getWindow返回的是一个PhoneWindow对象,接着看PhoneWindow#getLayoutInflater();
@Override
public LayoutInflater getLayoutInflater() {
return mLayoutInflater;
}
返回了一个LayoutInflater对象,其初始化是在PhoneWindow的构造方法里:
public PhoneWindow(Context context) {
super(context);
mLayoutInflater = LayoutInflater.from(context);
}
其最终调用了方式二中的LayoutInflater#from(Context context);方法。
布局解析过程
接着,分析LayoutInflater是如何将一个xml布局文件解析成一个View对象的。涉及到以下内容:
- LayoutInflater#inflate(...);的四个重构方法
- LayoutInflater#inflate(...);是如何解析视图的
LayoutInflater#inflate(...);的四个重构方法
通过LayoutInflater对外提供的四个inflate重构方法来入手视图解析流程:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root);
public View inflate(XmlPullParser parser, @Nullable ViewGroup root);
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot);
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot);
调用关系如下:
- 第一个重构方法最后调用了第三个重构方法,第三个重构方法最后调用了第四个重构方法。
- 第二个重构方法最终调用了第四个重构方法
第一个:
public View inflate(int resource, ViewGroup root) {
// 调用第三个重构方法
return inflate(resource, root, root != null);
}
第二个:
public View inflate(XmlPullParser parser, ViewGroup root) {
// 调用第四个重构方法
return inflate(parser, root, root != null);
}
第三个:
public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
// 通过resource资源文件获取xml解析器
final XmlResourceParser parser = res.getLayout(resource);
try {
// 调用第四个重构方法
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
第四个:
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
// 省略内容,后面分析
}
真正开始布局的解析流程的是第四个重构方法,也就是说我们只要分析第四个重构方法的流程就能知道xml布局文件是如何被解析的。
LayoutInflater#inflate(...);是如何解析视图的
视图的解析过程可以总结成:
- 使用XmlPullParser遍历xml文件内的所有节点
- 在遍历到某一节点时,根据节点名字生成对应的View对象
- 在生成View对象时,将AttributeSet以及Context传递给View对象的构造方法,在构造方法中,View或者其子类将通过AttributeSet获取自身的属性列表,并用来初始化View。如background等属性。
在分析视图的解析过程之前,需要先了解什么是XmlPullParser,他是第二个和第四个重构方法的参数,XmlPullParser是一个接口,定义了一系列解析xml文件的API。
java中解析xml的常用方式有DOM和SAX两种方式,pull解析是android提供的一种。
这里引用一段对pull方式的描述:
在android系统中,很多资源文件中,很多都是xml格式,在android系统中解析这些xml的方式,是使用pul解析器进行解析的,它和sax解析一样(个人感觉要比sax简单点),也是采用事件驱动进行解析的,当pull解析器,开始解析之后,我们可以调用它的next()方法,来获取下一个解析事件(就是开始文档,结束文档,开始标签,结束标签),当处于某个元素时可以调用XmlPullParser的getAttributte()方法来获取属性的值,也可调用它的nextText()获取本节点的值。
对xml解析方式的使用有兴趣可以参阅:
android解析XML总结(SAX、Pull、Dom三种方式)
那么XmlPullParser对象是如何生成的。看看重构方法三:
final XmlResourceParser parser = res.getLayout(resource);
res是Resource类对象,resource是资源文件id,看看Resource#getLayout(int id);方法的实现:
public XmlResourceParser getLayout(int id) throws NotFoundException {
return loadXmlResourceParser(id, "layout");
}
Resource#loadXmlResourceParser(int id, String type);方法最终将会返回一个XmlBlock#Parser类型的对象:
final class XmlBlock {
// ...
final class Parser implements XmlResourceParser {
// ...
}
// ...
}
XmlResourceParser继承自XmlPullParser、AttributeSet以及AutoCloseable(一个定义了不使用时需要关闭的接口):
public interface XmlResourceParser extends XmlPullParser, AttributeSet, AutoCloseable {
public void close();
}
也就是说最终返回了一个XmlPullParser接口的实现类Parser,Parser类还实现了AttributeSet接口。
那么大家经常在View的构造方法里见到的AttributeSet到底什么:
Android引入了pull解析,其中XmlPullParser这个接口定义了操作pull解析方式对xml文件的所有操作接口,包括对节点的操作,对节点内的属性的操作,以及next等接口。而AttributeSet则是Android针对资源文件的特点定义的一个接口,该接口描述了对节点内的属性集的操作接口,除了getAttributeValue、getAttributeCount等一些和XmlPullParser接口相同的接口外。AttributeSet还定义了一些如getIdAttribute、getAttributeResourceValue、getAttributeBooleanValue这些pull解析方式之外的一些带有android特性的接口,相当于是对节点的属性集合的操作接口进行了拓展。
这样看来,XmlBlock#Parser类除了实现了pull解析方式自带的接口定义外。还实现了AttributeSet接口内定义的一些具有android特性的接口。
但是Parser内并未存储节点下所有的Attributes(属性)。这些属性都是存在android.content.res.TypedArray内,而如何得到TypedArray类型对象,继续往下看。
回到LayoutInflater#inflate的第四个重构方法,看看是如何使用parser这个xml解析器的。
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
// ...
// 因为parser实现了AttributeSet接口,所以这里是强转
final AttributeSet attrs = Xml.asAttributeSet(parser);
// result是需要return的值
View result = root;
try {
// 通过一个循环,寻找根节点
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 (TAG_MERGE.equals(name)) {
// 如果根节点是merge标签
if (root == null || !attachToRoot) {
// merge标签要求传入的ViewGroup不能是空,并且attachToRoot必须为true, 否则报错
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
// 递归生成根节点下的所有子节点
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// 根据节点的信息(名称、属性)生成根节点View对象
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
// 根节点的LayoutParams属性
ViewGroup.LayoutParams params = null;
if (root != null) {
// 如果传入的ViewGroup不为空
// 调用root的generateLayoutParams方法来生成根节点的LayoutParams属性对象
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// 不需要讲根节点添加到传入的ViewGroup节点下,则将LayoutParams对象设置到根节点内
// 否则的话在后面将会通过addView方式设置params
temp.setLayoutParams(params);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
// 开始解析所有子节点
}
// 解析根节点下的子节点
rInflateChildren(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
// 结束了所有子节点的解析
}
if (root != null && attachToRoot) {
// 如果传入的ViewGroup不是空,并且需要添加根节点到其下面
root.addView(temp, params);
}
if (root == null || !attachToRoot) {
// 如果根节点为空,或者是attachToRoot为false,返回根节点
result = temp;
}
}
} catch (XmlPullParserException e) {
// ....
} catch (Exception e) {
// ....
} finally {
// ....
}
// return 结果(根节点或者是传入的ViewGroup)
return result;
}
}
这里有几个比较关键的地方,一一进行分析:
// 根据节点的信息(名称、属性)生成根节点View对象
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
createViewFromTag方法创建了对应节点的View对象:
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
if (name.equals("view")) {
// 如果节点名字为view,则取节点下面的class属性作为名字
name = attrs.getAttributeValue(null, "class");
}
// 不使用默认Theme属性的这部分逻辑跳过不讲
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();
}
// 几点名称为blink的时候,创建一个BlinkLayout类对象,继承自FrameLayout。
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}
try {
View view;
// mFactory和mFactory2是两个工厂类,可以对视图的创建进行hook,暂时不分析
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
// 和mFactory类似,暂不分析
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
// 最终会走到这,
if (view == null) {
// View的构造方法参数:context
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
// 如果节点名字不带".",说明是系统提供的View(Button/TextView等),走系统View的创建流程,android.view包下的
view = onCreateView(parent, name, attrs);
} else {
// 否则则说明是自定义View,走自定义View的创建流程
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
// 返回解析出来的View
return view;
} catch (InflateException e) {
// ...
} catch (ClassNotFoundException e) {
// ...
} catch (Exception e) {
// ...
}
}
最终会调用LayoutInflater#createView方法来创建指定名字的View(调用onCreateView方法最后也会调用createView方法):
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
// sConstructorMap存储了所有解析过的View的构造方法Constructor
Constructor<? extends View> constructor = sConstructorMap.get(name);
// 待解析的View的Class
Class<? extends View> clazz = null;
try {
if (constructor == null) {
// 缓存中没有该类型的构造方法,也就是之前没有解析过该Class类型的View,
// 通过反射获取Constructor对象,并缓存
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
// Filter这个东西是用来拦截节点解析的,
// onLoadClass返回false的话,将会调用failNotAllowed,就是报错,不允许解析
if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
}
// 反射获取Constructor对象,并缓存
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
} else {
if (mFilter != null) {
// 如果有拦截器的话,需要通过缓存的拦截信息判断是否需要拦截解析,
// 如果未缓存拦截信息的话,则动态从mFilter#onLoadClass中取出拦截信息
Boolean allowedState = mFilterMap.get(name);
if (allowedState == null) {
// New class -- remember whether it is allowed
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
mFilterMap.put(name, allowed);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
} else if (allowedState.equals(Boolean.FALSE)) {
failNotAllowed(name, prefix, attrs);
}
}
}
Object[] args = mConstructorArgs;
// View的构造方法里第二个参数是AttributeSet,一个用来解析属性的对象
args[1] = attrs;
// View对象的真正创建
final View view = constructor.newInstance(args);
if (view instanceof ViewStub) {
// 如果是ViewStub的话,需要为其设置一个copy的LayoutInflater
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
// 返回结果
return view;
} catch (NoSuchMethodException e) {
// 这个报错比较重要
InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class "
+ (prefix != null ? (prefix + name) : name));
ie.initCause(e);
throw ie;
} catch (ClassCastException e) {
// ...
} catch (ClassNotFoundException e) {
// ...
} catch (Exception e) {
// ...
} finally {
// ...
}
}
LayoutInflater是通过反射的方式创建View,并将context以及AttributeSet对象作为参数传入。
也就是说如果用户自定义View的时候,没有重写带两个参数的构造方法的话,将会报错。代码将会走到上面NoSuchMethodException这个catch中。例如下面这个报错信息(注意注释部分):
FATAL EXCEPTION: main
Process: com.example.j_liuchaoqun.myapplication, PID: 26075
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.j_liuchaoqun.myapplication/com.example.j_liuchaoqun.myapplication.MainActivity}: android.view.InflateException: Binary XML file line #13: Binary XML file line #13: Error inflating class com.example.j_liuchaoqun.myapplication.SlideTextView
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2793)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2864)
at android.app.ActivityThread.-wrap12(ActivityThread.java)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1567)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:156)
at android.app.ActivityThread.main(ActivityThread.java:6524)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:941)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:831)
Caused by: android.view.InflateException: Binary XML file line #13: Binary XML file line #13: Error inflating class com.example.j_liuchaoqun.myapplication.SlideTextView
Caused by: android.view.InflateException: Binary XML file line #13: Error inflating class com.example.j_liuchaoqun.myapplication.SlideTextView
// 大家主要看下面这行信息,在createView(LayoutInflater.java:625)方法中反射时,提示缺少一个SlideTextView(Context context, AttributeSet set);的构造方法。
Caused by: java.lang.NoSuchMethodException: <init> [class android.content.Context, interface android.util.AttributeSet]
at java.lang.Class.getConstructor0(Class.java:2204)
at java.lang.Class.getConstructor(Class.java:1683)
at android.view.LayoutInflater.createView(LayoutInflater.java:625)
at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:798)
at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:738)
at android.view.LayoutInflater.rInflate(LayoutInflater.java:869)
at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:832)
at android.view.LayoutInflater.inflate(LayoutInflater.java:518)
at android.view.LayoutInflater.inflate(LayoutInflater.java:426)
at android.view.LayoutInflater.inflate(LayoutInflater.java:377)
at android.support.v7.app.AppCompatDelegateImplV7.setContentView(AppCompatDelegateImplV7.java:255)
at android.support.v7.app.AppCompatActivity.setContentView(AppCompatActivity.java:109)
at com.example.j_liuchaoqun.myapplication.MainActivity.onCreate(MainActivity.java:11)
at android.app.Activity.performCreate(Activity.java:6910)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1123)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2746)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2864)
at android.app.ActivityThread.-wrap12(ActivityThread.java)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1567)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:156)
at android.app.ActivityThread.main(ActivityThread.java:6524)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:941)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:831)
在API21中,将会调用到View的一个四个参数的构造方法,低版本API中可能只有三个构造方法,但不管如何,最后都会调用到参数最多的那个构造方法,并在该方法中对View进行初始化,而初始化的信息,都将通过AttributeSet生成的TypedArray对象来获取。
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
this(context);
// 解析styleable.View的所有属性
final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
// ...
// 遍历解析出来的所有属性,并设置为当前View对象
final int N = a.getIndexCount();
for (int i = 0; i < N; i++) {
int attr = a.getIndex(i);
switch (attr) {
case com.android.internal.R.styleable.View_background:
// 背景
background = a.getDrawable(attr);
break;
}
// ...其他case
default:
break;
}
}
// ...
}
这里对其构造方法进行了简化,可以看到,AttributeSet是在这里使用的,通过context.obtainStyledAttributes方法将attrs.xml下定义的View这个styable属性集解析出来,android源码中的attrs.xml文件中定义了View的所有属性:
<!-- Attributes that can be used with {@link android.view.View} or
any of its subclasses. Also see {@link #ViewGroup_Layout} for
attributes that are processed by the view's parent. -->
<declare-styleable name="View">
<!-- Supply an identifier name for this view, to later retrieve it
with {@link android.view.View#findViewById View.findViewById()} or
{@link android.app.Activity#findViewById Activity.findViewById()}.
This must be a
resource reference; typically you set this using the
<code>@+</code> syntax to create a new ID resources.
For example: <code>android:id="@+id/my_id"</code> which
allows you to later retrieve the view
with <code>findViewById(R.id.my_id)</code>. -->
<attr name="id" format="reference" />
<!-- Supply a tag for this view containing a String, to be retrieved
later with {@link android.view.View#getTag View.getTag()} or
searched for with {@link android.view.View#findViewWithTag
View.findViewWithTag()}. It is generally preferable to use
IDs (through the android:id attribute) instead of tags because
they are faster and allow for compile-time type checking. -->
<attr name="tag" format="string" />
<!-- The initial horizontal scroll offset, in pixels.-->
<attr name="scrollX" format="dimension" />
<!-- The initial vertical scroll offset, in pixels. -->
<attr name="scrollY" format="dimension" />
<!-- A drawable to use as the background. This can be either a reference
to a full drawable resource (such as a PNG image, 9-patch,
XML state list description, etc), or a solid color such as "#ff000000"
(black). -->
<attr name="background" format="reference|color" />
<!-- Sets the padding, in pixels, of all four edges. Padding is defined as
space between the edges of the view and the view's content. A views size
will include it's padding. If a {@link android.R.attr#background}
is provided, the padding will initially be set to that (0 if the
drawable does not have padding). Explicitly setting a padding value
will override the corresponding padding found in the background. -->
<attr name="padding" format="dimension" />
<!-- Sets the padding, in pixels, of the left edge; see {@link android.R.attr#padding}. -->
<attr name="paddingLeft" format="dimension" />
<!-- Sets the padding, in pixels, of the top edge; see {@link android.R.attr#padding}. -->
<attr name="paddingTop" format="dimension" />
<!-- Sets the padding, in pixels, of the right edge; see {@link android.R.attr#padding}. -->
<attr name="paddingRight" format="dimension" />
<!-- Sets the padding, in pixels, of the bottom edge; see {@link android.R.attr#padding}. -->
<attr name="paddingBottom" format="dimension" />
<!-- Sets the padding, in pixels, of the start edge; see {@link android.R.attr#padding}. -->
<attr name="paddingStart" format="dimension" />
<!-- Sets the padding, in pixels, of the end edge; see {@link android.R.attr#padding}. -->
<attr name="paddingEnd" format="dimension" />
<!-- 属性太多,不一一列举 -->
</declare-styleable>
当然,如果你是View的子类,也有对应的属性,比如ListView:
<declare-styleable name="ListView">
<!-- Reference to an array resource that will populate the ListView. For static content,
this is simpler than populating the ListView programmatically. -->
<attr name="entries" />
<!-- Drawable or color to draw between list items. -->
<attr name="divider" format="reference|color" />
<!-- Height of the divider. Will use the intrinsic height of the divider if this
is not specified. -->
<attr name="dividerHeight" format="dimension" />
<!-- When set to false, the ListView will not draw the divider after each header view.
The default value is true. -->
<attr name="headerDividersEnabled" format="boolean" />
<!-- When set to false, the ListView will not draw the divider before each footer view.
The default value is true. -->
<attr name="footerDividersEnabled" format="boolean" />
<!-- Drawable to draw above list content. -->
<attr name="overScrollHeader" format="reference|color" />
<!-- Drawable to draw below list content. -->
<attr name="overScrollFooter" format="reference|color" />
</declare-styleable>
对应在ListView的构造方法里有:
public ListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
// ...
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.ListView, defStyleAttr, defStyleRes);
// 从节点中获取Divider属性,如果有定义的话,设置到ListView中
final Drawable d = a.getDrawable(R.styleable.ListView_divider);
if (d != null) {
// Use an implicit divider height which may be explicitly
// overridden by android:dividerHeight further down.
setDivider(d);
}
// 其他ListView提供的属性...
}
至此,xml中根节点的解析过程告一段落。
那么LayoutInflater是如何解析xml下的其他子节点的? 回过头来看LayoutInflater#inflate第四个重构方法里有一段代码:
// 解析根节点下的子节点
rInflateChildren(parser, temp, attrs, true);
该方法将会遍历View的所有子节点,并调用createViewFromTag对每一个节点进行解析,并把解析出来的View添加到父节点中。具体内如如何实现,大家可以看看源码。与xml的根节点解析类似。
inflate方法的attachToRoot(Boolean)参数
attachToRoot是inflate接收的一个参数,它有两重作用:
- 表示是否需要将解析出来的xml根节点add到传入的root布局中(如果root不为空的话)。
- 如果attachToRoot为true,则inflate方法将返回root对象,否则,将返回解析出来的xml根节点View对象。
inflate方法的root(ViewGroup)参数
如果root不为空,将会调用root的generateLayoutParams方法为xml跟布局生成LayoutParams对象。generateLayoutParams是ViewGroup中定义的方法。它的子类可以对其进行重写,以返回对应类型的LayoutParams
FrameLayout#generateLayoutParams(android.util.AttributeSet):
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new FrameLayout.LayoutParams(getContext(), attrs);
}
RelativeLayout#generateLayoutParams(android.util.AttributeSet):
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new RelativeLayout.LayoutParams(getContext(), attrs);
}
可以发现,如果传入的root是FrameLayout类型的话,将会生成FrameLayout.LayoutParams,如果传入的root是RelativeLayout类型的话,将会生成RelativeLayout.LayoutParams。
根据这样的规律,分析下面两种情况:
- xml根节点定义了属性android:layout_centerHorizontal="true",而inflate方法传入的root对象为FrameLayout类型,此时android:layout_centerHorizontal将会失效,因为FrameLayout.LayoutParam对象并不支持layout_centerHorizontal属性。
- xml根节点定义了属性android:layout_gravity="center",而inflate方法传入的的root对象为RelativeLayout类型,此时android:layout_gravity也会失效,因为RelativeLayout.LayoutParams并不支持layout_gravity属性。
- 同理还需要考虑LinearLayout.LayoutParams所支持的属性与xml根节点定义的属性是否有冲突。
如果传入的root对象为空,xml根节点的所有的以“layout_”开头的属性都将失效,因为没有root对象来为根节点生成对应的LayoutParams对象。
针对该特性,如果传入的root为空,将出现类似如根节点定义的宽高失效,如我定义的根节点宽度为50dp,高度也为50dp,最后显示出来的效果却是一个wrap_content的效果。为什么会出现上述原因,是因为如果根节点没有LayoutParams对象,那么在它被add到某一个ViewGroup上的时候,将会自动生成一个宽高为wrap_content的LayoutParams对象:
ViewGroup#addView(android.view.View, int):
public void addView(View child, int index) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
LayoutParams params = child.getLayoutParams();
if (params == null) {
// 如果LayoutParams为空的话,生成默认的
params = generateDefaultLayoutParams();
if (params == null) {
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
}
}
addView(child, index, params);
}
ViewGroup#generateDefaultLayoutParams:
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
总结
- LayoutInflater是android用来解析xml布局文件的一个类
- LayoutInflater内部使用Pull解析的方式,并对其进行了一定的扩展。
- LayoutInflater在生成View节点的时候,是通过反射的方式创建View对象,
反射调用的构造方法是带两个参数的那个,所以在定义View的时候必须重写带两个参数的构造方法。 - LayoutInflater在创建View对象的时候,会将xml节点的解析器AttributeSet传入到View的构造方法中。AttributeSet定义了用来解析xml节点属性的API。View通过AttributeSet生成TypedArray,并从中读取View节点中定义的属性。
- 最后LayoutInflater将会通过递归的方式创建xml根节点下的所有孩子节点。
- LayoutInflater#inflate方法接收一个root对象以及一个Boolean类型的attachToRoot变量。这两个参数的值,直接影响了inflate方法的返回值,以及生成的xml根节点的LayoutParams和属性。