1. 程式人生 > >Android LayoutInflater.inflate的使用及原始碼分析

Android LayoutInflater.inflate的使用及原始碼分析

在實際開發中我們常常需要inflate要給佈局然後新增到某個佈局容器裡面去, 要把xml佈局檔案轉成一個View物件 需要使用LayoutInflater.inflate方法. 在開發中常常使用如下幾種方式:

inflater.inflate(layoutId, null);
inflater.inflate(layoutId, root,false);
inflater.inflate(layoutId, root,true);

特別是初學者搞不靈清這種方式有什麼區別, 下面就一一來看看他們到底有什麼不同.

inflater.inflate(layoutId, null)

現在我有一個MainActivity, 我想往這個佈局裡add一個佈局, 該佈局如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginLeft="20dp"
    android:background="#50000000"
    android:orientation="vertical">
<Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="inflate01" android:textAllCaps="false" android:textSize="20sp" /> </LinearLayout>

該佈局非常簡單, 外層是一個LinearLayout, 背景顏色為#50000000, 大小都是wrap_content, 距左20dp.

然後在MainActivity執行新增操作:

LayoutInflater inflater = LayoutInflater.from(this);
View view = inflater.inflate(R.layout.inflater01, null);
root.addView(view);

新增View後, 發現並不是我們想要的, 如下圖所示:

這裡寫圖片描述

從上圖可以看出, 被新增的View, 它的寬度並不是wrap_content而是佔滿了螢幕, 還有設定的margin_left也沒有效果. 也就是受我們設定的這些都失效了.

那只有看看原始碼一探究竟了.

inflater.inflate(R.layout.inflater01, null) 方法裡呼叫了inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot), 在該方法又呼叫了 inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) 這個方法是最核心的方法:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {

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

        try {
            // Look for the root node.
            int type;
            while ((type = parser.next()) != XmlPullParser.START_TAG &&
                    type != XmlPullParser.END_DOCUMENT) {
                // Empty
            }

            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
                //把xml裡的相關屬性轉成View物件
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                ViewGroup.LayoutParams params = null;

                //如果引數root不為空
                if (root != null) {
                    // Create layout params that match root, if supplied
                    //根據在XML檔案中的設定,生成LayoutParamsView的LayoutParameter用來適應root佈局
                    //root佈局的根節點是哪個容器(如LinearLayout)那這個params就是哪個容器佈局的params(LinearLayout.LayoutParams)
                    params = root.generateLayoutParams(attrs);
                    //如果不新增到跟佈局, 則設定剛剛生成的params
                    if (!attachToRoot) {
                        // Set the layout params for temp if we are not
                        // attaching. (If we are, we use addView, below)
                        temp.setLayoutParams(params);
                    }
                }

                // Inflate all children under temp against its context.
                //初始化View所有的子控制元件
                rInflateChildren(parser, temp, attrs, true);

                // We are supposed to attach all the views we found (int temp)
                // to root. Do that now.
                if (root != null && attachToRoot) {
                    //把View新增到root佈局中
                    root.addView(temp, params);
                }

                // Decide whether to return the root that was passed in or the
                // top view found in xml.
                if (root == null || !attachToRoot) {
                    result = temp;
                }
            }

        } catch (XmlPullParserException e) {
            //igore code...
        } catch (Exception e) {
            //igore code...
        } finally {
            //igore code...
        }

        return result;
    }
}

上面的程式碼比較好理解, 主要的地方都加了註釋.

從上面的程式碼來看 inflater.inflate(layoutId, null) 方法, 因為root我們傳了null,所以 attachToRoot=false.

根據原始碼 inflater.inflate(layoutId, null) 方法執行主要流程如下:

1, 把xml裡的相關屬性轉成View物件

final View temp = createViewFromTag(root, name, inflaterContext, attrs);

2, 最後把 View temp返回了.

所以這個View是沒有 LayoutParams 物件的.

不信的話可以列印一下就知道了:

View view = inflater.inflate(R.layout.inflater01, null);
ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
if (layoutParams == null) {
    Log.e("MainActivity", "layoutParams is null");
} else {
    Log.e("MainActivity", "layoutParams width:height" + layoutParams.width + ":" + layoutParams.height);
}

毫無疑問輸出null.

一個View沒有 LayoutParams 是沒辦法展示到螢幕上的.

現在只有看看 root.addView(view) 方法了:

public void addView(View child) {
    addView(child, -1);
}

public void addView(View child, int index) {
    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }
    LayoutParams params = child.getLayoutParams();
    if (params == null) {
        params = generateDefaultLayoutParams();
        if (params == null) {
            throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
        }
    }
    addView(child, index, params);
}

從上面可以看出, 如果 child 的 LayoutParams為null, root會生成一個預設的 LayoutParams

所以看看 generateDefaultLayoutParams() 方法是什麼樣的? 因為我們這個root是LinearLayout, 所以要到 LinearLayout 的 generateDefaultLayoutParams() 裡去看,而不是ViewGroup :

@Override
protected LayoutParams generateDefaultLayoutParams() {
    if (mOrientation == HORIZONTAL) {
        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    } else if (mOrientation == VERTICAL) {
        return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
    }
    return null;
}

我們的root是LinearLayout方向為 VERTICAL 所以 LayoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); 這就解釋了我們是上面執行的效果了: 明明設定的寬和高都是 WRAP_CONTENT, 最後變成了寬度為 MATCH_PARENT, 高度為WRAP_CONTENT.

viewGroup.addView(view, LayoutParams); 
相當於如下兩行程式碼 :
view.setLayoutParams(LayoutParams);
viewGroup.addView(view)

有人可能會問 下面三個屬性失效了,那 android:background="#50000000"又起作用呢?

android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"

因為layout_width,layout_height,layout_marginLeft,layout_marginLeft都屬於LayoutParams, 但是background屬性不屬於此範圍, 同時所有的標籤屬性都會存在AttributeSet物件裡.

inflater.inflate(layoutId, root,false);

在MainActivity的新增View的方法改成如下形式 :

/**
 * inflater.inflate(layoutId, root, false);
 */
private void add02() {
    View view = inflater.inflate(R.layout.inflater01, root, false);
    ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
    if (layoutParams == null) {
        Log.e("MainActivity", "layoutParams is null");
    } else {
        Log.e("MainActivity", "layoutParams width:height" + layoutParams.width + ":" + layoutParams.height);
    }
    root.addView(view);
}

佈局檔案還是那個佈局檔案, 不同的只是把inflate方法的引數該了下:

inflater.inflate(R.layout.inflater01, root, false);

效果如下圖所示 :

這裡寫圖片描述

這個才是我們要的效果, 寬高和左邊距都是我們要的效果.

根據原始碼 inflater.inflate(R.layout.inflater01, root, false);方法執行流程流程如下:

1, 把xml裡的相關屬性轉成View物件

final View temp = createViewFromTag(root, name, inflaterContext, attrs);

2, 根據在XML檔案中的設定,生成LayoutParamsView的LayoutParameter用來適應root佈局, 並且設定給View

params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
    // Set the layout params for temp if we are not
    // attaching. (If we are, we use addView, below)
    temp.setLayoutParams(params);
}

2, 最後把 View temp 返回了.

所以, 最終呈現出來的是符合我們的預期的.

inflater.inflate(layoutId, root,true);

inflater.inflate(layoutId, root,true);inflater.inflate(layoutId, root,false); 唯一的區別就是 inflater.inflate(layoutId, root,true); 會自動把View新增到root裡, 不用我們來add進去.

/**
 * inflater.inflate(layoutId, root, true);
 */
private void add03() {
    View view = inflater.inflate(R.layout.inflater01, root, true);
    ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
    if (layoutParams == null) {
        Log.e("MainActivity", "layoutParams is null");
    } else {
        Log.e("MainActivity", "layoutParams width:height" + layoutParams.width + ":" + layoutParams.height);
    }
}