13

我的问题是:我想知道 xLayout(或一般的 ViewGroup)何时从 XML 添加子视图?我所说的“何时”是指在代码的哪个点,在 UI 工具包的“遍历”的哪个“通道”中?我应该覆盖 xLayout 或 ViewGroup 的哪个方法?

我已经完成了我的功课:我已经观看了在上一次 Google I/O 中介绍的“为 Android 编写自定义视图”(由 Adam Powell 和 Romain Guy),并且我已经阅读了 Adam Powell 对这篇 Google+帖子的评论。

4

2 回答 2

14

在 Android 的源代码中查找添加子项的确切位置。

我们可以看看setContentView(R.layout.some_id)引擎盖下正在做什么。

setContentView(int)调用PhoneWindow#setContentView(int)-PhoneWindow链接是一个具体的实现Window

@Override
public void setContentView(int layoutResID) {
    if (mContentParent == null) {
        installDecor();
    } else {
        mContentParent.removeAllViews();
    }
    mLayoutInflater.inflate(layoutResID, mContentParent);
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
}

该方法最终LayoutInflater#inflate(layoutResID, mContentParent) 调用ViewGroup#addView(View, LayoutParams). mContentParent介于两者之间,子视图

我想知道在将内容视图设置为包含自定义视图的 XML 文件后究竟会发生什么。在构造函数之后,代码中必须有一部分自定义视图“解析/读取/膨胀/转换”XML 声明的子视图到实际视图!(约翰管评论)

歧义:从 JohnTube 的评论来看,他似乎更感兴趣的是了解自定义视图是如何膨胀的。要知道这一点,我们必须看看LayoutInflaterLink的工作原理。

所以,答案Which method of xLayout or ViewGroup should I override ?ViewGroup#addView(View, LayoutParams)。请注意,此时,所有常规/自定义视图的膨胀已经发生。

自定义视图的膨胀:

以下方法LayoutInflater是在addView(View, LayoutParams)父/根上调用的地方:

注意:调用链到这个mLayoutInflater.inflate(layoutResID, mContentParent);PhoneWindow#setContentView(int)mContentParentDecorView: 可通过 访问的视图getWindow().getDecorView()

// Inflate a new view hierarchy from the specified XML node.
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)

// Recursive method used to descend down the xml hierarchy and instantiate views,     
// instantiate their children, and then call onFinishInflate().
void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs,
       boolean finishInflate) throws XmlPullParserException, IOException

此方法(和递归)中感兴趣的调用rInflate(XmlPullParser, View, AttributeSet, boolean)是:

temp = createViewFromTag(root, name, attrs);

让我们看看createViewFromTag(...)在做什么:

View createViewFromTag(View parent, String name, AttributeSet attrs) {
    ....
    ....
    if (view == null) {
        if (-1 == name.indexOf('.')) {
            view = onCreateView(parent, name, attrs);
        } else {
            view = createView(name, null, attrs);
        }
    }
    ....
}

period(.)决定onCreateView(...)是否createView(...)调用。

为什么要做这个检查?因为View定义在android.view, android.widgetorandroid.webkit包是通过它的类名来访问的。例如:

android.widget: Button, TextView etc.

android.view: ViewStub. SurfaceView, TextureView etc.

android.webkit: WebView

当遇到这些视图时,onCreateView(parent, name, attrs)被调用。此方法实际上链接到createView(...)

protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
    return createView(name, "android.view.", attrs);
}

这将处理SurfaceView,TextureView和 package.json 中定义的其他视图android.view。如果您有兴趣了解如何TextView, Button etc.处理,请查看PhoneLayoutInflaterLink - 它扩展LayoutInflater和覆盖onCreateView(...)以检查是否android.widget以及android.webkit是否是预期的包名称。事实上,调用getLayoutInflater()会为您提供PhoneLayoutInflater. 这就是为什么如果你要子类LayoutInflater化,你甚至不能膨胀最简单的布局——因为LayoutInflater只能处理来自android.view的视图。

无论如何,我离题了。这种额外的一点发生在常规视图中——它们的定义中没有 a period(.)。自定义视图的名称中确实有一个句点 - com.my.package.CustomView. 这就是LayoutInflater区分两者的方式。

因此,如果是常规视图(例如 Button),prefix则将android.widget作为第二个参数传递 - 对于自定义视图,这将是null. 然后prefix将 与 一起使用name以获取该特定视图类的构造函数。自定义视图不需要这个,因为它们name已经完全合格。我想这样做是为了方便。否则,您会以这种方式定义布局:

<android.widget.LinearLayout
    ...
    ... />  

(虽然它是合法的......)

此外,这也是来自支持库(例如 <android.support.v4.widget.DrawerLayout.../>)的视图必须使用完全限定名称的原因。

顺便说一句,如果您确实想将布局编写为:

<MyCustomView ../>

您所要做的就是扩展 LayoutInflater 并将您的包名称添加com.my.package.到在膨胀期间检查的字符串列表中。检查PhoneLayoutInflater这方面的帮助。

让我们看看自定义视图和常规视图在最后阶段会发生什么 - createView(...)

public final View createView(String name, String prefix, AttributeSet attrs)
                            throws ClassNotFoundException, InflateException {

    // Try looking for the constructor in cache
    Constructor<? extends View> constructor = sConstructorMap.get(name);
    Class<? extends View> clazz = null;

    try {
        if (constructor == null) {
            // Class not found in the cache, see if it's real, and try to add it
            clazz = mContext.getClassLoader().loadClass(
                 prefix != null ? (prefix + name) : name).asSubclass(View.class);
            ....
            // Get constructor   
            constructor = clazz.getConstructor(mConstructorSignature);
            sConstructorMap.put(name, constructor);
        } else {
            ....
        }

        Object[] args = mConstructorArgs;
        args[1] = attrs;

        // Obtain an instance
        final View view = constructor.newInstance(args);
        ....

        // We finally have a view!
        return view;
    }
    // A bunch of catch blocks: 
        - if the only constructor defined is `CustomView(Context)` - NoSuchMethodException
        - if `com.my.package.CustomView` doesn't extend View - ClassCastException
        - if `com.my.package.CustomView` is not found - ClassNotFoundException

    // All these catch blocks throw the often seen `InflateException`.
}

……一个View诞生了。

于 2014-08-24T00:46:25.087 回答
8

如果您谈论的是在 XML 中定义的 ViewGroup,它的子项会在视图膨胀时添加。这可能是当您使用 a 显式膨胀LayoutInflater或设置活动的内容视图时。(可能还有其他几次,尤其是在您使用存根视图时。)

如果您想自己将孩子添加到ViewGroup未膨胀的,您可以在视图的构造函数中执行此操作。

编辑:如果您想查看在视图膨胀时如何添加子项,这发生在对LayoutInflater.inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot). 的源代码android.view.LayoutInflater包含在 Android SDK 发行版中;在线版本可以在很多地方找到(例如,在 GrepCode上)。setContentView(int)例如,当您调用Activity或显式扩充布局资源时,最终会调用此方法。

子代实际上是在对rInflate(parser, root, attrs, false);("recursive inflate") 的调用中添加的,这可能会从方法中的几个不同位置调用inflate(),具体取决于 inflater 作为根标记找到的内容。您可以自己跟踪代码逻辑。一个有趣的点是,一个孩子直到它自己的孩子被递归膨胀并添加到它的父母中才被添加到它的父母中。

inflate和使用的另一种有趣的方法rInflatecreateViewFromTag。这可能依赖于可安装LayoutInflater.Factory(或.Factory2对象)来创建视图,或者可能最终调用createView. 在那里您可以看到如何调用视图的双参数构造函数 ( (Context context, AttributeSet attrs))。

于 2013-06-27T03:41:14.410 回答