android中include、merge、ViewStub使用与源码分析

在项目开发中,UI布局是我们都会遇到的问题,如果布局过于复杂,层级过深,不仅会影响阅读性,还会导致性能降低。Android官方给了几个优化的方法include、merge、ViewStub。这里我们我们简单的介绍下使用方法,注意事项,并从源码角度分析他们的好处,注意事项。

Include:
include是我们最常用的标签,它有点像C中的include头文件,我们把一套布局封装起来,等到使用的时候使用include标签引入即可。这样就提高了代码的复用性
不必每次都写一遍。先看下示例代码:
include文件:include_layout.xml

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent" android:layout_height="match_parent"
        android:layout_marginTop="50dp"
        android:id="@+id/my_layout_root_id">
    
        <Button
        android:id="@+id/back_btn"
            android:layout_marginTop="50dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@mipmap/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="include"
        android:textSize="18sp" />
    
    </RelativeLayout>

在MainActivity的布局文件activity_main.xml中引用

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.zhangy.include_merge_viewstub.MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!" />

    <include
        android:id="@+id/my_layout"
        layout="@layout/include_layout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <include
        android:id="@+id/my_merge_layout"
        layout="@layout/merge_layout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <ViewStub
        android:id="@+id/view_stub"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inflatedId="@+id/view_stub_layout"
        android:layout="@layout/viewstub_layout" />
</LinearLayout>

MainActivity

    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    //        View titleView = findViewById(R.id.my_layout_root_id) ;//这样会报错,因为我们重置了layout布局的id
            View titleView = findViewById(R.id.my_layout) ;
            ViewStub viewStub = (ViewStub)findViewById(R.id.view_stub) ;
            TextView titleTextView = (TextView)titleView.findViewById(R.id.title_tv) ;
            titleTextView.setText("yang");
            viewStub.inflate();
            viewStub.setOnInflateListener(new ViewStub.OnInflateListener() {
                @Override
                public void onInflate(ViewStub stub, View inflated) {
    
                }
            });
            viewStub.setVisibility(View.VISIBLE);
        }
    }

include标签使用很简单但是需要注意以下两点:

  1. 这里我们设置了include标签的Id为include_layout,这个id会覆盖include文件:include_layout.xml中根标签的id:my_layout_root_id;所以当用findViewByid(R.id.my_layout_root_id)方法是找不到根View的,如果不加以注意会报空指针异常。
  2. 如果想再include标签中使用android:** 这些属性集,必须先layout_width、layout_height。否则这些属性不生效

接下来我们从源码角度分析这两个注意事项,Activity的setContentView方法最终会调到LayoutInflater的rInflate方法解析xml文件,我们看看rInflate方法

      void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
        //获取xml深度
        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_TAG.equals(name)) {
                parseViewTag(parser, parent, attrs);
            } else if (TAG_INCLUDE.equals(name)) {//如果是include的标签
                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 (finishInflate) {
            parent.onFinishInflate();
        }
    }

这个方法其实就是遍历View树,并添加到根View中,当是include标签时调用parseInclude

    private void parseInclude(XmlPullParser parser, Context context, View parent,
            AttributeSet attrs) throws XmlPullParserException, IOException {
        int type;

        if (parent instanceof ViewGroup) {
           ...
            //获取include中layout
            int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
            if (layout == 0) {
                //include中没有设置layout,抛异常
                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: <include layout=\"@layout/layoutID\" />");
                }
                   ...
            } else {
                //获取layout的xml解析器
                final XmlResourceParser childParser = context.getResources().getLayout(layout);
                try {
                    //获取layout的属性集
                    final AttributeSet childAttrs = Xml.asAttributeSet(childParser);
                   ...
                    final String childName = childParser.getName();
                    if (TAG_MERGE.equals(childName)) {
                    ...//merge标签
                    } else {
                        //得到include文件的根布局
                        final View view = createViewFromTag(parent, childName,
                                context, childAttrs, hasThemeOverride);
                        //得到include文件挂载的父容器
                        final ViewGroup group = (ViewGroup) parent;
                        //得到include标签的属性
                        final TypedArray a = context.obtainStyledAttributes(
                                attrs, R.styleable.Include);
                        //我们在使用include的时设置的Id
                        final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
                        //我们在使用include的时设置的是否显示
                        final int visibility = a.getInt(R.styleable.Include_visibility, -1);
                        ...
                        ViewGroup.LayoutParams params = null;
                        try {
                            //注释1.从我们设置的include标签中获取布局属性,必须先layout_width、layout_height 如果没设置,try catch异常,params为null
                            params = group.generateLayoutParams(attrs);
                        } catch (RuntimeException e) {
                            // Ignore, just fail over to child attrs.
                        }
                        if (params == null) {
                            //从include跟布局标签中获取布局属性
                            params = group.generateLayoutParams(childAttrs);
                        }
                        //设置布局参数。如果include标签中的params!=null则会替换layout根布局的布局参数,让其都失效
                        view.setLayoutParams(params);

                        //解析所有子控件
                        rInflateChildren(childParser, view, childAttrs, true);
                        //注释2.这里就将我们设置的include标签中的Id设置给layout根布局,改变了原有id
                        if (id != View.NO_ID) {
                            view.setId(id);
                        }
                        //设置VISIBLE属性
                        switch (visibility) {
                            case 0:
                                view.setVisibility(View.VISIBLE);
                                break;
                            case 1:
                                view.setVisibility(View.INVISIBLE);
                                break;
                            case 2:
                                view.setVisibility(View.GONE);
                                break;
                        }
                        //将根view添加到父控件中
                        group.addView(view);
                    }
                } finally {
                    childParser.close();
                }
            }
        } else {
            throw new InflateException("<include /> can only be used inside of a ViewGroup");
        }
        LayoutInflater.consumeChildElements(parser);
    }

该方法就是解析include标签,先解析include标签属性,再解析layout布局文件获得一View,如果include的params!=null就覆盖该View的原有的params,如果我们设置了include的id,则覆盖原有的id。然后再解析layout布局的子View。最终将这个view添加到父View parent上。注释1、2处分别说明我们使用时的注意事项原因。

merge:
merge标签可以减少层级布局,它是将merge标签下的子view直接添加到merge标签的parent中,这样就减少了不必要的层级。先看下示例代码
merge布局:


    <merge xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical" android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <Button
            android:id="@+id/back_btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@mipmap/ic_launcher" />
    
        <TextView
            android:id="@+id/title_tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:gravity="center"
            android:text="我的title"
            android:textSize="18sp" />
    
    </merge>

merge标签使用见activity_main

merge标签的解析都会走到rInflate方法中

      void rInflate(XmlPullParser parser, View parent, Context context,
            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_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);//将merge标签下的子View直接添加到merge父容器中
            }
        }

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

以merge标签为跟标签都会调用viewGroup.addView(view, params)将其子View直接添加到merge父容器中,减少一层布局
需要注意的是,使用merge标签时LayoutInflate.inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
root!=null attachToRoot == true;否则会抛InflateException异常

ViewStub :
ViewStub标签最大的优点是当你需要时才会加载,使用他并不会影响UI初始化时的性能。各种不常用的布局想进度条、显示错误消息等可以使用ViewStub标签,以减少内存使用量,加快渲染速度。ViewStub是一个不可见的,大小为0的View,相当于一个“占位控件”。然后当ViewStub被设置为可见的时或调用了ViewStub.inflate()的时候,ViewStub所指向的布局就会被inflate实例化,且此布局文件直接将当前ViewStub替换掉,然后ViewStub的布局属性(layout_margin***、layout_width等)都会传给它所指向的布局。这样,就可以使用ViewStub在运行时动态显示布局,节约内存资源。先看示例代码:
viewstub_layout.xml:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical" android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <Button
            android:layout_marginTop="50dp"
            android:id="@+id/back_btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@mipmap/ic_launcher" />
    
        <TextView
            android:id="@+id/title_tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:gravity="center"
            android:text="merge"
            android:textSize="18sp" />
    
    </LinearLayout>

使用方法见activity_main.xml:

显示加载的布局有两种方法调用inflate方法,或者设置VISIBLE即可 见MainActivity

ViewStub重新了setVisibility方法

        public void setVisibility(int visibility) {
            if (mInflatedViewRef != null) {//如果不是第一次,跟正常的View一样
                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();//最后还是调用了inflate方法加载布局
                }
            }
        }

我们来看看ViewStub的inflate方法

    public View inflate() {
        final ViewParent viewParent = getParent();

        if (viewParent != null && viewParent instanceof ViewGroup) {
            if (mLayoutResource != 0) {
                final ViewGroup parent = (ViewGroup) viewParent;
                final LayoutInflater factory;
                if (mInflater != null) {
                    factory = mInflater;
                } else {
                    factory = LayoutInflater.from(mContext);
                }
                //mLayoutResource就是我们在ViewStub标签中的layout布局
                final View view = factory.inflate(mLayoutResource, parent, false);
                //mInflatedId就是我们在ViewStub标签中的inflateId,如果我们设置了,则设置给view
                if (mInflatedId != NO_ID) {
                    view.setId(mInflatedId);
                }
                //从父视图中查找ViewStub
                final int index = parent.indexOfChild(this);
                //注释1.把当前ViewStub对象从父视图中移除了
                parent.removeViewInLayout(this);

                final ViewGroup.LayoutParams layoutParams = getLayoutParams();
                //注释2.得到ViewStub的LayoutParams布局参数对象,如果存在就把它赋给被inflate的布局对象,不存在就按脚标添加
                if (layoutParams != null) {
                    parent.addView(view, index, layoutParams);
                } else {
                    parent.addView(view, index);
                }

                mInflatedViewRef = new WeakReference<View>(view);

                if (mInflateListener != null) {
                    mInflateListener.onInflate(this, view);//可以设置监听器在加载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");
        }
    }

从注释1我们可以看出不能再次调用inflate方法,因为已经移除了ViewStub对象,得到的viewParent就为null,此时判断时候就会走else抛出一个IllegalStateException异常:ViewStub must have a non-null ViewGroup viewParent。
使用ViewStub要注意,ViewStub只是个“占位符”,达到延迟加载的效果,当它指向的layout被加载后,它就会被父容器移除,但是从注释2看到布局文件的layout params是以ViewStub为准,其他布局属性是以布局文件自身为准。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容