源码分析android rinflate的使用-kb88凯时官网登录

来自:网络
时间:2023-05-29
阅读:
目录

这里接上一篇继续分析。

rinflate源码解析

这里详细理一理rinflate方法,作用就是找到传入的xmlpullparser当前层级所有的view并add到parent上:

    final void rinflatechildren(xmlpullparser parser, view parent, attributeset attrs,
            boolean finishinflate) throws xmlpullparserexception, ioexception {
        rinflate(parser, parent, parent.getcontext(), attrs, finishinflate);
    }
    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(" cannot be the root element");
                }
                parseinclude(parser, context, parent, attrs);
            } else if (tag_merge.equals(name)) {
                throw new inflateexception(" 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是个递归方法,代码很简单,递归-判断类型决定是否继续递归-递归。

递归

我们知道,递归最重要的就是结束条件的选取,这里的结束条件有这么几个:

  • type != xmlpullparser.end_tag
  • parser.getdepth() > depth
  • type != xmlpullparser.end_document

其实1和3都是常规的结束条件,最重要的是2这个条件,这个结束条件保证了当前循环只读取本层的view,我们结合一个例子来看一下。

下面是一个很简单的xmlpullparser解析的例子:



    
        

解析代码如下:

    public void readmainxml() {
        //1. 拿到资源文件
        inputstream is = getresources().openrawresource(r.raw.activity_main);
        //2. 拿到解析器对象
        xmlpullparser parser = xml.newpullparser();
        final int depth = parser.getdepth();
        try {
            //3. 初始化xp对象
            parser.setinput(is, "utf-8");
            //4.开始解析
            //获取当前节点的事件类型
            int type = parser.geteventtype();
            while (((type = parser.next()) != xmlpullparser.end_tag ||
                    parser.getdepth() > depth) && type != xmlpullparser.end_document) {
                switch (type) {
                    case xmlpullparser.start_tag:
                        int attrcount = parser.getattributecount();
                        logutil.d("depth:"   parser.getdepth()   " - "   parser.getname()   " 标签开始");
                        for (int i = 0; i < attrcount; i  ) {
                            string attrname = parser.getattributename(i);
                            string attrvalue = parser.getattributevalue(i);
                            //layout_width : match_parent
                            logutil.d("depth:"   parser.getdepth()   " - "   parser.getname()   "属性: "   attrname   " : "   attrvalue);
                        }
                        break;
                    case xmlpullparser.end_tag:
                        logutil.d("depth:"   parser.getdepth()   " - "   parser.getname()   "标签结束");
                        break;
                    default:
                }
            }
        } catch (exception e) {
            e.printstacktrace();
        }
    }
//    d: depth:1 - relativelayout 标签开始
//    d: depth:1 - relativelayout属性: layout_width : match_parent
//    d: depth:1 - relativelayout属性: layout_height : match_parent
//    d: depth:2 - linearlayout 标签开始
//    d: depth:2 - linearlayout属性: layout_width : wrap_content
//    d: depth:2 - linearlayout属性: layout_height : wrap_content
//    d: depth:3 - button 标签开始
//    d: depth:3 - button属性: id : @ id/btn_1
//    d: depth:3 - button属性: layout_width : 80dp
//    d: depth:3 - button属性: layout_height : 45dp
//    d: depth:3 - button标签结束
//    d: depth:2 - linearlayout标签结束
//    d: depth:2 - button 标签开始
//    d: depth:2 - button属性: id : @ id/btn_2
//    d: depth:2 - button属性: layout_width : match_parent
//    d: depth:2 - button属性: layout_height : 60dp
//    d: depth:2 - button标签结束
//    d: depth:1 - relativelayout标签结束

这里展示一个简单的xmlpullparser的例子,可以看到relativelayout有两个子view,分别是linearlayoutbutton2,depth都是2,结合上面的rinflate的代码可以理解,在view的递归树上,xmlpullparser的depth保证了层级,只会处理当前层级的view。

类型判断

方法体中做了类型的判断,特殊判断了几种类型如下:

tag_request_focus

非容器控件标签中放标签,表示将当前控件设为焦点,可以放到标签里面,多个edittext的时候使用标签首先获得焦点。

tag_tag

标签里面都可以放,类似于代码中使用view.settag:

    private void parseviewtag(xmlpullparser parser, view view, attributeset attrs)
            throws xmlpullparserexception, ioexception {
        final context context = view.getcontext();
        final typedarray ta = context.obtainstyledattributes(attrs, r.styleable.viewtag);
        final int key = ta.getresourceid(r.styleable.viewtag_id, 0);
        final charsequence value = ta.gettext(r.styleable.viewtag_value);
        view.settag(key, value);
        ta.recycle();
        consumechildelements(parser);
    }

根据id获取value,并把id当做key,设置parent的tag。可以看下面这个例子:

    
        
    

可以使用findviewbyid(r.id.et_1).gettag(r.id.tag1),得到tag_value值,注意不可以使用gettag(),有参数无参数获取的不是同一个属性。

tag_merge

这里还对标签做了二次的判断,保证标签不会出现在非root元素的位置。 如果不是上述特殊的标签,使用createviewfromtag加载出来view,并用当前的attrs加载成layoutparams设置给当前view,继续向下递归的同时把view add到parent.

tag_include

标签可以实现在一个layout中引用另一个layout的布局,这通常适合于界面布局复杂、不同界面有共用布局的app中,比如一个app的顶部布局、侧边栏布局、底部tab栏布局、listview和gridview每一项的布局等,将这些同一个app中有多个界面用到的布局抽取出来再通过标签引用,既可以降低layout的复杂度,又可以做到布局重用(布局有改动时只需要修改一个地方就可以了)。

这些类型之外就类似于之前分析过的处理,先调用createviewfromtag方法创建view,设置attrs属性,再调用递归方法rinflatechildren把view的子view add到view上,然后添加到parent上,直到层级遍历结束。

下面重点看parseinclude的源码分析:

parseinclude

private void parseinclude(xmlpullparser parser, context context, view parent,
            attributeset attrs) throws xmlpullparserexception, ioexception {
        int type;
//-------------------------------------第1部分-------------------------------------//
        if (!(parent instanceof viewgroup)) {
            throw new inflateexception(" can only be used inside of a viewgroup");
        }
        // 如果有theme属性,从当前view的attrs里面查看是否有theme属性,如果有,就重新创建contextthemewrapper,
        // 用当前view的theme替换之前contextthemewrapper里面的theme
        final typedarray ta = context.obtainstyledattributes(attrs, attrs_theme);
        final int themeresid = ta.getresourceid(0, 0);//inflateactivitymergetheme
        final boolean hasthemeoverride = themeresid != 0;
        if (hasthemeoverride) {
            context = new contextthemewrapper(context, themeresid);
        }
        ta.recycle();
        // 查看当前view的attrs里面是否有layout的id,也就是'@layout/xxxx‘,如果没有就返回0
        int layout = attrs.getattributeresourcevalue(null, attr_layout, 0);
        if (layout == 0) {
            //找不到先找这个layout属性的值'@layout/xxxx‘,看layout属性的string是否为空,如果是空就直接抛异常,不为空才去找layoutid
            final string value = attrs.getattributevalue(null, attr_layout);
            if (value == null || value.length() <= 0) {
                throw new inflateexception("you must specify a layout in the"
                      " include tag: ");
            }
            // 如果取不到,就尝试去"?attr/"下面找对应的属性。
            layout = context.getresources().getidentifier(
                value.substring(1), "attr", context.getpackagename());
        }
        // the layout might be referencing a theme attribute.
        if (mtempvalue == null) {
            mtempvalue = new typedvalue();
        }
        if (layout != 0 && context.gettheme().resolveattribute(layout, mtempvalue, true)) {
            layout = mtempvalue.resourceid;
        }
        if (layout == 0) {
            final string value = attrs.getattributevalue(null, attr_layout);
            throw new inflateexception("you must specify a valid layout "
                  "reference. the layout id "   value   " is not valid.");
        }
//-------------------------------------第2部分-------------------------------------//
        final view precompiled = tryinflateprecompiled(layout, context.getresources(),
            (viewgroup) parent, /*attachtoroot=*/true);
        if (precompiled == null) {
            final xmlresourceparser childparser = context.getresources().getlayout(layout);
            try {
                final attributeset childattrs = xml.asattributeset(childparser);
                while ((type = childparser.next()) != xmlpullparser.start_tag &&
                    type != xmlpullparser.end_document) {
                    // empty.
                }
                final string childname = childparser.getname();
                if (tag_merge.equals(childname)) {
                    // 如果是merge标签,不支持属性的设置,注意此处直接把parent作为父布局传入,也就是加载出来的子view直接挂到parent上。
                    rinflate(childparser, parent, context, childattrs, false);
                } else {
                    final view view = createviewfromtag(parent, childname,
                        context, childattrs, hasthemeoverride);
                    final viewgroup group = (viewgroup) parent;
                    // 获取include设置的id和visible。也就是说如果include设置了id和visible,会使用include设置的这两个属性
                    // 真正view设置的id和visible会不起作用
                    final typedarray a = context.obtainstyledattributes(
                        attrs, r.styleable.include);
                    final int id = a.getresourceid(r.styleable.include_id, view.no_id);
                    final int visibility = a.getint(r.styleable.include_visibility, -1);
                    a.recycle();
                    // 先尝试使用标签的属性去创建params,判断的标准是有没有width/height属性
                    // 如果没有则使用view的属性去创建params,然后调用view.setlayoutparams给view设置属性
                    // 换言之,如果设置了width/height属性,会整体覆盖view的属性,反之则不会。
                    viewgroup.layoutparams params = null;
                    try {
                        params = group.generatelayoutparams(attrs);
                    } catch (runtimeexception e) {
                        // ignore, just fail over to child attrs.
                    }
                    if (params == null) {
                        params = group.generatelayoutparams(childattrs);
                    }
                    view.setlayoutparams(params);
                    // inflate all children.
                    rinflatechildren(childparser, view, childattrs, true);
                    // 如果标签设置了id和visibility属性则一定会替换里面的id和visibility属性
                    // 换言之,标签设置了id和visibility属性,里面view的id和visibility会不起作用。
                    if (id != view.no_id) {
                        view.setid(id);
                    }
                    switch (visibility) {
                        case 0:
                            view.setvisibility(view.visible);
                            break;
                        case 1:
                            view.setvisibility(view.invisible);
                            break;
                        case 2:
                            view.setvisibility(view.gone);
                            break;
                    }
                    group.addview(view);
                }
            } finally {
                childparser.close();
            }
        }
        layoutinflater.consumechildelements(parser);
    }

两个部分:

  • 查找 标签是否有layout属性,并应用适合的theme属性
  • 判断是否是,不同的方式加载对应的view,替换对应的属性

第一部分:查找layout属性

最重要的就是用来做layout的替换,所以必须设置一个layout属性,没有设置layout属性的是没有意义的,有两种方式去设置这个layout属性: 一种是直接设置:

    

这种也是我们最常用的方式,这种方式我们称作

第二种方式是自定义一个reference,在attrs中定义,这样也可以用来实现重用,比如:

//attrs.xml
    
        
    
//style.xml
    

然后在layout中使用:



    

上面这种方式我们称作,或者下面这种我们称作


按照这几种的介绍我们来走一遍上面查找layout的代码:

        final typedarray ta = context.obtainstyledattributes(attrs, attrs_theme);
        final int themeresid = ta.getresourceid(0, 0);
        final boolean hasthemeoverride = themeresid != 0;
        if (hasthemeoverride) {
            context = new contextthemewrapper(context, themeresid);
        }
        ta.recycle();

这是方式的区别,方式说明传过来的context就有theme,方式表示能从attrs中找到theme属性,所以hasthemeoverride=true,如果需要覆盖就用当前view的theme重新创建了contextthemewrapper。这两者有一即可。

       // 查看当前view的attrs里面是否有layout的id,也就是'@layout/xxxx‘,如果没有就返回0
       int layout = attrs.getattributeresourcevalue(null, attr_layout, 0);
       if (layout == 0) {
           //找不到先找这个layout属性的值'@layout/xxxx‘,看layout属性的string是否为空,如果是空就直接抛异常,不为空才去找layoutid
           final string value = attrs.getattributevalue(null, attr_layout);
           if (value == null || value.length() <= 0) {
               throw new inflateexception("you must specify a layout in the"
                     " include tag: ");
           }
           // 如果取不到,就尝试去"?attr/"下面找对应的属性。
           layout = context.getresources().getidentifier(
               value.substring(1), "attr", context.getpackagename());
       }

关于方式,代码里其实写清楚了,先找@layout/xxx这样的,如果找不到就到?attr/下面找。

第二部分:加载对应的view并替换

这段的代码其实看上面代码里的注释就好了,很清晰。加载替换的layout有两种情况:

1.merge标签,我们知道 merge标签用于降低view树的层次来优化android的布局,所以merge标签并不是一层view结构,可以理解成一个占位,遇到merge标签就直接调用rinflate方法,找到所有的子view挂到parent上就好了,所以给设置什么属性,其实没什么作用。

2.非merge标签的其他viewgroup,createviewfromtag加载进来对应的viewgroup后

2.1. 尝试使用的属性,如果标签没有设置width/height这样的基础属性就使用加载进来的layout的属性。

2.2. 标签总是起作用的属性有两个,一个是id,一个是visibility,如果设置了这两个属性,总是会替换加载的layout的对应属性

设置完上面的属性之后,继续调用rinflatechildren去递归加载完所有的子view

其实这个逻辑很像刚inflate刚开始执行时候的逻辑,可以回忆一下之前的代码。

这里有个小demo来看清这几个的区别:

#styles.xml
    
    
    

总的布局文件如下:



    
    
    

两个子view的布局文件如下:

#include_test_merge.xml


    

#include_test_viewgroup.xml


    

显示效果图如下:

大致覆盖了上面说的几种include的方式

  • theme中设置layout,设置了theme可以不设置layout属性
  • include子view是标签和非标签的区别
  • 标签设置了width/height和其他位置相关的属性会使用外面设置的属性覆盖子view的属性,include_1没有设置属性所以使用的是include_test_viewgroup的属性,include_2设置了位置相关属性所以使用了设置的属性,从实际显示效果能看得出来。
  • 关于theme的覆盖,如果子view设置了theme,会使用子view设置的theme替换context(父布局relativelayout)的theme,根据android:background="?attr/colorprimary"可以看出来

总结

rinflate是递归方法,主要的递归判断条件是xmlpullparser的depth

rinflate中判断了多种类型,有requestfocus和tag这些特殊标签的处理,view的创建还是会调用createviewfromtag来处理

如果是include标签会使用parseinclude方法,因为标签的特殊性,会有一些和真实标签的属性和theme的判断和替换

设置theme就替换掉父布局的theme,两种方式设置layout属性,标签中直接设置layout或者使用theme中的layout。

标签中设置了位置属性会替换子view的属性,设置了id和visibility一定会生效。

以上就是源码分析android rinflate的使用的详细内容,更多关于android rinflate的资料请关注其它相关文章!

返回顶部
顶部
网站地图