和setContentView一样,LayoutInflater也算老朋友了,在分析setContentView中也多次看到了LayoutInflater。

LayoutInflater主要是用来加载布局,将id资源转化为view对象,我们在Activity中加载布局用到的是setContentView(),其实setContentView内部也是

使用LayoutInflater来加载布局的。下面我们就来解开这个神秘人物的面纱

1 用法

LayoutInflater layoutInflater = LayoutInflater.from(context); 
layoutInflater.inflate(resourceId, root);

或者

LayoutInflater layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
  layoutInflater.inflate(resourceId, root);

可以得到layoutInflater ,其实LayoutInflater.from里面调用的就是context.getSystemService

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;
    }

inflate()方法一般接收两个参数,第一个参数就是要加载的布局id,第二个参数是指给该布局的外部再嵌套一层父布局,如果不需要就直接传null


举个完整的栗子:

public class MainActivity extends Activity {  
  
    private LinearLayout mainLayout;  
  
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
        mainLayout = (LinearLayout) findViewById(R.id.main_layout);  
        LayoutInflater layoutInflater = LayoutInflater.from(this);  
        View buttonLayout = layoutInflater.inflate(R.layout.button_layout, null);  
        mainLayout.addView(buttonLayout);  
    }  
  
}

main_layout是一个普通的线性布局,button_layout就一个Button布局,整个过程就是将button_layout通过layoutInflater转化成View,然后加入到mainLayout




2源码分析

我们从LayoutInflater.from(context)开始出发

(1)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;
    }

然后接着是inflate


public View inflate(int resource, ViewGroup root) {
        return inflate(resource, root, root != null);
    }
public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
        if (DEBUG) System.out.println("INFLATING from resource: " + resource);
        XmlResourceParser parser = getContext().getResources().getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

这里涉及到XML解析方式,在XML解析篇会详谈,总之这里通过资源id得到了相应的解析器parser 


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

            final AttributeSet attrs = Xml.asAttributeSet(parser);
            ......
                final String name = parser.getName();
             //如果根节点名为merge
                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, attrs, false);
                } else {
                    // createViewFromTag根据根节点名创建View对象,传入了根节点名和参数
                    .....
                    temp = createViewFromTag(root, name, attrs);
                    ......
                    }

                    // 递归调用rInflate方法查找这个temp(view)的子元素,并添加到福布局中
                    rInflate(parser, temp, attrs, true);
                    
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                 ......

            return result;
        }
    }

上面涉及到两个重要方法,createViewFromTag,rInflate

先来看看createViewFromTag

View createViewFromTag(View parent, String name, AttributeSet attrs) {
        ......
        view = createView(name, null, attrs);
        ......  
    }
public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        Class<? extends View> clazz = null;

        try {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);

            if (constructor == null) {
                // Class not found in the cache, see if it's real, and try to add it
                clazz = mContext.getClassLoader().loadClass(
                        prefix != null ? (prefix + name) : name).asSubclass(View.class);
                ......
                constructor = clazz.getConstructor(mConstructorSignature);
                sConstructorMap.put(name, constructor);
            } 
            ......
            final View view = constructor.newInstance(args);
            if (view instanceof ViewStub) {
                // always use ourselves when inflating ViewStub later
                final ViewStub viewStub = (ViewStub) view;
                viewStub.setLayoutInflater(this);
            }
            return view;

        } catch (NoSuchMethodException e) {
          ......           
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

createView方法主要根据节点名通过反射的方式获取到节点实例,在这里只是创建了一个根布局实例,再来看看inflate中的rInflate方法


void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs,
            boolean finishInflate) throws XmlPullParserException, IOException {

        final int depth = parser.getDepth();
        int type;

        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)) {
                parseRequestFocus(parser, parent);
            } else if (TAG_INCLUDE.equals(name)) {
                if (parser.getDepth() == 0) {
                    throw new InflateException("<include /> cannot be the root element");
                }
                parseInclude(parser, parent, attrs);
            } else if (TAG_MERGE.equals(name)) {
                throw new InflateException("<merge /> must be the root element");
            } else if (TAG_1995.equals(name)) {
                final View view = new BlinkLayout(mContext, attrs);
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                rInflate(parser, view, attrs, true);
                viewGroup.addView(view, params);                
            } else {
                final View view = createViewFromTag(parent, name, attrs);
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                rInflate(parser, view, attrs, true);
                viewGroup.addView(view, params);
            }
        }

        if (finishInflate) parent.onFinishInflate();
    }

可以看出rInflate循环遍历根布局下的子元素并加入到根布局中(addView),如果子元素是ViewGroup,则继续递归(rInflate)。这样,

整个布局文件都解析完成后就形成了一个完整的DOM结构,最终会把最顶层的根布局返回,至此inflate()过程全部结束。


3 inflate参数问题

(1)三个参数的inflate方法

public View inflate(int resource, ViewGroup root, boolean attachToRoot) {


(1.1)root  !=null  &&  attachToRoot==true


当root不为null,attachToRoot为true时,表示将resource指定的布局添加到root中,且添加的过程中resource所指定的的布局的根节点的所有属性都是有效的(因为根节点有父容器)



比如

Activity#activity_main
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"   
    android:layout_width="match_parent"  
    android:layout_height="match_parent"  
    android:orientation="vertical"  
    android:id="@+id/ll"  
    tools:context="org.sang.layoutinflater.MainActivity">  
</LinearLayout>



inflate#inflate_layout
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    android:id="@+id/ll"  
    android:layout_width="200dp"  
    android:layout_height="200dp"  
    android:background="@color/colorPrimary"  
    android:gravity="center"  
    android:orientation="vertical">  
  
    <Button  
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content" />  
</LinearLayout>

把这个inflate_layout.xml布局文件添加到activity的布局中


protected void onCreate(Bundle savedInstanceState) {  
    super.onCreate(savedInstanceState);  
    setContentView(R.layout.activity_main);  
    LinearLayout root = (LinearLayout) findViewById(R.id.ll);  
    LayoutInflater inflater = LayoutInflater.from(this);  
    inflater.inflate(R.layout.inflate_layout, root,true);  
}

此时root  !=null  &&  attachToRoot=true,所以inflate_layout中的属性android:layout_width="200dp"    android:layout_height="200dp"  都会生效

且无需使用root.addView(),inflate_layout的view会自动添加到root容器中,因为当第三个参数为true时,会自动将第一个参数所指定的View添加到第二个参数所指定的View中

(1.2)root  !=null  &&  attachToRoot==false

attachToRoot为false,表示不将第一个参数所指定的View加入到root容器中,既然不加入到root容器,为啥不直接将root  =null呢?

这里涉及到一个很重要的概念,我们在开发的过程中给控件所指定的layout_width和layout_height表示一个控件在容器中的大小,但是这个控件必须在容器中,这个属性才有意义。如果我们直接把inflate_layout加载进来却不给它指定父布局,则inflate_layout布局根节点的 android:layout_width="200dp"    android:layout_height="200dp"  就会失效,因为inflate_layout不处于任何容器中,。如果我们想让inflate_layout根节点的属性有效,又不想让其处于某一个容器中,那我就可以设置root不为null,而attachToRoot为false。这样root只会协助inflate_layout的根节点生成布局参数,无其他作用。

对于activity_main,因为activity_main的父布局为DecorView中的content,所以activity_main根布局的所有属性都会生效,详情请参考Android布局加载之setContentView篇

如果想把inflate_layout加入到root布局则

protected void onCreate(Bundle savedInstanceState) {  
    super.onCreate(savedInstanceState);  
    setContentView(R.layout.activity_main);  
    LinearLayout ll = (LinearLayout) findViewById(R.id.ll);  
    LayoutInflater inflater = LayoutInflater.from(this);  
    View view = inflater.inflate(R.layout.inflate_layout, ll, false);  
    ll.addView(view);  
}

这里root  !=null  &&  attachToRoot==false,且有addView,运行后效果和(1.1)一样



(1.3)root==null

当root为null时,不论attachToRoot为true还是为false,显示效果都是一样的。当root为null表示我不需要将第一个参数所指定的布局添加到任何容器中,同时也表示没有任何容器来来协助第一个参数所指定布局的根节点生成布局参数。

这个时候不管我给inflate_layout根节点的宽高设置什么,都是没有效果的,它都是包裹button,如果我修改button,则button会立即有变化,因为button是处于某一个容器中的

protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
        LinearLayout ll = (LinearLayout) findViewById(R.id.ll);  
        LayoutInflater inflater = LayoutInflater.from(this);  
        View view = inflater.inflate(R.layout.linearlayout, null, false);  
        ll.addView(view);  
    }


(2)两个参数的inflate方法

public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {  
        return inflate(parser, root, root != null);  
    }

从源码可以看出root !=null时,对应(1.1)的情况,root==null,对应(1.3)的情况


上面涉及到了一个问题,一个布局要在容器之中它的根布局属性才会生效,这里DecorView没有父布局了,为什么会生效呢?