跳到主要內容

探討 LayoutInflater 的 inflate method

        在 Android 程式裡要動態新增 View,最直接的方法就是使用 LayoutInflater 提供的 inflate method。比較常用的有以下兩個 method:
inflate(int resource, ViewGroup root)
inflate(int resource, ViewGroup root, boolean attachToRoot)

        第一個參數 resource,就是 layout xml file,如 R.layout.your_file。第二個參數 root,指的是這個 inflate 的 view,要成為哪個 view 的 child view。最後一個參數 attachToRoot 是 inflate 的 view 是否要 attach 到 root view,這會影響到回傳的是哪個 view。假如 attachToRoot 為 true,則最後回傳的 view 為 root view (就是第二個參數的 view);反之就是回傳 inflate 的 view。

        第二個參數 ViewGroup root 需要特別說明一下,因為這會影響到如何 inflate 新的 view 出來。android 的 view 是有階層 (hierarchy) 的,因此 parent view 的屬性 (attribute) 會影響到 child view。假如你是如以下兩種方式使用 inflate method的話:
View view1 = LayoutInflater.from(mContext).inflate(R.layout.your_file, null)
View view2 = LayoutInflater.from(mContext).inflate(R.layout.your_file, null, false)

        因為沒有 root view,系統沒辦法把 inflate view 的屬性正確設定,因此系統會把 inflate view 的屬性設為 null,這會導致 inflate view 的屬性都非你所預期 (稍後會用 LayoutInflater source code 解說)。因此要很注意你 root view 是否要代 null。

        接下來直接看 LayoutInflater 的 source code,從 inflate(int resource, ViewGroup root) 開始:
/**
 * Inflate a new view hierarchy from the specified xml resource. Throws
 * {@link InflateException} if there is an error.
 * 
 * @param resource ID for an XML layout resource to load (e.g.,
 *        R.layout.main_page)
 * @param root Optional view to be the parent of the generated hierarchy.
 * @return The root View of the inflated hierarchy. If root was supplied,
 *         this is the root View; otherwise it is the root of the inflated
 *         XML file.
 */
public View inflate(int resource, ViewGroup root) {
     return inflate(resource, root, root != null);
}

        這裡面也只是 invoke 其他 inflate method,只是多了一個參數判斷 root view 是否為 null。其實這個就是 inflate(int resource, ViewGroup root, boolean attachToRoot)。
/**
 * Inflate a new view hierarchy from the specified xml resource. Throws
 * {@link InflateException} if there is an error.
 * 
 * @param resource ID for an XML layout resource to load (e.g.,
 *        R.layout.main_page)
 * @param root Optional view to be the parent of the generated hierarchy (if
 *        attachToRoot is true), or else simply an object that
 *        provides a set of LayoutParams values for root of the returned
 *        hierarchy (if attachToRoot is false.)
 * @param attachToRoot Whether the inflated hierarchy should be attached to
 *        the root parameter? If false, root is only used to create the
 *        correct subclass of LayoutParams for the root view in the XML.
 * @return The root View of the inflated hierarchy. If root was supplied and
 *         attachToRoot is true, this is root; otherwise it is the root of
 *         the inflated XML file.
 */
public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
    if (DEBUG) System.out.println("INFLATING from resource: " + resource);
    XmlResourceParser parser = getContext().getResources().getLayout(resource);
    try {
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}
        裡面的行為就是產生一個 XML parser,並再 invoke 其他 inflate method。
/**
 * Inflate a new view hierarchy from the specified XML node. Throws
 * {@link InflateException} if there is an error.
 * * Important   For performance
 * reasons, view inflation relies heavily on pre-processing of XML files
 * that is done at build time. Therefore, it is not currently possible to
 * use LayoutInflater with an XmlPullParser over a plain XML file at runtime.
 * 
 * @param parser XML dom node containing the description of the view
 *        hierarchy.
 * @param root Optional view to be the parent of the generated hierarchy (if
 *        attachToRoot is true), or else simply an object that
 *        provides a set of LayoutParams values for root of the returned
 *        hierarchy (if attachToRoot is false.)
 * @param attachToRoot Whether the inflated hierarchy should be attached to
 *        the root parameter? If false, root is only used to create the
 *        correct subclass of LayoutParams for the root view in the XML.
 * @return The root View of the inflated hierarchy. If root was supplied and
 *         attachToRoot is true, this is root; otherwise it is the root of
 *         the inflated XML file.
 */
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();
                
            if (DEBUG) {
                System.out.println("**************************");
                System.out.println("Creating root view: "
                        + name);
                System.out.println("**************************");
            }

            if (TAG_MERGE.equals(name)) {
                if (root == null || !attachToRoot) {
                    throw new InflateException("can be used only with a valid "
                            + "ViewGroup root and attachToRoot=true");
                }

                rInflate(parser, root, attrs, false);
            } else {
                // Temp is the root view that was found in the xml
                View temp;
                if (TAG_1995.equals(name)) {
                    temp = new BlinkLayout(mContext, attrs);
                } else {
                    temp = createViewFromTag(root, name, attrs);
                }

                ViewGroup.LayoutParams params = null;

                if (root != null) {
                    if (DEBUG) {
                        System.out.println("Creating params from root: " +
                                root);
                    }
                    // Create layout params that match root, if supplied
                    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);
                    }
                }

                if (DEBUG) {
                    System.out.println("-----> start inflating children");
                }
                // Inflate all children under temp
                rInflate(parser, temp, attrs, true);
                if (DEBUG) {
                    System.out.println("-----> done inflating children");
                }

                // We are supposed to attach all the views we found (int temp)
                // 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.
                if (root == null || !attachToRoot) {
                    result = temp;
                }
            }

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

        return result;
    }
}

        接下來大概看一下這個最長的 method 在做些什麼事:
View result = root;

        第 27 行先宣告一個預定要回傳的 view, 並先把 root view 指給它。

// Temp is the root view that was found in the xml
View temp;
if (TAG_1995.equals(name)) {
    temp = new BlinkLayout(mContext, attrs);
} else {
    temp = createViewFromTag(root, name, attrs);
}

        前面判斷完 XML 正確性後,在第 59 行就從 XML 生成 view。
ViewGroup.LayoutParams params = null;

if (root != null) {
    if (DEBUG) {
        System.out.println("Creating params from root: " +
                root);
    }
    // Create layout params that match root, if supplied
    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);
    }
}

        第 67 行宣告一個為 null 的 LayoutParams,並判斷 root view 是否為 null 來決定是否產一個正確的 LayoutParam。這在前面有提到假如 root view 為 null,則 inflate view 就不會依據 root view 去設定 LayoutParams,這也就是你的 inflate view 屬性不如預期的原因。從這邊也可以看到,假如你是使用 inflate(R.layout.your_file, parent, false) 的話,inflate view 還是會有正確的屬性,只是不會 attach 到 root view。
// Inflate all children under temp
rInflate(parser, temp, attrs, true);
if (DEBUG) {
    System.out.println("-----> done inflating children");
}

// We are supposed to attach all the views we found (int temp)
// 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.
if (root == null || !attachToRoot) {
    result = temp;
}

        後面幾行就是生成 inflate view 裡的 child view,是否要 attach root view,以及判斷回傳值是 inflate view 還是 root view。

        以上就是 inflate method 主要的行為,最後來說點結論吧:
1. 使用inflate(int resource, ViewGroup root) 就是使用 inflate(int resource, ViewGroup root, boolean       attachToRoot),只是第三個參數是看 root 是否為 null 來決定。

2. 要注意你是不是真的希望 root view為 null,因為這會影響到 inflate view。

3. inflate(R.layout.your_file, parent, true) 執行完後 inflate view 就會 attach 到 parent view 了,不需     要再 invoke parent.add(inflateView).

實際測試就懶得用了…有興趣的人可以自己試試看,或是參考下面的參考資料喔。

參考資料:
1. layout-inflation-as-intended

2. StackOverflow - making sense of layoutinflater

3. Android - LayoutInflater

留言

這個網誌中的熱門文章

如何把 Status Bar 變透明

        Android 從 4.4 (KitKat, api level 19) 後才支援這個功能, 到了 5.0 (Lollipop, api level 21) 自訂性更高, 可以讓我們設定各種顏色, 當然也包含透明色。以下分別介紹如何使用這兩種版本的方法。         方法1: 利用 attribute " android:windowTranslucentStatus ", 在 style.xml 加上這個 attribute 就好。要注意的是 Android 版本要在 4.4 以上才可以用這個 attribute: <resources> <!-- Base application theme for API 19+. This theme completely replaces AppTheme from res/values/styles.xml on API 19+ devices. --> <style name="AppTheme" parent="@style/AppBaseTheme"> <!-- API 19 theme customizations can go here. --> <item name="android:windowTranslucentStatus">true</item> </style> </resources>         下面的圖分別為 4.4 跟 5.0 的手機使用這個 attribute 的結果:         因為設定了這個 attribute, 畫面會從 status bar 下方開始畫。要解決這個有兩種方法, 第一個是在 layout 畫面設定 attribute "android:fitsSystemWindows " <RelativeLayout xmlns:android="http://schemas.android.com/apk/res

透明背景的 AlertDialog

        Android dialog 預設都會有一個背景底色,要怎麼把它去掉呢?其實直接改 AlertDialog 的 style 就可以了,但我改好久…以下的範例是使用了 Support v7 AppCompat 的 theme,其他 theme 我不知道可不可以,有興趣的人可以試試看。 <style name="TransparentDialog" parent="@style/Base.Theme.AppCompat.Dialog"> <item name="android:windowFrame">@null</item> <item name="android:windowBackground">@android:color/transparent</item> <item name="android:windowIsFloating">true</item> <item name="android:windowTitleStyle">@null</item> <item name="android:background">@android:color/transparent</item> <item name="android:windowNoTitle">true</item> <item name="android:alertDialogStyle">@style/TransparentDialog.Color</item> </style> <style name="TransparentDialog.Color" parent="@style/Base.Theme.AppCompat.Dialog"> <item name="androi

Deprecated API

        當透過 IDE 使用 API,或是查 Document 時,常會看到一段途述:  This method was deprecated… 這是什麼意思呢?這是告訴我們還是可以使用此 method,但未來可能會沒有用、甚至是 在新版本的 SDK 會把這個 method 移掉 。         以 API level 11 出現的 Fragment  為例子,在使用時常會 override 一個 callback 叫 onAttach (Activity activity),但到了 API level 23 (Lollipop) 時,可以發現 onAttach (Activity activity) 被標記為 deprecated 了,取而代之的是 onAttach (Context context)。         像我個人寫程式不太喜歡出現 warning,而使用到 deprecated api 會被 Android Studio 檢查出來,我在用 Fragment 時就直接都換成 onAttach (Context context),結果程式在 api level 較低的手機就出現不符合預期的行為。Debug 了一陣子才發現 onAttach (Context context) 是新的 API,舊版本 SDK 當然沒有這個  API,自然就不會執行到該 API。         因此,雖然有些 API 被標記為 deprecated,但當你的程式還需要支援較舊版本時,還是得使用這些 API,假如要用的 API 在 Support Library  也有對應版本的話,我會建議用 Support Library 版本,因為放在裡面的 API 本來就是為了向下相容而設計的。以 Support Libaray 裡的Fragment 來說,onAttach (Activity activity) 一樣被標記為 deprecated,也一樣有 onAttach (Context context)  ,但它卻向下相容到 api level 4,因此為了向下相容性,使用 Support Library 是比較好的。 參考資料 1. Android Fragment 2. Android Support v4 Fragment 3. An