跳到主要內容

探討 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:...

透明背景的 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...

Borderless Ripple Effect using Appcompat v21

        Google 日前發表了新的設計準則: Material design, 其中定義了新的 touch effect: Ripple effect。 而在最近也發佈了新的 support v7 library,可以讓 api level 7 以上都能建立 Material design 的 UI。我自己是滿偷懶的, 都只會設計一種 layout/style, 讓所有 api level 通吃, 此時 support library 就幫了我很大的忙 XDD。         最近剛好看到如何讓 view 按下去的時候會有 ripple effect, Google 官方其實就有提供說明了, 請參考  https://developer.android.com/training/material/animations.html#Touch , 只要在 layout xml, 想出現此效果的view 加上 android:background= ?android:attr/selectableItemBackground  就可以啦~~ 要是想向下相容, 就必需使用 support v7 library, 其實也只是把上面那行改成 android:background="?attr/selectableItemBackground"         但是請注意, 使用上述產生的 ripple effect 最大範圍是 view 的大小。有注意看 Google 的一些 app, Appbar(Action bar/ Toolbar) 上的按鈕按下去的話, ripple effect 是會超出 view, 也就是說, 邊界會是圓形的而不是方形的。要達到這個效果的話, 只要把  selectableItemBackground 改成  selectableItemBackgroundBorderless   就可以啦, support v7 libaray 也可以用喔。         最後再提醒一點, ripple effect 目前...