浅谈 LayoutInflater

版权声明:本文为博主原创文章,未经博主允许不得转载。

微博:厉圣杰

源码:AndroidDemo/View

文中如有纰漏,欢迎大家留言指出。

在 Android 的开发中,想必大家都用过 LayoutInflater 吧。恩,就是平时自定义控件经常会用到的。啊,你连自定义控件都没有用到过?不要紧,那 Activity 中的 setContentView() 肯定用到过吧。通过查看 Android 源码,你会发现,其实 setContentView() 方法最终也是调用 LayoutInflater 类。说到这,大家想必应该能猜到 LayoutInflater 这个类是干嘛的了吧?

LayoutInflater 类就是用来加载布局的,它的用法很简单,我们可以通过两种方式来获取 LayoutInflater 的对象。第一种方式是我们平时自定义 View 的时候会经常使用的,写法如下:

LayoutInflater inflater = LayoutInflater.from(context);

第二种方式就是调用系统服务,写法如下:

LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.Layout_INFLATER_SERVICE);

通过查看 LayoutInflater.from(Context context) 方法的源码,你会发现其实它还是通过调用系统服务来获取 LayoutInflater 对象。

/**
* Obtains the LayoutInflater from the given 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;
}

获取 LayoutInflater 对象之后,我们就可以通过调用 inflate() 方法来加载布局,inflate() 有四种重载形式,源码如下,先重点看方法名和参数就好,后面还会回过来讲:

//方法一
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
//当root==null时,attachToRoot=false
return inflate(resource, root, root != null);
} //方法二
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();
}
} //方法三
public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {
return inflate(parser, root, root != null);
} //方法四
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; 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("**************************");
} 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 {
// 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");
} // 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);
} return result;
}
}

通常,我们使用前两种方式来加载布局,也就是靠传递布局的 ID 来实现加载,而不是使用后两种方式通过 XmlPullParser 来实现布局加载。而通过查看上述源码,不难发现前面三种方法最终都是调用第四种方式来加载布局,所有布局加载的主要逻辑也集中在第四个方法中。通过第四个方法的参数,我们可以知道 LayoutInflater 是通过 Pull 方式来解析布局文件的

通过以上简单的阐述,我们来分析一下 inflate() 前两种方法的使用。

有时候,学以致用这是一个让人纠结的命题,明明是先学后用,但有时候先用后学会比先学后用起到更好的效果。我想这也是现在某些速成培训班流行的结果,这里并不是讽刺,有时候实践的确比原理重要,就像很多理论都是先发现现象再去探究原因。

下面,我们就通过几个简单的例子来透过现象看本质。

首先,我们来创建需要被 LayoutInflater 载入的布局 layout_single_button.xml:

<?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="我是一个按钮"> </Button>

这真是一个简单的不能再简单的布局,只有一个 Button 控件。

MainActivity 的 布局文件 activity_main.xml 如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_layout_inflater"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.littlejie.view.LayoutInflaterActivity"> <Button
android:id="@+id/btn_root_is_not_null"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="root!=null" /> <Button
android:id="@+id/btn_root_is_null"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:width="100dp"
android:height="100dp"
android:text="root=null" /> <!-- 容器,用于直观的在Activity上动态演示添加操作-->
<LinearLayout
android:id="@+id/ll_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"></LinearLayout> </LinearLayout>

现在我们用 LayoutInflater 将 layout_single_button.xml 中的按钮添加到 activity_main.xml 中的 ll_container 中, MainActivity 的代码如下:

public class LayoutInflaterActivity extends Activity implements View.OnClickListener {

    private static final String TAG = LayoutInflaterActivity.class.getSimpleName();

    private LinearLayout mLlContainer;
private LayoutInflater mInflater; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_layout_inflater); findViewById(R.id.btn_root_is_not_null).setOnClickListener(this);
findViewById(R.id.btn_root_is_null).setOnClickListener(this); mLlContainer = (LinearLayout) findViewById(R.id.ll_container);
//获取LayoutInflater实例
//也可以通过 mInflater = (LayoutInflater) this.getSystemService(Context.Layout_INFLATER_SERVICE)来获取
mInflater = LayoutInflater.from(this);
} @Override
public void onClick(View v) {
//执行点击事件前,先把mLlContainer中的view移除
mLlContainer.removeAllViews();
switch (v.getId()) {
case R.id.btn_root_is_not_null:
inflateWithRoot();
break;
case R.id.btn_root_is_null:
inflateWithoutRoot();
break;
}
} /**
* 使用 inflate(int resource,ViewGroup root) 载入布局
* root != null
*/
private void inflateWithRoot() {
View inflateView = mInflater.inflate(R.layout.layout_single_button, mLlContainer);
Log.d(TAG, "method = inflateWithRoot,inflaterView type = " + inflateView.getClass().getSimpleName());
//此处必须注释掉,否则会抛出如下crash
//java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first
//mLlContainer.addView(inflateView);
} /**
* 使用 inflate(int resource,ViewGroup root) 载入布局
* root != null
*/
private void inflateWithoutRoot() {
View inflateView = mInflater.inflate(R.layout.layout_single_button, null);
Log.d(TAG, "method = inflateWithoutRoot,inflaterView type = " + inflateView.getClass().getSimpleName());
//此处注释必须要去掉,否则看不到载入的button
//mLlContainer.addView(inflateView);
}
}

运行程序,分别点击两个按钮,输出日志如下:

10-22 21:08:57.680 28603-28603/com.littlejie.view D/LayoutInflaterActivity: method = inflateWithRoot,inflaterView type = LinearLayout
10-22 21:08:58.990 28603-28603/com.littlejie.view D/LayoutInflaterActivity: method = inflateWithoutRoot,inflaterView type = Button

你会发现执行 inflateWithRoot() 方法会在屏幕上显示 Button,而执行 inflateWithoutRoot() 方法并没有显示 Button。

现在我们去掉注释,再重新执行一遍,咦,执行 inflateWithRoot() 方法竟然崩溃了,而执行 inflateWithoutRoot() 方法的却在屏幕上显示 Button。现象在注释里都写,那你知道为什么嘛?还有,执行点击事件的日志,View Type 为什么一个是 Button,而另一个确实 LinearLayout,我们明明载入的是 Button 啊~

带着这些疑问,我们去看 Android 源码,前面有提到, inflate() 方法的最终都是调用第四种重载形式来实现载入布局,所以我们重点要看方法四的代码。你可能已经注意到方法四有一个 attachToRoot 参数,那么方法一是怎么获取这个参数的呢?查看源码,可以很容易的知道:

attachToRoot = root != null

关于 root 和 attachToRoot 的组合效果会在后面给出结论。

我们接着来看源码:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
//先把root赋给result
View result = root; try {
//Todo:解析XML final String name = parser.getName(); 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 {
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs); //Todo // Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true); // 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 () {
//Todo
} finally {
//Todo
}
return result;
}
}

可以发现 LayoutInflater 调用 createViewFromTag() 方法来创建布局的 Root View ,然后通过调用 rInflateChildren() 方法来循环遍历生成 子View 。(rInflateChildren() 最终是调用 rInflate() 方法来生成 View,有兴趣的可以去看下源码)接下去的两个 if语句 是我们此次分析的重点

对于 inflate(int resouce,ViewGroup root) 来说,两个 if语句 是互斥的。那么上述问题就很好解释了:

  • inflateWithRoot() 方法因为 root != null 导致只会执行第一个 if语句,因此会将布局layout_single_button.xml 中的 Button 添加到 mLlContainer 中,这也就解释了为什么返回的 View 是 LinearLayout 类型,而执行 mLlContainer.addView() 会抛出 java.lang.IllegalStateException
  • inflateWithoutRoot() 方法因为 root == null 导致只执行第二个 if语句,所以返回的 View 是 Button 类型,并且没有 父View ,所以如果不执行 mLlContainer.addView() ,就不会显示在屏幕上。

看到这里,相比你已经知道 root 和 attachToRoot 的作用了吧。原谅我偷懒,以下内容摘自郭霖博客

  1. 如果root为null,attachToRoot将失去作用,设置任何值都没有意义。
  2. 如果root不为null,attachToRoot设为true,则会给加载的布局文件的指定一个父布局,即root。
  3. 如果root不为null,attachToRoot设为false,则会将布局文件最外层的所有layout属性进行设置,当该view被添加到父view当中时,这些layout属性会自动生效。
  4. 在不设置attachToRoot参数的情况下,如果root不为null,attachToRoot参数默认为true。

补充

看见补充部分,想必你肯定认真的看完了前面的内容,这里咱们来谈谈一些刚刚没细谈的。

  • 开头说到的关于 setContentView() 其实也是调用 LayoutInflater 类来实现布局加载的事情,可能你通过 Android Studio 查看源码,发现最终调用的 Window 类 setContentView()方法:
/**
* Abstract base class for a top-level window look and behavior policy. An
* instance of this class should be used as the top-level view added to the
* window manager. It provides standard UI policies such as a background, title
* area, default key processing, etc.
*
* <p>The only existing implementation of this abstract class is
* android.view.PhoneWindow, which you should instantiate when needing a
* Window.
*/
public abstract class Window {
//...
public abstract void setContentView(@LayoutRes int layoutResID);
//...
}

咦,竟然是抽象类的一个抽象方法,恩,那肯定是在子类中实现的。赶紧看下有哪个类是继承 Window 的。用 Android Studio 一看,我勒个去,这货没子类?这是什么鬼?

等等,再看一眼类注释,好像有说 Window 有个实现类 android.view.PhoneWindow (位于包:package com.android.internal.policy;)。通常,你可以在以下目录中找到 PhoneWindow 类:

android-sdk-path/sources/android-24/com/android/internal/policy

如果你下载 AOSP 源码,你可以在以下目录找到 PhoneWindow :

android-source-code/frameworks/base/core/java/com/android/internal/policy

setContentView() 方法源码摘录如下:

@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) {
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;
}

可以看见一行代码 mLayoutInflater.inflate(layoutResID, mContentParent);,所以 setContentView() 方法也是通过 LayoutInflater 来实现布局加载,只不过 Android 为了方便我们使用对此进行了封装而已。

  • 你可能注意到 inflate() 方法中有这么一个判断:
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);
}

所以在使用 标签的时候要一定要设置 root,否则会抛出异常。

总结

尝试自己阅读源码解释清楚某个问题现象,不过逻辑还是有点不清晰,语言组织的也不是很好,再接再厉~

不积跬步无以至千里

不积小流无以成江海

浅谈 LayoutInflater的更多相关文章

  1. 浅谈 Fragment 生命周期

    版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 源码:AndroidDemo/Fragment 文中如有纰漏,欢迎大家留言指出. Fragment 是在 Android 3.0 中 ...

  2. 浅谈RecyclerView(完美替代ListView,GridView)

    Android RecyclerView 是Android5.0推出来的,导入support-v7包即可使用. 个人体验来说,RecyclerView绝对是一款功能强大的控件. 首先总结下Recycl ...

  3. 浅谈TabLayout(ViewPager+Tab联动)

    google发布了的Android Support Design库中提供了TabLayout 通过TabLayout+ViewPager实现导航栏效果,点击Tab ,ViewPager跟随变化,滑动V ...

  4. Android性能优化的浅谈

    一.概要: 本文主要以Android的渲染机制.UI优化.多线程的处理.缓存处理.电量优化以及代码规范等几方面来简述Android的性能优化 二.渲染机制的优化: 大多数用户感知到的卡顿等性能问题的最 ...

  5. 安卓开发_浅谈ListView(自定义适配器)

    ListView作为一个实际开发中使用率非常高的视图,一般的系统自带的适配器都无法满足开发中的需求,这时候就需要开发人员来自定义适配器使得ListView能够有一个不错的显示效果 有这样一个Demo ...

  6. 浅谈Java的throw与throws

    转载:http://blog.csdn.net/luoweifu/article/details/10721543 我进行了一些加工,不是本人原创但比原博主要更完善~ 浅谈Java异常 以前虽然知道一 ...

  7. 浅谈SQL注入风险 - 一个Login拿下Server

    前两天,带着学生们学习了简单的ASP.NET MVC,通过ADO.NET方式连接数据库,实现增删改查. 可能有一部分学生提前预习过,在我写登录SQL的时候,他们鄙视我说:“老师你这SQL有注入,随便都 ...

  8. 浅谈WebService的版本兼容性设计

    在现在大型的项目或者软件开发中,一般都会有很多种终端, PC端比如Winform.WebForm,移动端,比如各种Native客户端(iOS, Android, WP),Html5等,我们要满足以上所 ...

  9. 浅谈angular2+ionic2

    浅谈angular2+ionic2   前言: 不要用angular的语法去写angular2,有人说二者就像Java和JavaScript的区别.   1. 项目所用:angular2+ionic2 ...

随机推荐

  1. shell注释

    sh里没有多行注释,只能每一行加一个#号.只能像这样: #-------------------------------------------- # 这是一个自动打ipa的脚本,基于webfrogs ...

  2. 学习AOP之透过Spring的Ioc理解Advisor

    花了几天时间来学习Spring,突然明白一个问题,就是看书不能让人理解Spring,一方面要结合使用场景,另一方面要阅读源代码,这种方式理解起来事半功倍.那看书有什么用呢?主要还是扩展视野,毕竟书是别 ...

  3. 前端框架 EasyUI (0) 重新温习(序言)

    几年前,参与过一个项目.那算是一个小型的信息管理系统,BS 结构的,前端用的是基于 jQuery 的 EasyUI 框架. 我进 Team 的时候,项目已经进入开发阶段半个多月了.听说整个项目的框架是 ...

  4. 恋爱虽易,相处不易:当EntityFramework爱上AutoMapper

    剧情开始 为何相爱? 相处的问题? 女人的伟大? 剧情收尾? 有时候相识即是一种缘分,相爱也不需要太多的理由,一个眼神足矣,当EntityFramework遇上AutoMapper,就是如此,恋爱虽易 ...

  5. 让姑姑不再划拳 码农也要有原则 : SOLID via C#

    “姑娘,别这样.我们是有原则的.” “一个有原则的程序猿是不会写出 “摧毁地球” 这样的程序的,他们会写一个函数叫 “摧毁行星”而把地球当一个参数传进去.” “对,是时候和那些只会滚键盘的麻瓜不同了, ...

  6. nodejs进阶(5)—接收请求参数

    1. get请求参数接收 我们简单举一个需要接收参数的例子 如果有个查找功能,查找关键词需要从url里接收,http://localhost:8000/search?keyword=地球.通过前面的进 ...

  7. mysql进阶之存储过程

    往往看别人的代码会有这样的感慨: 看不懂 理还乱 是离愁 别是一番滋味在心头 为什么要使用存储过程? 在mysql开发中使用存储过程的理由: 当希望在不同的应用程序或平台上执行相同的函数,或者封装特定 ...

  8. Openfiler配置RAC共享存储

    将 Openfiler 用作 iSCSI 存储服务器,主要操作步骤如下: 1.设置 iSCSI 服务 2.配置网络访问 3.指定物理存储器并对其分区 4.创建新的卷组 5.创建所有逻辑卷 6.为每个逻 ...

  9. Hadoop学习之旅二:HDFS

    本文基于Hadoop1.X 概述 分布式文件系统主要用来解决如下几个问题: 读写大文件 加速运算 对于某些体积巨大的文件,比如其大小超过了计算机文件系统所能存放的最大限制或者是其大小甚至超过了计算机整 ...

  10. win7下利用ftp实现华为路由器的上传和下载

    win7下利用ftp实现华为路由器的上传和下载 1.  Win7下ftp的安装和配置 (1)开始->控制面板->程序->程序和功能->打开或关闭Windows功能 (2)在Wi ...