LayoutInflater机制原理

通过实例引出问题

在开始之前我们先来做一个测试,我们平时最常见的就是ListView的Adapter中使用LayoutInflater加载xml的item布局文件,所以咱们就以ListView为例,如下:

省略掉Activity代码等,首先给出Activity的布局文件,如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ListView
        android:id="@+id/listview"
        android:dividerHeight="5dp"
        android:layout_width="match_parent"
        android:layout_height="match_parent"></ListView>

</LinearLayout>

给出两种不同的ListView的item布局文件。

textview_layout.xml文件:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="100dp"
    android:layout_height="40dp"
    android:text="Text Test"
    android:background="#ffa0a00c"/>

textview_layout_parent.xml文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    android:layout_height="wrap_content"
    android:layout_width="wrap_content"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <TextView xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="100dp"
        android:layout_height="40dp"
        android:text="Text Test"
        android:background="#ffa0a00c"/>

</LinearLayout>

ListView的自定义Adapter文件:

public class InflateAdapter extends BaseAdapter {
    private LayoutInflater mInflater = null;

    public InflateAdapter(Context context) {
        mInflater = LayoutInflater.from(context);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        //说明:这里是测试inflate方法参数代码,不再考虑性能优化等TAG处理
        return getXmlToView(convertView, position, parent);
    }

    private View getXmlToView(View convertView, int position, ViewGroup parent) {
        View[] viewList = {
                mInflater.inflate(R.layout.textview_layout, null),
//                mInflater.inflate(R.layout.textview_layout, parent),
                mInflater.inflate(R.layout.textview_layout, parent, false),
//                mInflater.inflate(R.layout.textview_layout, parent, true),
                mInflater.inflate(R.layout.textview_layout, null, true),
                mInflater.inflate(R.layout.textview_layout, null, false),

                mInflater.inflate(R.layout.textview_layout_parent, null),
//                mInflater.inflate(R.layout.textview_layout_parent, parent),
                mInflater.inflate(R.layout.textview_layout_parent, parent, false),
//                mInflater.inflate(R.layout.textview_layout_parent, parent, true),
                mInflater.inflate(R.layout.textview_layout_parent, null, true),
                mInflater.inflate(R.layout.textview_layout_parent, null, false),
        };

        convertView = viewList[position];

        return convertView;
    }
}

当前代码运行结果:


layoutinflater_listview_show_result.png

PS:当打开上面viewList数组中任意一行注释都会抛出异常(java.lang.UnsupportedOperationException: addView(View, LayoutParams) is not supported in AdapterView)。

从LayoutInflater源码实例化说起

我们先看一下源码中LayoutInflater实例化获取的方法:

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实例时不也就两种写法吗,如下:

LayoutInflater lif = LayoutInflater.from(Context context);

LayoutInflater lif = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

可以看见from方法仅仅是对getSystemService的一个安全封装而已。

LayoutInflater源码的View inflate(…)方法族剖析

得到LayoutInflater对象之后我们就是传递xml然后解析得到View,如下方法:

public View inflate(int resource, ViewGroup root) {
        return inflate(resource, root, root != null);
    }

继续看inflate(int resource, ViewGroup root, boolean attachToRoot)方法,如下:

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) + ")");
      }

      final XmlResourceParser parser = res.getLayout(resource);
      try {
          return inflate(parser, root, attachToRoot);
      } finally {
          parser.close();
      }
  }

会发现无论哪个inflate重载方法最后都调运了inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)方法;

public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
      synchronized (mConstructorArgs) {
          Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

          final AttributeSet attrs = Xml.asAttributeSet(parser);
          Context lastContext = (Context)mConstructorArgs[0];
          mConstructorArgs[0] = mContext;
          //定义返回值,初始化为传入的形参root
          View result = root;

          try {
              // Look for the root node.
              int type;
              while ((type = parser.next()) != XmlPullParser.START_TAG &&
                      type != XmlPullParser.END_DOCUMENT) {
                  // Empty
              }
              //如果一开始就是END_DOCUMENT,那说明xml文件有问题
              if (type != XmlPullParser.START_TAG) {
                  throw new InflateException(parser.getPositionDescription()
                          + ": No start tag found!");
              }
              //有了上面判断说明这里type一定是START_TAG,也就是xml文件里的root node
              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)) {
              //处理merge tag的情况(merge,你懂的,APP的xml性能优化)
                  //root必须非空且attachToRoot为true,否则抛异常结束(APP使用merge时要注意的地方,
                  //因为merge的xml并不代表某个具体的view,只是将它包起来的其他xml的内容加到某个上层
                  //ViewGroup中。)
                  if (root == null || !attachToRoot) {
                      throw new InflateException("<merge /> can be used only with a valid "
                              + "ViewGroup root and attachToRoot=true");
                  }
                  //递归inflate方法调运
                  rInflate(parser, root, attrs, false, false);
              } else {
                  // Temp is the root view that was found in the xml
                  //xml文件中的root view,根据tag节点创建view对象
                  final View temp = createViewFromTag(root, name, attrs, false);

                  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
                      //根据root生成合适的LayoutParams实例
                      params = root.generateLayoutParams(attrs);
                      if (!attachToRoot) {
                          // Set the layout params for temp if we are not
                          // attaching. (If we are, we use addView, below)
                          //如果attachToRoot=false就调用view的setLayoutParams方法
                          temp.setLayoutParams(params);
                      }
                  }

                  if (DEBUG) {
                      System.out.println("-----> start inflating children");
                  }
                  // Inflate all children under temp
                  //递归inflate剩下的children
                  rInflate(parser, temp, attrs, true, 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非空且attachToRoot=true则将xml文件的root view加到形参提供的root里
                      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) {
                      //返回xml里解析的root view
                      result = temp;
                  }
              }

          } catch (XmlPullParserException e) {
              InflateException ex = new InflateException(e.getMessage());
              ex.initCause(e);
              throw ex;
          } catch (IOException e) {
              InflateException ex = new InflateException(
                      parser.getPositionDescription()
                      + ": " + e.getMessage());
              ex.initCause(e);
              throw ex;
          } finally {
              // Don't retain static reference on context.
              mConstructorArgs[0] = lastContext;
              mConstructorArgs[1] = null;
          }

          Trace.traceEnd(Trace.TRACE_TAG_VIEW);
          //返回参数root或xml文件里的root view
          return result;
      }
  }

从上面的源码分析我们可以看出inflate方法的参数含义:

  • inflate(xmlId, null); 只创建temp的View,然后直接返回temp。
  • inflate(xmlId, parent); 创建temp的View,然后执行root.addView(temp, params);最后返回root。
  • inflate(xmlId, parent, true); 创建temp的View,然后执行root.addView(temp, params);最后返回root。
  • inflate(xmlId, parent, false); 创建temp的View,然后执行temp.setLayoutParams(params);然后再返回temp。
  • inflate(xmlId, null, false); 只创建temp的View,然后直接返回temp。
  • inflate(xmlId, null, true); 只创建temp的View,然后直接返回temp。

在此先强调一个Android的概念

我们经常使用View的layout_width和layout_height来设置View的大小,而且一般都可以正常工作,所以有人时常认为这两个属性就是设置View的真实大小一样;然而实际上这些属性是用于设置View在ViewGroup布局中的大小的;这就是为什么Google的工程师在变量命名上将这种属性叫作layout_width和layout_height,而不是width和height的原因了。

到此其实已经可以说明我们上面示例部分执行效果差异的原因了

详细解释如下:

  • mInflater.inflate(R.layout.textview_layout, null)不能正确处理我们设置的宽和高是因为layout_width,layout_height是相对了父级设置的,而此temp的getLayoutParams为null。
  • mInflater.inflate(R.layout.textview_layout, parent)能正确显示我们设置的宽高是因为我们的View在设置setLayoutParams时params = root.generateLayoutParams(attrs)不为空。
    Inflate(resId , parent,false ) 可以正确处理,因为temp.setLayoutParams(params);这个params正是root.generateLayoutParams(attrs);得到的。
  • mInflater.inflate(R.layout.textview_layout, null, true)与mInflater.inflate(R.layout.textview_layout, null, false)不能正确处理我们设置的宽和高是因为layout_width,layout_height是相对了父级设置的,而此temp的getLayoutParams为null。
  • textview_layout_parent.xml作为item可以正确显示的原因是因为TextView具备上级ViewGroup,上级ViewGroup的layout_width,layout_height会失效,当前的TextView会有效而已。

上面例子中说放开那些注释运行会报错java.lang.UnsupportedOperationException:
addView(View, LayoutParams) is not supported

  • 因为AdapterView源码中调用了root.addView(temp, params);而此时的root是我们的ListView,ListView为AdapterView的子类,所以我们看下AdapterView抽象类中addView源码即可明白为啥了,如下:
/**
    * This method is not supported and throws an UnsupportedOperationException when called.
    *
    * @param child Ignored.
    *
    * @throws UnsupportedOperationException Every time this method is invoked.
    */
   @Override
   public void addView(View child) {
       throw new UnsupportedOperationException("addView(View) is not supported in AdapterView");
   }

我们在写App时Activity中指定布局文件的时候,xml布局文件或者我们用java编写的View最外层的那个布局是可以指定大小的啊?他们最外层的layout_width和layout_height都是有作用的啊?

是这样的,还记得我们上面的分析吗?我们自己的xml布局通过setContentView()方法放置到哪去了呢?记不记得id为content的FrameLayout呢?所以我们xml或者java的View的最外层布局的layout_width和layout_height属性才会有效果,就是这么回事而已。

总结

  • View的layout_width和layout_height来设置View在ViewGroup布局中的大小的;这就是为什么Google的工程师在变量命名上将这种属性叫作layout_width和layout_height,而不是width和height的原因了。

  • LayoutInflater lif = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 这是最底层的获取方法

  • inflate重载方法最后都调运了inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)方法

  • inflate 方法参数含义

  • inflate(xmlId, null); 只创建temp的View,然后直接返回temp。

  • inflate(xmlId, parent); 创建temp的View,然后执行root.addView(temp, params);最后返回root。

  • inflate(xmlId, parent, true); 创建temp的View,然后执行root.addView(temp, params);最后返回root。

  • inflate(xmlId, parent, false); 创建temp的View,然后执行temp.setLayoutParams(params);然后再返回temp。

  • inflate(xmlId, null, false); 只创建temp的View,然后直接返回temp。

  • inflate(xmlId, null, true); 只创建temp的View,然后直接返回temp。

  • mInflater.inflate(R.layout.textview_layout, parent) 程序奔溃,因为root是我们的ListView,ListView为AdapterView的子类,AdapterView的addView抛出了异常,不支持addView;

  • activity的布局支持设置大小因为最外面有个content的framelayout

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,076评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,658评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,732评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,493评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,591评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,598评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,601评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,348评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,797评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,114评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,278评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,953评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,585评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,202评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,180评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,139评论 2 352

推荐阅读更多精彩内容