LayoutInflater#inflate()方法詳解

1.方法原型及參數的意義

public View inflate(@LayoutRes int resource, 
                        @Nullable ViewGroup root, 
                            boolean attachToRoot)  

初次接觸這個方法還是在1年多以前使用RecycleView的Adapter的時候,當時怎麼也理解不了這是在幹嘛,後來勉強知道這個方法主要是用於把編寫好的XML轉化爲能在屏幕上顯示的View,再後來知道了各個參數的意義,但是沒深究過原因。恰逢最近在看源碼,就結合方法的源碼來分析一下各個參數的不同取值的效果。
三個參數中的第一個很好理解,第二個和第三個如果就初次接觸的話還是會有點懵逼的。

resource:需要轉換成View的佈局文件。
root: 可選的參數,表示爲resource提供一個根佈局,使得他的寬高屬性生效。
attachToRoot:是否將resource的View加入到root中,然後返回。

2.舉例探究參數不同取值的情形

首先,創建一個Activity和他對應的佈局文件,並且新建一個待加載的佈局文件layout_to_be_added.xml。

TestActivity.java

public class TestActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);

    }
}

activity_test.xml

<?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"
    android:id="@+id/ll_activity_test"
    android:background="#8af47a"
    >

</LinearLayout>

layout_to_be_added.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/layout_to_be_add"
    android:background="#dc4444"
    >

    <Button
        android:id="@+id/btn_test"
        android:text="一個Button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true" />
</RelativeLayout>

此時的界面如下圖:
初始的時候

2.1 root != null, attachToRoot == true

在這種情況下,如果想把layout_to_be_added.xml加載到activity_test.xml中,按照常規的思路,可能會寫出這樣的代碼。

public class TestActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);

        LinearLayout activityLayout = (LinearLayout) findViewById(R.id.ll_activity_test);//獲取到activity的ViewGroup

        View layoutToBeAdded =
                LayoutInflater.from(this).inflate(R.layout.layout_to_be_added,activityLayout,true);

        activityLayout.addView(layoutToBeAdded);
    }
}

看似沒有一點毛病,但是run一下竟然報錯了:

java.lang.IllegalStateException: The specified child already has a parent.
You must call removeView() on the child's parent first.

報錯的主要原因是子佈局早已有了父佈局,無法添加。

其實這個錯誤發生的原因很好理解,由於root不爲空(指定成了Activity的佈局),而attachToRoot又爲true,這會使得layout_to_be_added.xml對應的View會在inflate方法內部被加載到activityLayout之中,然後返回。既然已經加載進去了,自然不需要再次單獨調用addView方法。
既然如此,將activityLayout.addView(layoutToBeAdded);註釋掉即可,註釋掉後會顯示如下界面:

正常顯示

2.2 root != null, attachToRoot == false

那麼,如果root設置爲activityLayout,attachToRoot爲false又會如何呢?顯然,這時候返回的View就是layout_to_be_added.xml對應的View,最後需要手動調用addView方法添加到Activity中:

public class TestActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);

        LinearLayout activityLayout = (LinearLayout) findViewById(R.id.ll_activity_test);//獲取到activity的ViewGroup

        View layoutToBeAdded =
                LayoutInflater.from(this).inflate(R.layout.layout_to_be_added,activityLayout,false);

        activityLayout.addView(layoutToBeAdded);
    }
}

當然,此時顯示肯定是一切正常:

正常顯示

到了這裏心中難免會有疑惑:明明只需要返回layout_to_be_added.xml對應的View,把root設置爲activityLayout幹啥?可不可以把它設置成其他的ViewGroup?答案是當然可以。
這裏的root的作用僅僅是爲了讓layout_to_be_added.xml設置的佈局參數(如layout_width,layout_height,layout_gravity等)生效,因爲view的onMeasure過程是由父容器傳遞過來的,如果沒有指定父容器,id爲id="@+id/layout_to_be_add"的這個RelativeLayout的佈局參數就會失效,所以必須得指定ViewGroup,當然這裏可以指定任意的ViewGroup,因爲它的作用僅僅是爲了讓View的佈局參數有效,完全可以直接new一個Layout,比如activityLayout是一個LinearLayout,這裏完全就可以這樣寫:

LinearLayout l = new LinearLayout(this);
View layoutToBeAdded =
        LayoutInflater.from(this).inflate(R.layout.layout_to_be_added,l,false);

2.3 root == null, attachToRoot == true /false

結合第二種情況的分析,如果不指定root,那麼layout_to_be_added.xml對應的View的佈局參數將失效,這時候無論attachToRoot設置爲什麼值都會出現如下的顯示效果:
root設置爲null的時候
此時由於Button是有父佈局的,所以Button的佈局參數還是有效的。

3.結合源碼分析不同參數的意義

LayoutInflater.java

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
          //省略無關代碼...
        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);//又調用了另一個重載方法
        } finally {
            parser.close();
        }
    }

很明顯,在try的語句塊裏調用了另一個重載方法:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

            final Context inflaterContext = mContext;
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context) mConstructorArgs[0];
            mConstructorArgs[0] = inflaterContext;

            //默認返回root
            View result = root;

            try {
                // Look for the root node.
                int type;
                    //...

                final String name = parser.getName();
                //...

                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");
                    }

                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    // Temp is the root view that was found in the xml
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                            //...
                        // Create layout params that match root, if supplied
                        //構建ViewGroup.LayoutParams,以便設置給temp
                        params = root.generateLayoutParams(attrs);
                        //attachToRoot爲false,則直接將ViewGroup.LayoutParams設置給temp
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);
                        }
                    }

                        //...

                    //重點看這裏,如果root不爲null,且attachToRoot爲true,則將View添加到root中去
                    // to root. Do that now.
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }

                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.

                    //如果沒有設置root,或者attachToRoot爲false,則直接將View賦值給result,最後返回,由於沒有root,故temp的佈局參數會失效(因爲從上面的代碼可以看出並沒有給temp設置ViewGroup.LayoutParams)
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }

            } catch (XmlPullParserException e) {
                final InflateException ie = new InflateException(e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } catch (Exception e) {
                final InflateException ie = new InflateException(parser.getPositionDescription()
                        + ": " + e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } finally {
                // Don't retain static reference on context.
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;

                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }

            return result;
        }
    }

結合源碼,我也在裏面給出了關鍵步驟的註釋,整個邏輯還是不難理解。
總之,如果沒有設置root,則不會給temp設置正確LayoutParams,這樣一來temp的佈局參數就成了默認的了(相當於沒有)。當然如果將attachToRoot設置爲true,在設置了root的情況下,temp會被直接加入root,然後返回,否則,將會返回沒有佈局參數的temp(這裏就是指layout_to_be_added.xml對應的View)。而如果attachToRoot設置爲了flase,這不會將View加載到root中。

4.不同情形下參數該如何傳值?

這裏的傳值主要以attachToRoot的值分情況討論:

4.1 常見到的情形是attachToRoot==flase的情況:

如果希望單純滴把一個layout轉換成View,attachToRoot就應該爲false,比如:
1.經典的RecyclerView的Adapter的場景:

     @Override
        public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
        {

            View view = LayoutInflater.from(
                    parent.getContext()).inflate(R.layout.item_home, parent,
                    false)
            MyViewHolder holder = new MyViewHolder(view);
            return holder;
        }

這裏如果將attachToRoot設置成true是肯定會報錯的,爲啥?當然是因爲RecyclerView的item的添加與刪除是由Adapter全權負責的,並不需要我們手動操作,控制權不在我們這裏,當然,這一點在其源碼裏也能夠體現:

if (child.getParent() != null) {

            throw new IllegalStateException("The specified child already has a parent. " +

                    "You must call removeView() on the child's parent first.");

        }
}

2.給Fragment設置佈局的時候

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_class_running_ranking, container, false);
    }

以上代碼是ViewPager+Fragment+TabLayout的經典使用場景下的Fragment,這裏Fragment何時添加到容器中,也全靠adapter來處理,不用我們手動操作。

3.在自定義View中用於將某個佈局轉化成View的時候。

4.2 再看看attachToRoot==true的情況:

這種情況下主要是會用到inflate的另一個重載方法:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root)
很顯然這樣用會直接將layout轉換成View,然後加載到我們希望的ViewGoup中,多用於自定義ViewGroup中,比較簡單。

最後,爲了View的能像佈局文件裏描繪的那樣顯示,能傳root的時候還是傳吧~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章