你真懂的ViewStub,include,merge么

2022-06-22 14:35:51 浏览数 (1)

1.include

include就是为了解决重复定义相同布局的问题

代码语言:javascript复制
<?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" >  

    <ImageButton  
        android:id="@ id/back_btn"  
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content"  
        android:src="@drawable/ic_launcher" />  

    <TextView  
        android:id="@ id/title_tv"  
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content"  
        android:layout_centerVertical="true"  
        android:layout_marginLeft="20dp"  
        android:layout_toRightOf="@ id/back_btn"  
        android:gravity="center"  
        android:text="我的title"  
        android:textSize="18sp" />  

</RelativeLayout>  

include布局文件:

代码语言:javascript复制
<?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" >  

    <include  
        android:id="@ id/my_title_ly"  
        android:layout_width="match_parent"  
        android:layout_height="wrap_content"  
        layout="@layout/my_title_layout" />  

    <!-- 代码省略 -->
</LinearLayout>  

这样就可以使用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下的子控件则会抛出空指针。代码如下 :

代码语言:javascript复制
View titleView = findViewById(R.id.my_title_parent_id) ;  
// 此时 titleView 为空,找不到。此时空指针
 TextView titleTextView = (TextView)titleView.findViewById(R.id.title_tv) ;  
titleTextView.setText("new Title");  

看源码: LayoutInflater#rInflate

代码语言:javascript复制
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();
        }
    }

匹配到TAG_INCLUDE,执行parseInclude方法,主要做以下几件事 1.获取属性集,即在include标签中设置的属性 2.解析include的布局的根元素 3.获取include包含布局的布局属性并设置 4.解析根元素下的子view 5. 将include中设置的id设置给根view,因此实际上my_title_layout.xml中的RelativeLayout的id会变成include标签中的id,include不设置id,那么也可以通过relative的找到. 6. 将根view添加到父控件中

2.ViewStub

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

代码语言:javascript复制
<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"> 

my_comment_layout.xml如下:

代码语言:javascript复制
<?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>  

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

代码语言:javascript复制
public class MainActivity extends Activity {  

    public void onCreate(Bundle b){  
        // main.xml中包含上面的ViewStub  
        setContentView(R.layout.main);  

        // 方式1,获取ViewStub,  
        ViewStub listStub = (ViewStub) findViewById(R.id.stub_import);  
        // 加载评论列表布局  
        listStub.setVisibility(View.VISIBLE);  
        // 获取到评论ListView,注意这里是通过ViewStub的inflatedId来获取  
            ListView commLv = findViewById(R.id.stub_comm_lv);  
                if ( listStub.getVisibility() == View.VISIBLE ) {  
                       // 已经加载, 否则还没有加载  
                }  
            }  
       }  

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

代码语言:javascript复制
 private void initialize(Context context) {  
        mContext = context;  
        setVisibility(GONE);// 设置不可见  
        setWillNotDraw(true);// 设置不绘制  
    }  

    @Override  
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
        setMeasuredDimension(0, 0);// 宽高都为0  
    }  

setVisibility中如果还没渲染view会调用inflate()方法渲染

代码语言:javascript复制
 @Override
    @android.view.RemotableViewMethod(asyncImpl = "setVisibilityAsync")
    public void setVisibility(int visibility) {
        if (mInflatedViewRef != null) {
            View view = mInflatedViewRef.get();
            if (view != null) {
                view.setVisibility(visibility);
            } else {
                throw new IllegalStateException("setVisibility called on un-referenced view");
            }
        } else {
            super.setVisibility(visibility);
            if (visibility == VISIBLE || visibility == INVISIBLE) {
                inflate();
            }
        }
    }
代码语言:javascript复制
 public View inflate() {
        final ViewParent viewParent = getParent();

        if (viewParent != null && viewParent instanceof ViewGroup) {
            if (mLayoutResource != 0) {
                final ViewGroup parent = (ViewGroup) viewParent;
                final View view = inflateViewNoAdd(parent);
                replaceSelfWithView(view, parent);

                mInflatedViewRef = new WeakReference<>(view);
                if (mInflateListener != null) {
                    mInflateListener.onInflate(this, view);
                }

                return view;
            } else {
                throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
            }
        } else {
            throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
        }
    }

主要看inflateViewNoAdd

代码语言:javascript复制
  private View inflateViewNoAdd(ViewGroup parent) {
        final LayoutInflater factory;
        if (mInflater != null) {
            factory = mInflater;
        } else {
            factory = LayoutInflater.from(mContext);
        }
        final View view = factory.inflate(mLayoutResource, parent, false);

        if (mInflatedId != NO_ID) {
            view.setId(mInflatedId);
        }
        return view;
    }

这里如果设置了mInflatedId,会把view的id改为mInflatedId

然后是replaceSelfWithView方法

代码语言:javascript复制
  private void replaceSelfWithView(View view, ViewGroup parent) {
        final int index = parent.indexOfChild(this);
        parent.removeViewInLayout(this);

        final ViewGroup.LayoutParams layoutParams = getLayoutParams();
        if (layoutParams != null) {
            parent.addView(view, index, layoutParams);
        } else {
            parent.addView(view, index);
        }
    }

里面会通过父控件把viewstub自己移除,将最终渲染的控件插入会对应的index

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

Merge

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

代码语言:javascript复制
/** 
 * Exercise <merge /> tag in XML files. 
 */  
public class Merge extends Activity {  
    private LinearLayout mLayout;  

    @Override  
    protected void onCreate(Bundle icicle) {  
        super.onCreate(icicle);  

        mLayout = new LinearLayout(this);  
        mLayout.setOrientation(LinearLayout.VERTICAL);  
        LayoutInflater.from(this).inflate(R.layout.merge_tag, mLayout);  

        setContentView(mLayout);  
    }  

    public ViewGroup getLayout() {  
        return mLayout;  
    }  
}  

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

代码语言:javascript复制
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    android:layout_width="fill_parent"  
    android:layout_height="fill_parent">  

    <ImageView    
        android:layout_width="fill_parent"   
        android:layout_height="fill_parent"   

        android:scaleType="center"  
        android:src="@drawable/golden_gate" />  

    <TextView  
        android:layout_width="wrap_content"   
        android:layout_height="wrap_content"   
        android:layout_marginBottom="20dip"  
        android:layout_gravity="center_horizontal|bottom"  

        android:padding="12dip"  

        android:background="#AA000000"  
        android:textColor="#ffffffff"  

        android:text="Golden Gate" />  

</FrameLayout> 

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

代码语言:javascript复制
<merge xmlns:android="http://schemas.android.com/apk/res/android">  

    <ImageView    
        android:layout_width="fill_parent"   
        android:layout_height="fill_parent"   

        android:scaleType="center"  
        android:src="@drawable/golden_gate" />  

    <TextView  
        android:layout_width="wrap_content"   
        android:layout_height="wrap_content"   
        android:layout_marginBottom="20dip"  
        android:layout_gravity="center_horizontal|bottom"  

        android:padding="12dip"  

        android:background="#AA000000"  
        android:textColor="#ffffffff"  

        android:text="Golden Gate" />  

</merge>  
代码语言:javascript复制
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;  

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

               // m如果是erge标签,那么调用rInflate进行解析  
               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");  
                   }  
                   // 解析merge标签  
                   rInflate(parser, root, attrs, false);  
               } else {  
                  // 代码省略  
               }  

           } catch (XmlPullParserException e) {  
               // 代码省略  
           }   

           return result;  
       }  
   }  


      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)) {  
                // 代码省略 
               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);  
               // 获取merge标签的parent  
               final ViewGroup viewGroup = (ViewGroup) parent;  
               // 获取布局参数  
               final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);  
               // 递归解析每个子元素  
               rInflate(parser, view, attrs, true);  
               // 将子元素直接添加到merge标签的parent view中  
               viewGroup.addView(view, params);  
           }  
       }  

       if (finishInflate) parent.onFinishInflate();  
   }  

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

0 人点赞