目录
这里接上一篇继续分析。
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,分别是linearlayout
和button2
,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的资料请关注其它相关文章!