欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android布局优化之ViewStub、include、merge使用与源码分析

程序员文章站 2024-03-26 13:59:47
...


在开发中UI布局是我们都会遇到的问题,随着UI越来越多,布局的重复性、复杂度也会随之增长。Android官方给了几个优化的方法,但是网络上的资料基本上都是对官方资料的翻译,这些资料都特别的简单,经常会出现问题而不知其所以然。这篇文章就是对这些问题的更详细的说明,也欢迎大家多留言交流。


一、include


首先用得最多的应该是include,按照官方的意思,include就是为了解决重复定义相同布局的问题。例如你有五个界面,这五个界面的顶部都有布局一模一样的一个返回按钮和一个文本控件,在不使用include的情况下你在每个界面都需要重新在xml里面写同样的返回按钮和文本控件的顶部栏,这样的重复工作会相当的恶心。使用include标签,我们只需要把这个会被多次使用的顶部栏独立成一个xml文件,然后在需要使用的地方通过include标签引入即可。其实就相当于C语言、C++中的include头文件一样,我们把一些常用的、底层的API封装起来,然后复用,需要的时候引入它即可,而不必每次都自己写一遍。示例如下 :


my_title_layout.xml

<?xml version=”1.0” encoding=”utf-8”?> 

<RelativeLayout xmlns:android=http://schemas.android.com/apk/res/android 

    android:layout_width=“match_parent” 

    android:id=“@+id/my_title_parent_id 

    android:layout_height=“wrap_content” >  

<SPAN class=hljs-tag>&lt;<SPAN class=hljs-title>ImageButton</SPAN>  
    <SPAN class=hljs-attribute>android:id</SPAN>=<SPAN class=hljs-value>"@+id/back_btn"</SPAN>  
    <SPAN class=hljs-attribute>android:layout_width</SPAN>=<SPAN class=hljs-value>"wrap_content"</SPAN>  
    <SPAN class=hljs-attribute>android:layout_height</SPAN>=<SPAN class=hljs-value>"wrap_content"</SPAN>  
    <SPAN class=hljs-attribute>android:src</SPAN>=<SPAN class=hljs-value>"@drawable/ic_launcher"</SPAN> /&gt;</SPAN>  

<SPAN class=hljs-tag>&lt;<SPAN class=hljs-title>TextView</SPAN>  
    <SPAN class=hljs-attribute>android:id</SPAN>=<SPAN class=hljs-value>"@+id/title_tv"</SPAN>  
    <SPAN class=hljs-attribute>android:layout_width</SPAN>=<SPAN class=hljs-value>"wrap_content"</SPAN>  
    <SPAN class=hljs-attribute>android:layout_height</SPAN>=<SPAN class=hljs-value>"wrap_content"</SPAN>  
    <SPAN class=hljs-attribute>android:layout_centerVertical</SPAN>=<SPAN class=hljs-value>"true"</SPAN>  
    <SPAN class=hljs-attribute>android:layout_marginLeft</SPAN>=<SPAN class=hljs-value>"20dp"</SPAN>  
    <SPAN class=hljs-attribute>android:layout_toRightOf</SPAN>=<SPAN class=hljs-value>"@+id/back_btn"</SPAN>  
    <SPAN class=hljs-attribute>android:gravity</SPAN>=<SPAN class=hljs-value>"center"</SPAN>  
    <SPAN class=hljs-attribute>android:text</SPAN>=<SPAN class=hljs-value>"我的title"</SPAN>  
    <SPAN class=hljs-attribute>android:textSize</SPAN>=<SPAN class=hljs-value>"18sp"</SPAN> /&gt;</SPAN>  

</RelativeLayout>

  • 1

include布局文件:

<?xml version=”1.0” encoding=”utf-8”?> 

<LinearLayout xmlns:android=http://schemas.android.com/apk/res/android 

    android:layout_width=“match_parent” 

    android:layout_height=“match_parent” 

    android:orientation=“vertical” >  

<SPAN class=hljs-tag>&lt;<SPAN class=hljs-title>include</SPAN>  
    <SPAN class=hljs-attribute>android:id</SPAN>=<SPAN class=hljs-value>"@+id/my_title_ly"</SPAN>  
    <SPAN class=hljs-attribute>android:layout_width</SPAN>=<SPAN class=hljs-value>"match_parent"</SPAN>  
    <SPAN class=hljs-attribute>android:layout_height</SPAN>=<SPAN class=hljs-value>"wrap_content"</SPAN>  
    <SPAN class=hljs-attribute>layout</SPAN>=<SPAN class=hljs-value>"@layout/my_title_layout"</SPAN> /&gt;</SPAN>  

<SPAN class=hljs-comment>&lt;!-- 代码省略 --&gt;</SPAN>

</LinearLayout>

  • 1


这样我们就可以使用my_title_layout了。


注意事项



  • 使用include最常见的问题就是findViewById查找不到目标控件,这个问题出现的前提是在include时设置了id,而在findViewById时却用了被include进来的布局的根元素id。例如上述例子中,include时设置了该布局的id为my_title_ly,而my_title_layout.xml中的根视图的id为my_title_parent_id。此时如果通过findViewById来找my_title_parent_id这个控件,然后再查找my_title_parent_id下的子控件则会抛出空指针。代码如下 :
View titleView = findViewById(R.id.my_title_parent_id) ; 

// 此时 titleView 为空,找不到。此时空指针 

 TextView titleTextView = (TextView)titleView.findViewById(R.id.title_tv) ; 

titleTextView.setText("new Title");  
  • 1

其正确的使用形式应该如下:

// 使用include时设置的id,即R.id.my_title_ly 

View titleView = findViewById(R.id.my_title_ly) ; 

// 通过titleView找子控件 

TextView titleTextView = (TextView)titleView.findViewById(R.id.title_tv) ; 

titleTextView.setText("new Title");  
  • 1

或者更简单的直接查找它的子控件:

TextView titleTextView = (TextView)findViewById(R.id.title_tv) ; 

titleTextView.setText("new Title");  
  • 1

那么使用findViewById(R.id.my_title_parent_id)为什么会报空指针呢? 我们来分析它的源码看看吧。对于布局文件的解析,最终都会调用到LayoutInflater的inflate方法,该方法最终又会调用rInflate方法,我们看看这个方法。

    /**  

     * Recursive method used to descend down the xml hierarchy and instantiate  

     * views, instantiate their children, and then call onFinishInflate().  

     */ 

    void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs, 

            boolean finishInflate) throws XmlPullParserException, IOException {  

    <SPAN class=hljs-keyword>final</SPAN> <SPAN class=hljs-keyword>int</SPAN> depth = parser.getDepth();  
    <SPAN class=hljs-keyword>int</SPAN> type;  
     <SPAN class=hljs-comment>// 迭代xml中的所有元素,挨个解析  </SPAN>
    <SPAN class=hljs-keyword>while</SPAN> (((type = parser.next()) != XmlPullParser.END_TAG ||  
            parser.getDepth() &gt; depth) &amp;&amp; type != XmlPullParser.END_DOCUMENT) {  

        <SPAN class=hljs-keyword>if</SPAN> (type != XmlPullParser.START_TAG) {  
            <SPAN class=hljs-keyword>continue</SPAN>;  
        }  

        <SPAN class=hljs-keyword>final</SPAN> String name = parser.getName();  

        <SPAN class=hljs-keyword>if</SPAN> (TAG_REQUEST_FOCUS.equals(name)) {  
            parseRequestFocus(parser, parent);  
        } <SPAN class=hljs-keyword>else</SPAN> <SPAN class=hljs-keyword>if</SPAN> (TAG_INCLUDE.equals(name)) {<SPAN class=hljs-comment>// 如果xml中的节点是include节点,则调用parseInclude方法  </SPAN>
            <SPAN class=hljs-keyword>if</SPAN> (parser.getDepth() == <SPAN class=hljs-number>0</SPAN>) {  
                <SPAN class=hljs-keyword>throw</SPAN> <SPAN class=hljs-keyword>new</SPAN> InflateException(<SPAN class=hljs-string>"&lt;include /&gt; cannot be the root element"</SPAN>);  
            }  
            parseInclude(parser, parent, attrs);  
        } <SPAN class=hljs-keyword>else</SPAN> <SPAN class=hljs-keyword>if</SPAN> (TAG_MERGE.equals(name)) {  
            <SPAN class=hljs-keyword>throw</SPAN> <SPAN class=hljs-keyword>new</SPAN> InflateException(<SPAN class=hljs-string>"&lt;merge /&gt; must be the root element"</SPAN>);  
        } <SPAN class=hljs-keyword>else</SPAN> <SPAN class=hljs-keyword>if</SPAN> (TAG_1995.equals(name)) {  
            <SPAN class=hljs-keyword>final</SPAN> View view = <SPAN class=hljs-keyword>new</SPAN> BlinkLayout(mContext, attrs);  
            <SPAN class=hljs-keyword>final</SPAN> ViewGroup viewGroup = (ViewGroup) parent;  
            <SPAN class=hljs-keyword>final</SPAN> ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);  
            rInflate(parser, view, attrs, <SPAN class=hljs-keyword>true</SPAN>);  
            viewGroup.addView(view, params);                  
        } <SPAN class=hljs-keyword>else</SPAN> {  
            <SPAN class=hljs-keyword>final</SPAN> View view = createViewFromTag(parent, name, attrs);  
            <SPAN class=hljs-keyword>final</SPAN> ViewGroup viewGroup = (ViewGroup) parent;  
            <SPAN class=hljs-keyword>final</SPAN> ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);  
            rInflate(parser, view, attrs, <SPAN class=hljs-keyword>true</SPAN>);  
            viewGroup.addView(view, params);  
        }  
    }  

    <SPAN class=hljs-keyword>if</SPAN> (finishInflate) parent.onFinishInflate();  
}  </CODE><UL class=pre-numbering style="ZOOM: 1"><LI>1</LI></UL></PRE>

这个方法其实就是遍历xml中的所有元素,然后挨个进行解析。例如解析到一个标签,那么就根据用户设置的一些layout_width、layout_height、id等属性来构造一个TextView对象,然后添加到父控件(ViewGroup类型)中。标签也是一样的,我们看到遇到include标签时,会调用parseInclude函数,这就是对标签的解析,我们看看吧。

private void parseInclude(XmlPullParser parser, View parent, AttributeSet attrs) 

           throws XmlPullParserException, IOException {  

   <SPAN class=hljs-keyword>int</SPAN> type;  

   <SPAN class=hljs-keyword>if</SPAN> (parent <SPAN class=hljs-keyword>instanceof</SPAN> ViewGroup) {  
       <SPAN class=hljs-keyword>final</SPAN> <SPAN class=hljs-keyword>int</SPAN> layout = attrs.getAttributeResourceValue(<SPAN class=hljs-keyword>null</SPAN>, <SPAN class=hljs-string>"layout"</SPAN>, <SPAN class=hljs-number>0</SPAN>);  
       <SPAN class=hljs-keyword>if</SPAN> (layout == <SPAN class=hljs-number>0</SPAN>) {<SPAN class=hljs-comment>// include标签中没有设置layout属性,会抛出异常  </SPAN>
           <SPAN class=hljs-keyword>final</SPAN> String value = attrs.getAttributeValue(<SPAN class=hljs-keyword>null</SPAN>, <SPAN class=hljs-string>"layout"</SPAN>);  
           <SPAN class=hljs-keyword>if</SPAN> (value == <SPAN class=hljs-keyword>null</SPAN>) {  
               <SPAN class=hljs-keyword>throw</SPAN> <SPAN class=hljs-keyword>new</SPAN> InflateException(<SPAN class=hljs-string>"You must specifiy a layout in the"</SPAN>  
                       + <SPAN class=hljs-string>" include tag: &lt;include layout=\"@layout/layoutID\" /&gt;"</SPAN>);  
           } <SPAN class=hljs-keyword>else</SPAN> {  
               <SPAN class=hljs-keyword>throw</SPAN> <SPAN class=hljs-keyword>new</SPAN> InflateException(<SPAN class=hljs-string>"You must specifiy a valid layout "</SPAN>  
                       + <SPAN class=hljs-string>"reference. The layout ID "</SPAN> + value + <SPAN class=hljs-string>" is not valid."</SPAN>);  
           }  
       } <SPAN class=hljs-keyword>else</SPAN> {  
           <SPAN class=hljs-keyword>final</SPAN> XmlResourceParser childParser =  
                   getContext().getResources().getLayout(layout);  

           <SPAN class=hljs-keyword>try</SPAN> {<SPAN class=hljs-comment>// 获取属性集,即在include标签中设置的属性  </SPAN>
               <SPAN class=hljs-keyword>final</SPAN> AttributeSet childAttrs = Xml.asAttributeSet(childParser);  

               <SPAN class=hljs-keyword>while</SPAN> ((type = childParser.next()) != XmlPullParser.START_TAG &amp;&amp;  
                       type != XmlPullParser.END_DOCUMENT) {  
                   <SPAN class=hljs-comment>// Empty.  </SPAN>
               }  

               <SPAN class=hljs-keyword>if</SPAN> (type != XmlPullParser.START_TAG) {  
                   <SPAN class=hljs-keyword>throw</SPAN> <SPAN class=hljs-keyword>new</SPAN> InflateException(childParser.getPositionDescription() +  
                           <SPAN class=hljs-string>": No start tag found!"</SPAN>);  
               }  
               <SPAN class=hljs-comment>// 1、解析include中的第一个元素  </SPAN>
               <SPAN class=hljs-keyword>final</SPAN> String childName = childParser.getName();  
               <SPAN class=hljs-comment>// 如果第一个元素是merge标签,那么调用rInflate函数解析  </SPAN>
               <SPAN class=hljs-keyword>if</SPAN> (TAG_MERGE.equals(childName)) {  
                   <SPAN class=hljs-comment>// Inflate all children.  </SPAN>
                   rInflate(childParser, parent, childAttrs, <SPAN class=hljs-keyword>false</SPAN>);  
               } <SPAN class=hljs-keyword>else</SPAN> {<SPAN class=hljs-comment>// 2、我们例子中的情况会走到这一步,首先根据include的属性集创建被include进来的xml布局的根view  </SPAN>
                   <SPAN class=hljs-comment>// 这里的根view对应为my_title_layout.xml中的RelativeLayout  </SPAN>
                   <SPAN class=hljs-keyword>final</SPAN> View view = createViewFromTag(parent, childName, childAttrs);  
                   <SPAN class=hljs-keyword>final</SPAN> ViewGroup group = (ViewGroup) parent;<SPAN class=hljs-comment>// include标签的parent view  </SPAN>

                   ViewGroup.LayoutParams params = <SPAN class=hljs-keyword>null</SPAN>;  
                   <SPAN class=hljs-keyword>try</SPAN> {<SPAN class=hljs-comment>// 获3、取布局属性  </SPAN>
                       params = group.generateLayoutParams(attrs);  
                   } <SPAN class=hljs-keyword>catch</SPAN> (RuntimeException e) {  
                       params = group.generateLayoutParams(childAttrs);  
                   } <SPAN class=hljs-keyword>finally</SPAN> {  
                       <SPAN class=hljs-keyword>if</SPAN> (params != <SPAN class=hljs-keyword>null</SPAN>) {<SPAN class=hljs-comment>// 被inlcude进来的根view设置布局参数  </SPAN>
                           view.setLayoutParams(params);  
                       }  
                   }  

                   <SPAN class=hljs-comment>// 4、Inflate all children. 解析所有子控件  </SPAN>
                   rInflate(childParser, view, childAttrs, <SPAN class=hljs-keyword>true</SPAN>);  

                   <SPAN class=hljs-comment>// Attempt to override the included layout's android:id with the  </SPAN>
                   <SPAN class=hljs-comment>// one set on the &lt;include /&gt; tag itself.  </SPAN>
                   TypedArray a = mContext.obtainStyledAttributes(attrs,  
                       com.android.internal.R.styleable.View, <SPAN class=hljs-number>0</SPAN>, <SPAN class=hljs-number>0</SPAN>);  
                   <SPAN class=hljs-keyword>int</SPAN> id = a.getResourceId(com.android.internal.R.styleable.View_id, View.NO_ID);  
                   <SPAN class=hljs-comment>// While we're at it, let's try to override android:visibility.  </SPAN>
                   <SPAN class=hljs-keyword>int</SPAN> visibility = a.getInt(com.android.internal.R.styleable.View_visibility, -<SPAN class=hljs-number>1</SPAN>);  
                   a.recycle();  
                    <SPAN class=hljs-comment>// 5、将include中设置的id设置给根view,因此实际上my_title_layout.xml中的RelativeLayout的id会变成include标签中的id,include不设置id,那么也可以通过relative的找到.  </SPAN>
                   <SPAN class=hljs-keyword>if</SPAN> (id != View.NO_ID) {  
                       view.setId(id);  
                   }  

                   <SPAN class=hljs-keyword>switch</SPAN> (visibility) {  
                       <SPAN class=hljs-keyword>case</SPAN> <SPAN class=hljs-number>0</SPAN>:  
                           view.setVisibility(View.VISIBLE);  
                           <SPAN class=hljs-keyword>break</SPAN>;  
                       <SPAN class=hljs-keyword>case</SPAN> <SPAN class=hljs-number>1</SPAN>:  
                           view.setVisibility(View.INVISIBLE);  
                           <SPAN class=hljs-keyword>break</SPAN>;  
                       <SPAN class=hljs-keyword>case</SPAN> <SPAN class=hljs-number>2</SPAN>:  
                           view.setVisibility(View.GONE);  
                           <SPAN class=hljs-keyword>break</SPAN>;  
                   }  
                   <SPAN class=hljs-comment>// 6、将根view添加到父控件中  </SPAN>
                   group.addView(view);  
               }  
           } <SPAN class=hljs-keyword>finally</SPAN> {  
               childParser.close();  
           }  
       }  
   } <SPAN class=hljs-keyword>else</SPAN> {  
       <SPAN class=hljs-keyword>throw</SPAN> <SPAN class=hljs-keyword>new</SPAN> InflateException(<SPAN class=hljs-string>"&lt;include /&gt; can only be used inside of a ViewGroup"</SPAN>);  
   }  

   <SPAN class=hljs-keyword>final</SPAN> <SPAN class=hljs-keyword>int</SPAN> currentDepth = parser.getDepth();  
   <SPAN class=hljs-keyword>while</SPAN> (((type = parser.next()) != XmlPullParser.END_TAG ||  
           parser.getDepth() &gt; currentDepth) &amp;&amp; type != XmlPullParser.END_DOCUMENT) {  
       <SPAN class=hljs-comment>// Empty  </SPAN>
   }  

}

  • 1


整个过程就是根据不同的标签解析不同的元素,首先会解析include元素,然后再解析被include进来的布局的root view元素。在我们的例子中对应的root view就是id为my_title_parent_id的RelativeLayout,然后再解析root view下面的所有元素,这个过程是从上面注释的2~4的过程,然后是设置布局参数。我们注意看注释5处,这里就解释了为什么include标签和被引入的布局的根元素都设置了id的情况下,通过被引入的根元素的id来查找子控件会找不到的情况。我们看到,注释5处的会判断include标签的id如果不是View.NO_ID的话会把该id设置给被引入的布局根元素的id,即此时在我们的例子中被引入的id为my_title_parent_id的根元素RelativeLayout的id被设置成了include标签中的id,即RelativeLayout的id被动态修改成了”my_title_ly”。因此此时我们再通过“my_title_parent_id”这个id来查找根元素就会找不到了!
所以结论就是: 如果include中设置了id,那么就通过include的id来查找被include布局根元素的View;如果include中没有设置Id, 而被include的布局的根元素设置了id,那么通过该根元素的id来查找该view即可。拿到根元素后查找其子控件都是一样的。


二、ViewStub


我们先看看官方的说明:
ViewStub is a lightweight view with no dimension and doesn’t draw anything or participate in the layout. As such, it’s cheap to inflate and cheap to leave in a view hierarchy. Each ViewStub simply needs to include the android:layout attribute to specify the layout to inflate.


其实ViewStub就是一个宽高都为0的一个View,它默认是不可见的,只有通过调用setVisibility函数或者Inflate函数才会将其要装载的目标布局给加载出来,从而达到延迟加载的效果,这个要被加载的布局通过android:layout属性来设置。例如我们通过一个ViewStub来惰性加载一个消息流的评论列表,因为一个帖子可能并没有评论,此时我可以不加载这个评论的ListView,只有当有评论时我才把它加载出来,这样就去除了加载ListView带来的资源消耗以及延时,示例如下 :

<ViewStub 

    android:id="@+id/stub_import" 

    android:inflatedId="@+id/stub_comm_lv" 

    android:layout="@layout/my_comment_layout" 

    android:layout_width="fill_parent" 

    android:layout_height="wrap_content" 

    android:layout_gravity="bottom" /  
  • 1

my_comment_layout.xml如下:

<?xml version=”1.0” encoding=”utf-8”?> 

<ListView xmlns:android=http://schemas.android.com/apk/res/android 

    android:layout_width=“match_parent” 

    android:id=“@+id/my_comm_lv 

    android:layout_height=“match_parent” >  

</ListView>

  • 1


在运行时,我们只需要控制id为stub_import的ViewStub的可见性或者调用inflate()函数来控制是否加载这个评论列表即可。示例如下 :

public class MainActivity extends Activity {  

<SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>void</SPAN> <SPAN class=hljs-title>onCreate</SPAN>(Bundle b){  
    <SPAN class=hljs-comment>// main.xml中包含上面的ViewStub  </SPAN>
    setContentView(R.layout.main);  

    <SPAN class=hljs-comment>// 方式1,获取ViewStub,  </SPAN>
    ViewStub listStub = (ViewStub) findViewById(R.id.stub_import);  
    <SPAN class=hljs-comment>// 加载评论列表布局  </SPAN>
    listStub.setVisibility(View.VISIBLE);  
    <SPAN class=hljs-comment>// 获取到评论ListView,注意这里是通过ViewStub的inflatedId来获取  </SPAN>
        ListView commLv = findViewById(R.id.stub_comm_lv);  
            <SPAN class=hljs-keyword>if</SPAN> ( listStub.getVisibility() == View.VISIBLE ) {  
                   <SPAN class=hljs-comment>// 已经加载, 否则还没有加载  </SPAN>
            }  
        }  
   }  </CODE><UL class=pre-numbering style="ZOOM: 1"><LI>1</LI></UL></PRE>

通过setVisibility(View.VISIBILITY)来加载评论列表,此时你要获取到评论ListView对象的话,则需要通过findViewById来查找,而这个id并不是就是ViewStub的id。
这是为什么呢 ?


我们先看ViewStub的部分代码吧:

 

        @SuppressWarnings({“UnusedDeclaration”}) 

    public ViewStub(Context context, AttributeSet attrs, int defStyle) { 

        TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ViewStub, 

                defStyle, 0); 

        // 获取inflatedId属性   

        mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID); 

        mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);  

    a.recycle();  

    a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.View, defStyle, <SPAN class=hljs-number>0</SPAN>);  
    mID = a.getResourceId(R.styleable.View_id, NO_ID);  
    a.recycle();  

    initialize(context);  
}  

<SPAN class=hljs-keyword>private</SPAN> <SPAN class=hljs-keyword>void</SPAN> <SPAN class=hljs-title>initialize</SPAN>(Context context) {  
    mContext = context;  
    setVisibility(GONE);<SPAN class=hljs-comment>// 设置不可教案  </SPAN>
    setWillNotDraw(<SPAN class=hljs-keyword>true</SPAN>);<SPAN class=hljs-comment>// 设置不绘制  </SPAN>
}  

<SPAN class=hljs-annotation>@Override</SPAN>  
<SPAN class=hljs-keyword>protected</SPAN> <SPAN class=hljs-keyword>void</SPAN> <SPAN class=hljs-title>onMeasure</SPAN>(<SPAN class=hljs-keyword>int</SPAN> widthMeasureSpec, <SPAN class=hljs-keyword>int</SPAN> heightMeasureSpec) {  
    setMeasuredDimension(<SPAN class=hljs-number>0</SPAN>, <SPAN class=hljs-number>0</SPAN>);<SPAN class=hljs-comment>// 宽高都为0  </SPAN>
}  


<SPAN class=hljs-annotation>@Override</SPAN>  
<SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>void</SPAN> <SPAN class=hljs-title>setVisibility</SPAN>(<SPAN class=hljs-keyword>int</SPAN> visibility) {  
    <SPAN class=hljs-keyword>if</SPAN> (mInflatedViewRef != <SPAN class=hljs-keyword>null</SPAN>) {<SPAN class=hljs-comment>// 如果已经加载过则只设置Visibility属性  </SPAN>
        View view = mInflatedViewRef.get();  
        <SPAN class=hljs-keyword>if</SPAN> (view != <SPAN class=hljs-keyword>null</SPAN>) {  
            view.setVisibility(visibility);  
        } <SPAN class=hljs-keyword>else</SPAN> {  
            <SPAN class=hljs-keyword>throw</SPAN> <SPAN class=hljs-keyword>new</SPAN> IllegalStateException(<SPAN class=hljs-string>"setVisibility called on un-referenced view"</SPAN>);  
        }  
    } <SPAN class=hljs-keyword>else</SPAN> {<SPAN class=hljs-comment>// 如果未加载,这加载目标布局  </SPAN>
        <SPAN class=hljs-keyword>super</SPAN>.setVisibility(visibility);  
        <SPAN class=hljs-keyword>if</SPAN> (visibility == VISIBLE || visibility == INVISIBLE) {  
            inflate();<SPAN class=hljs-comment>// 调用inflate来加载目标布局  </SPAN>
        }  
    }  
}  

<SPAN class=hljs-javadoc>/** 
 * Inflates the layout resource identified by {@link #getLayoutResource()} 
 * and replaces this StubbedView in its parent by the inflated layout resource. 
 * 
 *<SPAN class=hljs-javadoctag> @return</SPAN> The inflated layout resource. 
 * 
 */</SPAN>  
<SPAN class=hljs-keyword>public</SPAN> View <SPAN class=hljs-title>inflate</SPAN>() {  
    <SPAN class=hljs-keyword>final</SPAN> ViewParent viewParent = getParent();  

    <SPAN class=hljs-keyword>if</SPAN> (viewParent != <SPAN class=hljs-keyword>null</SPAN> &amp;&amp; viewParent <SPAN class=hljs-keyword>instanceof</SPAN> ViewGroup) {  
        <SPAN class=hljs-keyword>if</SPAN> (mLayoutResource != <SPAN class=hljs-number>0</SPAN>) {  
            <SPAN class=hljs-keyword>final</SPAN> ViewGroup parent = (ViewGroup) viewParent;<SPAN class=hljs-comment>// 获取ViewStub的parent view,也是目标布局根元素的parent view  </SPAN>
            <SPAN class=hljs-keyword>final</SPAN> LayoutInflater factory = LayoutInflater.from(mContext);  
            <SPAN class=hljs-keyword>final</SPAN> View view = factory.inflate(mLayoutResource, parent,  
                    <SPAN class=hljs-keyword>false</SPAN>);<SPAN class=hljs-comment>// 1、加载目标布局  </SPAN>
          <SPAN class=hljs-comment>// 2、如果ViewStub的inflatedId不是NO_ID则把inflatedId设置为目标布局根元素的id,即评论ListView的id  </SPAN>
            <SPAN class=hljs-keyword>if</SPAN> (mInflatedId != NO_ID) {  
                view.setId(mInflatedId);  
            }  

            <SPAN class=hljs-keyword>final</SPAN> <SPAN class=hljs-keyword>int</SPAN> index = parent.indexOfChild(<SPAN class=hljs-keyword>this</SPAN>);  
            parent.removeViewInLayout(<SPAN class=hljs-keyword>this</SPAN>);<SPAN class=hljs-comment>// 3、将ViewStub自身从parent中移除  </SPAN>

            <SPAN class=hljs-keyword>final</SPAN> ViewGroup.LayoutParams layoutParams = getLayoutParams();  
            <SPAN class=hljs-keyword>if</SPAN> (layoutParams != <SPAN class=hljs-keyword>null</SPAN>) {  
                parent.addView(view, index, layoutParams);<SPAN class=hljs-comment>// 4、将目标布局的根元素添加到parent中,有参数  </SPAN>
            } <SPAN class=hljs-keyword>else</SPAN> {  
                parent.addView(view, index);<SPAN class=hljs-comment>// 4、将目标布局的根元素添加到parent中  </SPAN>
            }  

            mInflatedViewRef = <SPAN class=hljs-keyword>new</SPAN> WeakReference&lt;View&gt;(view);  

            <SPAN class=hljs-keyword>if</SPAN> (mInflateListener != <SPAN class=hljs-keyword>null</SPAN>) {  
                mInflateListener.onInflate(<SPAN class=hljs-keyword>this</SPAN>, view);  
            }  

            <SPAN class=hljs-keyword>return</SPAN> view;  
        } <SPAN class=hljs-keyword>else</SPAN> {  
            <SPAN class=hljs-keyword>throw</SPAN> <SPAN class=hljs-keyword>new</SPAN> IllegalArgumentException(<SPAN class=hljs-string>"ViewStub must have a valid layoutResource"</SPAN>);  
        }  
    } <SPAN class=hljs-keyword>else</SPAN> {  
        <SPAN class=hljs-keyword>throw</SPAN> <SPAN class=hljs-keyword>new</SPAN> IllegalStateException(<SPAN class=hljs-string>"ViewStub must have a non-null ViewGroup viewParent"</SPAN>);  
    }  
}</CODE><UL class=pre-numbering style="ZOOM: 1"><LI>1</LI></UL></PRE>

可以看到,其实最终加载目标布局的还是inflate()函数,在该函数中将加载目标布局,获取到根元素后,如果mInflatedId不为NO_ID则把mInflatedId设置为根元素的id,这也是为什么我们在获取评论ListView时会使用findViewById(R.id.stub_comm_lv)来获取,其中的stub_comm_lv就是ViewStub的inflatedId。当然如果你没有设置inflatedId的话还是可以通过评论列表的id来获取的,例如findViewById(R.id.my_comm_lv)。然后就是ViewStub从parent中移除、把目标布局的根元素添加到parent中。最后会把目标布局的根元素返回,因此我们在调用inflate()函数时可以直接获得根元素,省掉了findViewById的过程。
还有一种方式加载目标布局的就是直接调用ViewStub的inflate()方法,示例如下 :

public class MainActivity extends Activity {  

<SPAN class=hljs-comment>// 把commLv2设置为类的成员变量  </SPAN>
ListView commLv2 = <SPAN class=hljs-keyword>null</SPAN>;  
<SPAN class=hljs-comment>//  </SPAN>
<SPAN class=hljs-keyword>public</SPAN> <SPAN class=hljs-keyword>void</SPAN> <SPAN class=hljs-title>onCreate</SPAN>(Bundle b){  
    <SPAN class=hljs-comment>// main.xml中包含上面的ViewStub  </SPAN>
    setContentView(R.layout.main);  

    <SPAN class=hljs-comment>// 方式二  </SPAN>
    ViewStub listStub2 = (ViewStub) findViewById(R.id.stub_import) ;  
    <SPAN class=hljs-comment>// 成员变量commLv2为空则代表未加载  </SPAN>
    <SPAN class=hljs-keyword>if</SPAN> ( commLv2 == <SPAN class=hljs-keyword>null</SPAN> ) {  
    <SPAN class=hljs-comment>// 加载评论列表布局, 并且获取评论ListView,inflate函数直接返回ListView对象  </SPAN>
      commLv2 = (ListView)listStub2.inflate();  
    } <SPAN class=hljs-keyword>else</SPAN> {  
    <SPAN class=hljs-comment>// ViewStub已经加载  </SPAN>
    }  

}  

}

  • 1


注意事项



  1. 判断是否已经加载过, 如果通过setVisibility来加载,那么通过判断可见性即可;如果通过inflate()来加载是不可以通过判断可见性来处理的,而需要使用方式2来进行判断。
  2. findViewById的问题,注意ViewStub中是否设置了inflatedId,如果设置了则需要通过inflatedId来查找目标布局的根元素。

三、Merge


首先我们看官方的说明:
The tag helps eliminate redundant view groups in your view hierarchy when including one layout within another. For example, if your main layout is a vertical LinearLayout in which two consecutive views can be re-used in multiple layouts, then the re-usable layout in which you place the two views requires its own root view. However, using another LinearLayout as the root for the re-usable layout would result in a vertical LinearLayout inside a vertical LinearLayout. The nested LinearLayout serves no real purpose other than to slow down your UI performance.


其实就是减少在include布局文件时的层级。标签是这几个标签中最让我费解的,大家可能想不到,标签竟然会是一个Activity,里面有一个LinearLayout对象。

/**  

 * Exercise <merge /> tag in XML files.  

 */ 

public class Merge extends Activity { 

    private LinearLayout mLayout;  

<SPAN class=hljs-annotation>@Override</SPAN>  
<SPAN class=hljs-keyword>protected</SPAN> <SPAN class=hljs-keyword>void</SPAN> <SPAN class=hljs-title>onCreate</SPAN>(Bundle icicle) {  
    <SPAN class=hljs-keyword>super</SPAN>.onCreate(icicle);  

    mLayout = <SPAN class=hljs-keyword>new</SPAN> LinearLayout(<SPAN class=hljs-keyword>this</SPAN>);  
    mLayout.setOrientation(LinearLayout.VERTICAL);  
    LayoutInflater.from(<SPAN class=hljs-keyword>this</SPAN>).inflate(R.layout.merge_tag, mLayout);  

    setContentView(mLayout);  
}  

<SPAN class=hljs-keyword>public</SPAN> ViewGroup <SPAN class=hljs-title>getLayout</SPAN>() {  
    <SPAN class=hljs-keyword>return</SPAN> mLayout;  
}  

}

  • 1


使用merge来组织子元素可以减少布局的层级。例如我们在复用一个含有多个子控件的布局时,肯定需要一个ViewGroup来管理,例如这样 :

<FrameLayout xmlns:android=http://schemas.android.com/apk/res/android 

    android:layout_width=“fill_parent” 

    android:layout_height=“fill_parent”>  

<SPAN class=hljs-tag>&lt;<SPAN class=hljs-title>ImageView</SPAN>    
    <SPAN class=hljs-attribute>android:layout_width</SPAN>=<SPAN class=hljs-value>"fill_parent"</SPAN>   
    <SPAN class=hljs-attribute>android:layout_height</SPAN>=<SPAN class=hljs-value>"fill_parent"</SPAN>   

    <SPAN class=hljs-attribute>android:scaleType</SPAN>=<SPAN class=hljs-value>"center"</SPAN>  
    <SPAN class=hljs-attribute>android:src</SPAN>=<SPAN class=hljs-value>"@drawable/golden_gate"</SPAN> /&gt;</SPAN>  

<SPAN class=hljs-tag>&lt;<SPAN class=hljs-title>TextView</SPAN>  
    <SPAN class=hljs-attribute>android:layout_width</SPAN>=<SPAN class=hljs-value>"wrap_content"</SPAN>   
    <SPAN class=hljs-attribute>android:layout_height</SPAN>=<SPAN class=hljs-value>"wrap_content"</SPAN>   
    <SPAN class=hljs-attribute>android:layout_marginBottom</SPAN>=<SPAN class=hljs-value>"20dip"</SPAN>  
    <SPAN class=hljs-attribute>android:layout_gravity</SPAN>=<SPAN class=hljs-value>"center_horizontal|bottom"</SPAN>  

    <SPAN class=hljs-attribute>android:padding</SPAN>=<SPAN class=hljs-value>"12dip"</SPAN>  

    <SPAN class=hljs-attribute>android:background</SPAN>=<SPAN class=hljs-value>"#AA000000"</SPAN>  
    <SPAN class=hljs-attribute>android:textColor</SPAN>=<SPAN class=hljs-value>"#ffffffff"</SPAN>  

    <SPAN class=hljs-attribute>android:text</SPAN>=<SPAN class=hljs-value>"Golden Gate"</SPAN> /&gt;</SPAN>  

</FrameLayout>

  • 1


将该布局通过include引入时就会多引入了一个FrameLayout层级,此时结构如下 :


Android布局优化之ViewStub、include、merge使用与源码分析


使用merge标签就会消除上图中蓝色的FrameLayout层级。示例如下 :

<merge xmlns:android=http://schemas.android.com/apk/res/android>  

<SPAN class=hljs-tag>&lt;<SPAN class=hljs-title>ImageView</SPAN>    
    <SPAN class=hljs-attribute>android:layout_width</SPAN>=<SPAN class=hljs-value>"fill_parent"</SPAN>   
    <SPAN class=hljs-attribute>android:layout_height</SPAN>=<SPAN class=hljs-value>"fill_parent"</SPAN>   

    <SPAN class=hljs-attribute>android:scaleType</SPAN>=<SPAN class=hljs-value>"center"</SPAN>  
    <SPAN class=hljs-attribute>android:src</SPAN>=<SPAN class=hljs-value>"@drawable/golden_gate"</SPAN> /&gt;</SPAN>  

<SPAN class=hljs-tag>&lt;<SPAN class=hljs-title>TextView</SPAN>  
    <SPAN class=hljs-attribute>android:layout_width</SPAN>=<SPAN class=hljs-value>"wrap_content"</SPAN>   
    <SPAN class=hljs-attribute>android:layout_height</SPAN>=<SPAN class=hljs-value>"wrap_content"</SPAN>   
    <SPAN class=hljs-attribute>android:layout_marginBottom</SPAN>=<SPAN class=hljs-value>"20dip"</SPAN>  
    <SPAN class=hljs-attribute>android:layout_gravity</SPAN>=<SPAN class=hljs-value>"center_horizontal|bottom"</SPAN>  

    <SPAN class=hljs-attribute>android:padding</SPAN>=<SPAN class=hljs-value>"12dip"</SPAN>  

    <SPAN class=hljs-attribute>android:background</SPAN>=<SPAN class=hljs-value>"#AA000000"</SPAN>  
    <SPAN class=hljs-attribute>android:textColor</SPAN>=<SPAN class=hljs-value>"#ffffffff"</SPAN>  

    <SPAN class=hljs-attribute>android:text</SPAN>=<SPAN class=hljs-value>"Golden Gate"</SPAN> /&gt;</SPAN>  

</merge>

  • 1


效果图如下 :
Android布局优化之ViewStub、include、merge使用与源码分析


那么它是如何实现的呢,我们还是看源码吧。相关的源码也是在LayoutInflater的inflate()函数中。

public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) { 

       synchronized (mConstructorArgs) { 

           final AttributeSet attrs = Xml.asAttributeSet(parser); 

           Context lastContext = (Context)mConstructorArgs[0]; 

           mConstructorArgs[0] = mContext; 

           View result = root;  

       <SPAN class=hljs-keyword>try</SPAN> {  
           <SPAN class=hljs-comment>// Look for the root node.  </SPAN>
           <SPAN class=hljs-keyword>int</SPAN> type;  
           <SPAN class=hljs-keyword>while</SPAN> ((type = parser.next()) != XmlPullParser.START_TAG &amp;&amp;  
                   type != XmlPullParser.END_DOCUMENT) {  
               <SPAN class=hljs-comment>// Empty  </SPAN>
           }  

           <SPAN class=hljs-keyword>if</SPAN> (type != XmlPullParser.START_TAG) {  
               <SPAN class=hljs-keyword>throw</SPAN> <SPAN class=hljs-keyword>new</SPAN> InflateException(parser.getPositionDescription()  
                       + <SPAN class=hljs-string>": No start tag found!"</SPAN>);  
           }  

           <SPAN class=hljs-keyword>final</SPAN> String name = parser.getName();  

           <SPAN class=hljs-comment>// m如果是erge标签,那么调用rInflate进行解析  </SPAN>
           <SPAN class=hljs-keyword>if</SPAN> (TAG_MERGE.equals(name)) {  
               <SPAN class=hljs-keyword>if</SPAN> (root == <SPAN class=hljs-keyword>null</SPAN> || !attachToRoot) {  
                   <SPAN class=hljs-keyword>throw</SPAN> <SPAN class=hljs-keyword>new</SPAN> InflateException(<SPAN class=hljs-string>"&lt;merge /&gt; can be used only with a valid "</SPAN>  
                           + <SPAN class=hljs-string>"ViewGroup root and attachToRoot=true"</SPAN>);  
               }  
               <SPAN class=hljs-comment>// 解析merge标签  </SPAN>
               rInflate(parser, root, attrs, <SPAN class=hljs-keyword>false</SPAN>);  
           } <SPAN class=hljs-keyword>else</SPAN> {  
              <SPAN class=hljs-comment>// 代码省略  </SPAN>
           }  

       } <SPAN class=hljs-keyword>catch</SPAN> (XmlPullParserException e) {  
           <SPAN class=hljs-comment>// 代码省略  </SPAN>
       }   

       <SPAN class=hljs-keyword>return</SPAN> result;  
   }  

}

  <SPAN class=hljs-keyword>void</SPAN> rInflate(XmlPullParser parser, View parent, <SPAN class=hljs-keyword>final</SPAN> AttributeSet attrs,  
       <SPAN class=hljs-keyword>boolean</SPAN> finishInflate) <SPAN class=hljs-keyword>throws</SPAN> XmlPullParserException, IOException {  

   <SPAN class=hljs-keyword>final</SPAN> <SPAN class=hljs-keyword>int</SPAN> depth = parser.getDepth();  
   <SPAN class=hljs-keyword>int</SPAN> type;  

   <SPAN class=hljs-keyword>while</SPAN> (((type = parser.next()) != XmlPullParser.END_TAG ||  
           parser.getDepth() &gt; depth) &amp;&amp; type != XmlPullParser.END_DOCUMENT) {  

       <SPAN class=hljs-keyword>if</SPAN> (type != XmlPullParser.START_TAG) {  
           <SPAN class=hljs-keyword>continue</SPAN>;  
       }  

       <SPAN class=hljs-keyword>final</SPAN> String name = parser.getName();  

       <SPAN class=hljs-keyword>if</SPAN> (TAG_REQUEST_FOCUS.equals(name)) {  
           parseRequestFocus(parser, parent);  
       } <SPAN class=hljs-keyword>else</SPAN> <SPAN class=hljs-keyword>if</SPAN> (TAG_INCLUDE.equals(name)) {  
            <SPAN class=hljs-comment>// 代码省略 </SPAN>
           parseInclude(parser, parent, attrs);  
       } <SPAN class=hljs-keyword>else</SPAN> <SPAN class=hljs-keyword>if</SPAN> (TAG_MERGE.equals(name)) {  
           <SPAN class=hljs-keyword>throw</SPAN> <SPAN class=hljs-keyword>new</SPAN> InflateException(<SPAN class=hljs-string>"&lt;merge /&gt; must be the root element"</SPAN>);  
       } <SPAN class=hljs-keyword>else</SPAN> <SPAN class=hljs-keyword>if</SPAN> (TAG_1995.equals(name)) {  
           <SPAN class=hljs-keyword>final</SPAN> View view = <SPAN class=hljs-keyword>new</SPAN> BlinkLayout(mContext, attrs);  
           <SPAN class=hljs-keyword>final</SPAN> ViewGroup viewGroup = (ViewGroup) parent;  
           <SPAN class=hljs-keyword>final</SPAN> ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);  
           rInflate(parser, view, attrs, <SPAN class=hljs-keyword>true</SPAN>);  
           viewGroup.addView(view, params);                  
       } <SPAN class=hljs-keyword>else</SPAN> { <SPAN class=hljs-comment>// 我们的例子会进入这里  </SPAN>
           <SPAN class=hljs-keyword>final</SPAN> View view = createViewFromTag(parent, name, attrs);  
           <SPAN class=hljs-comment>// 获取merge标签的parent  </SPAN>
           <SPAN class=hljs-keyword>final</SPAN> ViewGroup viewGroup = (ViewGroup) parent;  
           <SPAN class=hljs-comment>// 获取布局参数  </SPAN>
           <SPAN class=hljs-keyword>final</SPAN> ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);  
           <SPAN class=hljs-comment>// 递归解析每个子元素  </SPAN>
           rInflate(parser, view, attrs, <SPAN class=hljs-keyword>true</SPAN>);  
           <SPAN class=hljs-comment>// 将子元素直接添加到merge标签的parent view中  </SPAN>
           viewGroup.addView(view, params);  
       }  
   }  

   <SPAN class=hljs-keyword>if</SPAN> (finishInflate) parent.onFinishInflate();  

}

  • 1


其实就是如果是merge标签,那么直接将其中的子元素添加到merge标签parent中,这样就保证了不会引入额外的层级。


在开发过程中,我们一定要尽量去深究一些常用技术点的本质,这样才能避免出了问题不知如何解决的窘境。追根究底才能知道为什么是这样,也是自我成长的必经之路。



$(function () {
$('pre.prettyprint code').each(function () {
var lines = $(this).text().split('\n').length;
var $numbering = $('
    ').addClass('pre-numbering').hide();
    $(this).addClass('has-numbering').parent().append($numbering);
    for (i = 1; i <= lines; i++) {
    $numbering.append($('
  • ').text(i));
    };
    $numbering.fadeIn(1700);
    });
    });