android中include、merge、ViewStub使用與源碼分析

在項(xiàng)目開發(fā)中,UI布局是我們都會(huì)遇到的問題,如果布局過于復(fù)雜,層級(jí)過深,不僅會(huì)影響閱讀性,還會(huì)導(dǎo)致性能降低。Android官方給了幾個(gè)優(yōu)化的方法include、merge、ViewStub。這里我們我們簡單的介紹下使用方法,注意事項(xiàng),并從源碼角度分析他們的好處,注意事項(xiàng)。

Include:
include是我們最常用的標(biāo)簽,它有點(diǎn)像C中的include頭文件,我們把一套布局封裝起來,等到使用的時(shí)候使用include標(biāo)簽引入即可。這樣就提高了代碼的復(fù)用性
不必每次都寫一遍。先看下示例代碼:
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) ;//這樣會(huì)報(bào)錯(cuò),因?yàn)槲覀冎刂昧薼ayout布局的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標(biāo)簽使用很簡單但是需要注意以下兩點(diǎn):

  1. 這里我們?cè)O(shè)置了include標(biāo)簽的Id為include_layout,這個(gè)id會(huì)覆蓋include文件:include_layout.xml中根標(biāo)簽的id:my_layout_root_id;所以當(dāng)用findViewByid(R.id.my_layout_root_id)方法是找不到根View的,如果不加以注意會(huì)報(bào)空指針異常。
  2. 如果想再include標(biāo)簽中使用android:** 這些屬性集,必須先layout_width、layout_height。否則這些屬性不生效

接下來我們從源碼角度分析這兩個(gè)注意事項(xiàng),Activity的setContentView方法最終會(huì)調(diào)到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;
        //迭代解析各個(gè)標(biāo)簽
        while (((type = parser.next()) != XmlPullParser.END_TAG ||
                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

            if (type != XmlPullParser.START_TAG) {
                continue;
            }
            //獲取標(biāo)簽名
            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的標(biāo)簽
                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();
        }
    }

這個(gè)方法其實(shí)就是遍歷View樹,并添加到根View中,當(dāng)是include標(biāo)簽時(shí)調(diào)用parseInclude

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

        if (parent instanceof ViewGroup) {
           ...
            //獲取include中l(wèi)ayout
            int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
            if (layout == 0) {
                //include中沒有設(shè)置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標(biāo)簽
                    } else {
                        //得到include文件的根布局
                        final View view = createViewFromTag(parent, childName,
                                context, childAttrs, hasThemeOverride);
                        //得到include文件掛載的父容器
                        final ViewGroup group = (ViewGroup) parent;
                        //得到include標(biāo)簽的屬性
                        final TypedArray a = context.obtainStyledAttributes(
                                attrs, R.styleable.Include);
                        //我們?cè)谑褂胕nclude的時(shí)設(shè)置的Id
                        final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
                        //我們?cè)谑褂胕nclude的時(shí)設(shè)置的是否顯示
                        final int visibility = a.getInt(R.styleable.Include_visibility, -1);
                        ...
                        ViewGroup.LayoutParams params = null;
                        try {
                            //注釋1.從我們?cè)O(shè)置的include標(biāo)簽中獲取布局屬性,必須先layout_width、layout_height 如果沒設(shè)置,try catch異常,params為null
                            params = group.generateLayoutParams(attrs);
                        } catch (RuntimeException e) {
                            // Ignore, just fail over to child attrs.
                        }
                        if (params == null) {
                            //從include跟布局標(biāo)簽中獲取布局屬性
                            params = group.generateLayoutParams(childAttrs);
                        }
                        //設(shè)置布局參數(shù)。如果include標(biāo)簽中的params!=null則會(huì)替換layout根布局的布局參數(shù),讓其都失效
                        view.setLayoutParams(params);

                        //解析所有子控件
                        rInflateChildren(childParser, view, childAttrs, true);
                        //注釋2.這里就將我們?cè)O(shè)置的include標(biāo)簽中的Id設(shè)置給layout根布局,改變了原有id
                        if (id != View.NO_ID) {
                            view.setId(id);
                        }
                        //設(shè)置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標(biāo)簽,先解析include標(biāo)簽屬性,再解析layout布局文件獲得一View,如果include的params!=null就覆蓋該View的原有的params,如果我們?cè)O(shè)置了include的id,則覆蓋原有的id。然后再解析layout布局的子View。最終將這個(gè)view添加到父View parent上。注釋1、2處分別說明我們使用時(shí)的注意事項(xiàng)原因。

merge:
merge標(biāo)簽可以減少層級(jí)布局,它是將merge標(biāo)簽下的子view直接添加到merge標(biāo)簽的parent中,這樣就減少了不必要的層級(jí)。先看下示例代碼
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標(biāo)簽使用見activity_main

merge標(biāo)簽的解析都會(huì)走到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標(biāo)簽下的子View直接添加到merge父容器中
            }
        }

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

以merge標(biāo)簽為跟標(biāo)簽都會(huì)調(diào)用viewGroup.addView(view, params)將其子View直接添加到merge父容器中,減少一層布局
需要注意的是,使用merge標(biāo)簽時(shí)LayoutInflate.inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
root!=null attachToRoot == true;否則會(huì)拋InflateException異常

ViewStub :
ViewStub標(biāo)簽最大的優(yōu)點(diǎn)是當(dāng)你需要時(shí)才會(huì)加載,使用他并不會(huì)影響UI初始化時(shí)的性能。各種不常用的布局想進(jìn)度條、顯示錯(cuò)誤消息等可以使用ViewStub標(biāo)簽,以減少內(nèi)存使用量,加快渲染速度。ViewStub是一個(gè)不可見的,大小為0的View,相當(dāng)于一個(gè)“占位控件”。然后當(dāng)ViewStub被設(shè)置為可見的時(shí)或調(diào)用了ViewStub.inflate()的時(shí)候,ViewStub所指向的布局就會(huì)被inflate實(shí)例化,且此布局文件直接將當(dāng)前ViewStub替換掉,然后ViewStub的布局屬性(layout_margin***、layout_width等)都會(huì)傳給它所指向的布局。這樣,就可以使用ViewStub在運(yùn)行時(shí)動(dòng)態(tài)顯示布局,節(jié)約內(nèi)存資源。先看示例代碼:
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:

顯示加載的布局有兩種方法調(diào)用inflate方法,或者設(shè)置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();//最后還是調(diào)用了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就是我們?cè)赩iewStub標(biāo)簽中的layout布局
                final View view = factory.inflate(mLayoutResource, parent, false);
                //mInflatedId就是我們?cè)赩iewStub標(biāo)簽中的inflateId,如果我們?cè)O(shè)置了,則設(shè)置給view
                if (mInflatedId != NO_ID) {
                    view.setId(mInflatedId);
                }
                //從父視圖中查找ViewStub
                final int index = parent.indexOfChild(this);
                //注釋1.把當(dāng)前ViewStub對(duì)象從父視圖中移除了
                parent.removeViewInLayout(this);

                final ViewGroup.LayoutParams layoutParams = getLayoutParams();
                //注釋2.得到ViewStub的LayoutParams布局參數(shù)對(duì)象,如果存在就把它賦給被inflate的布局對(duì)象,不存在就按腳標(biāo)添加
                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);//可以設(shè)置監(jiān)聽器在加載View前回調(diào)
                }

                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我們可以看出不能再次調(diào)用inflate方法,因?yàn)橐呀?jīng)移除了ViewStub對(duì)象,得到的viewParent就為null,此時(shí)判斷時(shí)候就會(huì)走else拋出一個(gè)IllegalStateException異常:ViewStub must have a non-null ViewGroup viewParent。
使用ViewStub要注意,ViewStub只是個(gè)“占位符”,達(dá)到延遲加載的效果,當(dāng)它指向的layout被加載后,它就會(huì)被父容器移除,但是從注釋2看到布局文件的layout params是以ViewStub為準(zhǔn),其他布局屬性是以布局文件自身為準(zhǔn)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容