LayoutInflater源码分析与常见问题解疑

本篇文章主要分析LayoutInflater的inflate方法,并解决inflate方法使用不当造成的问题。

LayoutInflater实际开发中使用较多,动态添加布局、列表适配器设置布局等场景大多数情况都需要LayoutInflater的参与。LayoutInflaterV是View由XML转化为Class过程中必不可少的,我们常用的 setContentView内部也调用了LayoutInflater,详情可阅读Activity加载UI流程

获取LayoutInflater的方式有以下几种:

 LayoutInflater LayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
 LayoutInflater LayoutInflater = LayoutInflater getLayoutInflater();
 LayoutInflater LayoutInflater = LayoutInflater.from(context);

它们没有本质区别,可以根据需要进行选择,都是通过如下方式获取的:

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

熟悉getSystemService方法都知道,本质就是从Map中取出Context.LAYOUT_INFLATER_SERVICE这个key对应的服务,然后强转一下LayoutInflater即可。
我们在activity中调用 getLayoutInflater()即可获得LayoutInflater,其在activity中的实现如下:

 public LayoutInflater getLayoutInflater() {
        return getWindow().getLayoutInflater();
    }

其在方法内调用了 getWindow()的getLayoutInflater()方法,getWindow()获取的Window对象本质就PhoneWindow,PhoneWindow的 getLayoutInflater()方法如下:

public LayoutInflater getLayoutInflater() {
        return mLayoutInflater;
    }

PhoneWindow直接返回了其成员变量mLayoutInflater,该成员变量的创建是在PhoneWindow的构造函数内。

 public PhoneWindow(Context context) {
        super(context);
        mLayoutInflater = LayoutInflater.from(context);
    }

也就是说activity内调用 getLayoutInflater()获取 LayoutInflater在实质是调用了 LayoutInflater.from(context)方法,LayoutInflater的from方法实现如下:

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

所以最终还是调用了context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)来获得LayoutInflater对象的。

inflate

LayoutInflater的inflate方法有多个形式的重载,最终调用的是 inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) 这个方法。这几个方法都非常的重要,因为很多LayoutInflater相关的问题与这几个方法密切相关。

方法一:内部调用了方法三,如果root不为null,则第三个参数为true,否则为false

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

方法二:内部调用了方法四,如果root不为null,则第三个参数为true,否则为false

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

方法三:现创建 XmlResourceParser实例,然后内部调用了方法四

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

方法四:对XML进行解析,注意参数不同的情况

 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("**************************");
                }
		//如果标签是merge,则root不能为空,且attachToRoot不能为false,因为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");
                    }
					//解析内部View,传入的父布局为root
                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    // Temp is the root view that was found in the xml    
		  //找到xml中的根布局
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);
		  //设置LayoutParams为null,这个很关键,很多布局参数跟实际有出入的问题都是它导致的,xml里的LayoutParams刚解析出默认为null
                    ViewGroup.LayoutParams params = null;
			//如果root布局(非XML)存在,则根据XML根布局的属性生成LayoutParams。也就是说加入root为null,XML里的根布局的View布局参数一定为null
					//attachToRoot为false则将LayoutParams设置给XML里的根布局
                    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. 
		//解析所有的子View,并把其添加到XML的根布局里
                    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.  
		//如果root不为null,且attachToRoot为true则将xml根布局添加到root里
                    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.
		  //如果root为null或者attachToRoot则直接返回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;
        }
    }

方法四整体的逻辑比较简单,而且我也给了中文注释,但有几点还是需要强调一下:
如果XML的根布局标签为Merge,则root(parent)不能为空且attachToRoot必须为true,因为Merge标签需要将其合并到root布局,解析内部View时传入的父布局必须是Root。
从XML解析出的根View布局参数是否为null与父布局root有关,root存在则会根据属性生成对应的布局参数,如果root不存在则布局参数为null。
如果父布局root存在且attachToRoot为true则将XML解析出的根布局添加到root中并返回root,否则返回XML的根布局。

常见问题解疑

如果能够理解上面的几点,那么就可以解决常见的跟LayoutInflater相关的问题了,如:
问题1:列表的item布局为啥inflate出来效果跟设置的布局参数不一致?
在这里插入图片描述
在这里插入图片描述

第一张图是为正确效果,第二张是因为inflate参数没传对导致的。

 View view = LayoutInflater.from(mContext).inflate(R.layout.item_pic_zero, null);

传入的parent为null,所以导致inflate出来的View的布局参数为null,在adapter里动态添加布局虽然会自动创建一个布局参数,但是已经跟原有的设置的属性不一致了。
类似的还有这两种写法,其实parent为null了,后面的参数已经么有意义了,attachToRoot是添加到parent的标志。

View view = LayoutInflater.from(mContext).inflate(R.layout.item_pic_zero, null ,true);
  View view = LayoutInflater.from(mContext).inflate(R.layout.item_pic_zero, null ,false);

问题2:为什么列表inflate的条目时会报The specified child already has a parent. You must call removeView() on the child’s parent first错误?
在这里插入图片描述
从错误日志很明显可以看到是因为子View已经存在父布局了只能移除后才能再次添加。
这是因为attachToRoot错误的传成了True;

 View view = LayoutInflater.from(mContext).inflate(R.layout.item_pic_zero, parent, true);

attachToRoot代表的是将XML布局添加到parent,并返回parent,而parent本身是有父布局的,所以再次添加这个布局的时候就会报错。
正确的写法为:

 View view = LayoutInflater.from(mContext).inflate(R.layout.item_pic_three, parent, false);

父布局不为空,但attachToRoot为false,也就是说解析出XML布局以及其布局参数并返回。

哪inflate(viewID, parent, true)什么时候用呢?子View需要被inflate出来并添加到父布局时使用,常见于一些自定义控件中。

rInflate

inflate方法根据是否为Merge标签分别调用了 rInflate和 rInflateChildren,从下面的代码可以看到rInflateChildren内部也是调用rInflate完成解析任务的。

/**
     * Recursive method used to inflate internal (non-root) children. This
     * method calls through to {@link #rInflate} using the parent context as
     * the inflation context.
     * <strong>Note:</strong> Default visibility so the BridgeInflater can
     * call it.
     */
    final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
            boolean finishInflate) throws XmlPullParserException, IOException {
        rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
    }

    /**
     * Recursive method used to descend down the xml hierarchy and instantiate
     * views, instantiate their children, and then call onFinishInflate().
     * <p>
     * <strong>Note:</strong> Default visibility so the BridgeInflater can
     * override it.
     */
    void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {

        final int depth = parser.getDepth();
        int type;
        boolean pendingRequestFocus = false;

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

        if (pendingRequestFocus) {
            parent.restoreDefaultFocus();
        }

        if (finishInflate) {
            parent.onFinishInflate();
        }
    }

rInflate方法对View的tag以及include等进行了处理,结合inflate方法可以看到,标签为Merge则传入的parent为root,否则为解析出的XML的布局temp,这就是为什么Merge标签能将其内部布局合并到父布局的原因。
除此之外, rInflate也调用了createViewFromTag进行View的解析,这个方法非常的重要,是LayoutInflater.的核心知识点之一,后面会有专门文章介绍。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值