Android 老生常谈LayoutInflater的新认知

  现在看我文章的多数是一些老Android了,相信每个人使用起LayoutInflater都是家常便饭,信手拈来。

  但即使是这样,我仍然觉得这个知识点有可以分析的地方,看完之后或许你对LayoutInflater又会有一些新的认识。

  首先概括一下LayoutInflater是用来做什么的。

  我们都知道,在开发Android应用程序的时候,编写布局基本都是通过xml文件来编写的。当然你也完全可以在代码中纯手写布局,但是写过的人都清楚,这样编写布局会非常麻烦。

  那么通过xml编写的布局文件是如何转换成Android中的一个View对象从而显示在应用程序当中的呢?这就是LayoutInflater的作用了。

  简单来说,LayoutInflater的工作就是将使用xml文件编写的布局转换成Android里的View对象,并且这也是Android中将xml布局转换成View的唯一方式。

  可能有些朋友会说,不对啊,我平时也没怎么用过LayoutInflater,xml布局转换成View不是调用Activity里的setContentView()方法就可以了吗?

  这是因为Android SDK在上层给我们做了一些很好的封装,让开发工作变得更加简单。如果你打开setContentView()方法的源码去了解一下,就会发现它的底层同样也是使用的LayoutInflater:

  @Override

  public void setContentView(int resId) {

  ensureSubDecor();

  ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);

  contentParent.removeAllViews();

  LayoutInflater.from(mContext).inflate(resId, contentParent);

  mAppCompatWindowCallback.getWrapped().onContentChanged();

  }

  那么LayoutInflater又是如何将一个xml布局转换成一个View对象的呢?

  这当然是一个非常复杂的过程,但是如果简要概括的话,最重要的无非就是两步:

  这里我不想在文章中带着大家一步步追源码,这样文章看起来可能会又累又枯燥,因此我就只贴出一些我认为比较关键的代码。

  解析xml文件内容的代码片段:

  public View inflate(@LayoutRes int resource,

  @Nullable ViewGroup root,

  boolean attachToRoot) {

  ...

  XmlResourceParser parser = res.getLayout(resource);

  try {

  return inflate(parser, root, attachToRoot);

  } finally {

  parser.close();

  }

  }

  可以看到,这里获取到了一个XmlResourceParser对象,用于对xml文件进行解析。由于具体的解析规则过于复杂,我们就不跟进去看了。

  使用反射创建View对象的代码片段:

  public final View createView(@NonNull Context viewContext, @NonNull String name,

  @Nullable String prefix, @Nullable AttributeSet attrs)

  throws ClassNotFoundException, InflateException {

  ...

  if (constructor == null) {

  // Class not found in the cache, see if it's real, and try to add it

  clazz = Class.forName(prefix != null ? (prefix + name) : name, false,

  mContext.getClassLoader()).asSubclass(View.class);

  constructor = clazz.getConstructor(mConstructorSignature);

  constructor.setAccessible(true);

  sConstructorMap.put(name, constructor);

  }

  ...

  try {

  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]));

  }

  return view;

  }

  ...

  }

  看到这里,我们就将LayoutInflater大体的工作原理基本了解了。

  但是正如前面所说,本篇文章并不是要带着大家去读源码的,而是想要从用法层面对LayoutInflater有些新的理解。

  那么LayoutInflater最常见的用法如下:

  View view = LayoutInflater.from(context).inflate(resourceId, parent, false);

  这段代码的意思是,首先调用LayoutInflater的from()方法去获取一个LayoutInflater的实例,然后再调用它的inflate()方法去解析并加载一个布局,从而转换成一个View对象并返回。

  然而我认为这段代码对于新手来说却及其不友好,甚至对于很多的老手来说也是。

  我们来看一下inflate()方法的参数定义:

  public View inflate(int resource,

  @Nullable ViewGroup root,

  boolean attachToRoot) {

  ...

  }

  inflate()方法接收3个参数,第一个参数resource还比较好理解,就是我们要解析加载的xml文件的资源id。第二个参数root,和第三个参数attachToRoot是什么意思?可能即使不少做过多年Android开发的程序员也未必能解释得清楚。

  而这段代码在我们使用RecyclerView,或者使用Fragment时都是一定会用到的。我在写《第一行代码》时由于在很早的章节就要讲RecyclerView的用法,但是却又感觉很难向初学者解释清楚LayoutInflater的相关内容,所以我一直都觉得这块内容没有讲好。只能先用死记硬背的方式,暂时就记着这部分代码必须这么写。

  而今天,我希望能将LayoutInflater真正讲讲清楚。

  我们知道,Android的布局结构是一种树状结构。每个布局都可以包含若干个子布局,每个子布局又可以继续包含子布局,以此构建出任意样式的View呈现给用户。

  因此,我们大致可以明白,每个布局它都是要有一个父布局的。

  这也是inflate()方法第二个参数root的作用,就是给当前要解析加载的xml布局指定一个父布局。

  那么一个布局可不可以没有父布局呢?当然也是可以的,这也是为什么root参数被标为@Nullable的原因。

  但是如果我们inflate出来了一个没有父布局的布局,又该如何去展示它呢?那自然是没有办法去展示的,所以只能后面再用addView的方式将它添加到某个现有的布局下面。又或者你inflate出来的布局就是个顶层布局,所以它不需要有父布局。但是这些场景都比较少见,因此大多数情况下,我们在使用LayoutInflater的inflate()方法时都是要指定父布局的。

  另外,如果不为inflate出来的布局指定父布局,还会出现另外一种问题,我们通过一个例子来讲解一下。

  这里我们定义一个button_layout.xml布局文件,代码如下所示:

  <?xml version="1.0" encoding="utf-8"?>