澳门新葡萄京官网注册 4

澳门新葡萄京官网注册LayoutInflater原理解析

之前做过一个项目(随心壁纸),主要展示过去每期的壁纸主题以及相应的壁纸,而且策划要求,最好可以动态变换主题呈现方式,这样用户体验会比较好。嗯,好吧,策划的话,咱们也没法反驳,毕竟这样搞,确实很不错。于是开始去研究这方面的东西。

用法
获取LayoutInflater

首先,我想到的是照片墙效果,改变图片就能有不同的呈现方式。可是这样的话,文字以及更深层的自定义效果,就无法实现了。然后,思考了下,决定仿照android原生布局文件解析方式,自己去动态解析布局。

首先要注意LayoutInflater本身是一个抽象类,我们不可以直接通过new的方式去获得它的实例,通常有下面三种方式:

先来看下android 原生布局文件解析流程:

第一种:

第一步:调用LayoutInflater的inflate函数解析xml文件得到一个view,然后来看看inflate函数:

LayoutInflater inflater = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

//使用常见的API方法去解析xml布局文件,     
LayoutInflater layoutInflater = (LayoutInflater)getSystemService();    
View root = layoutInflater.inflate(R.layout.main, null,false);

第二种:

第二步:在inflate函数中,获取一个XmlResourceParser来解析xml布局文件,再往下跟inflate(parser,
root, attachToRoot):

LayoutInflater inflater = LayoutInflater.from(context);

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

第三种:

第三步:inflate函数中会根据布局的节点名创建根视图,接着根据方法中传进来的root参数,判断是否为空,如果不为null,则为该根视图赋予外面父视图的布局参数。接着调用rInflate函数来为根视图添加所有字节点。

在Activity内部调用getLayoutInflater()方法

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; //该mConstructorArgs属性最后会作为参数传递给View的构造函数
            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(); //节点名,即API中的控件或者自定义View完整限定名

                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, 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) {
                        // 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);
                        }
                    }

                    // Inflate all children under temp
                    rInflate(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) {
                        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) {
                //...
            } finally {
                // Don't retain static reference on context.
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;
            }

            return result;
        }
}

看看后面两种方法的实现:

第四步:rInflate方法中主要是去递归调用布局文件根视图的子节点。将解析得到的view添加到parentView。

public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError(“LayoutInflater not found.”);
}
return LayoutInflater;
}

 /**
     * 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 {

        final int depth = parser.getDepth();
        int type;

        while (((type = parser.next()) != XmlPullParser.END_TAG ||
                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

            if (type != XmlPullParser.START_TAG) {
                continue;
            }

            final String name = parser.getName();

            if (TAG_REQUEST_FOCUS.equals(name)) {  //处理<requestFocus />标签
                parseRequestFocus(parser, parent);
            } else if (TAG_INCLUDE.equals(name)) {  //处理<include />标签 
                if (parser.getDepth() == 0) {
                    throw new InflateException("<include /> cannot be the root element");
                }
                parseInclude(parser, parent, attrs); //解析<include />节点
            } else if (TAG_MERGE.equals(name)) { //处理<merge />标签  
                throw new InflateException("<merge /> must be the root element");
            } else if (TAG_1995.equals(name)) {  //处理<blink />标签
                final View view = new BlinkLayout(mContext, attrs);
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                rInflate(parser, view, attrs, true);
                viewGroup.addView(view, params);                
            } else {
                //根据节点名构建一个View实例对象
                final View view = createViewFromTag(parent, name, attrs);
                final ViewGroup viewGroup = (ViewGroup) parent;
                //调用generateLayoutParams()方法返回一个LayoutParams实例对象,
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                rInflate(parser, view, attrs, true); //继续递归调用 
                viewGroup.addView(view, params); //OK,将该View以特定LayoutParams值添加至父View
            }
        }

        if (finishInflate) parent.onFinishInflate();  //完成解析过程,通知..
    }

在Activity内部调用getLayoutInflater方法其实调用的是PhoneWindow的mLayoutInflater:

在rInflate方法的37行代码中,final View view = createViewFromTag(parent,
name,
attrs),由节点名等参数构建的一个view实例对象,由于下面的代码会越来越大,就直接贴出主要实现函数,具体可参见Android源码。

public PhoneWindow(Context context) {
super(context);
mLayoutInflater = LayoutInflater.from(context);
}

/**
     * default visibility so the BridgeInflater can override it.
     */
    View createViewFromTag(View parent, String name, AttributeSet attrs) {

        //...
        try {
            //...

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

            return view;

        } catch (InflateException e) {
            //...
        }
    }

所以,这几个方法实际上殊途同归,都是通过调用Context的getSystemService方法去获取。获取到的是PhoneLayoutInflater这个实现类,具体的获取过程就不在这里展开分析了。

然后再往onCreateView()中跟下去,会发现,它其实主要还是实现了createView();所以我们直接CreateView实现。

public class Policy implements IPolicy {

public LayoutInflater makeNewLayoutInflater(Context context) {
return new PhoneLayoutInflater(context);
}
}

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

加载布局

在createView(name, “android.view.”,
attrs)中,会用反射机制创建android.view.XXX(比如TextView)的实例对象,并返回。

我们用一个简单的例子,介绍下LayoutInflater的用法:

这也就是LayoutInflater.inflate的布局解析流程了

这个例子的目标是在屏幕上展示一个按钮,点击按钮时,会通过LayoutInflater把一个橙色背景的TextView以match_parent的形式加载到一块宽高为300dp的RelativeLayout中。

当你熟悉了流程,接下来为你讲解的,随心壁纸的动态解析布局思路,你就基本懂的大半了

首先创建两个布局文件:

由于最初的layoutInflater.inflate(R.layout.main,
null,false)函数,传入的是R.layout.main资源id,而对于我们的项目,布局文件是在线更新的,是间接存储在sd卡中,所以这种解析方式就不行了,所幸LayoutInflater的api方法还提供了inflate(XmlPullParser
parser, ViewGroup root,boolean
attachToRoot)解析,根据文件保存路径生成需要的xmlPullParser:

demo_layout.xml

public XmlPullParser getXmlPullParser(String resource) {
        XmlPullParser parser = Xml.newPullParser();
        try {
            // InputStream is=mContext.getAssets().open("transfer_main.xml");
            FileInputStream is = new FileInputStream(resource);

            parser.setInput(is, "utf-8");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return parser;
}

<?xml version=”1.0″ encoding=”utf-8″?>
<TextView
xmlns:android=”http://schemas.android.com/apk/res/android”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:layout_gravity=”center”
android:gravity=”center”
android:background=”#ff750c”
android:text=”Hello , world !” />

后面的第二步等其实都是一样的流程(我会在后面贴出实现demo)。但是有一点需要特别注意,也是必须实现的一点:

activity_main.xml

因为所下载的布局文件得到的解析流,跟程序里res/layout/xxx.xml有一个非常大的不同,资源id!!程序中的布局文件,里面所注册的id以及text,或者drawble都是可以真实在R目录下找到的,而下载的布局文件是没有这个福利的,它是我们外面生成的,并没有经过apk编译过程。所以为了能得到下载文件里的布局各个id,我们需要自己实现对View的属性解析。

<?xml version=”1.0″ encoding=”utf-8″?>
<RelativeLayout
xmlns:android=”http://schemas.android.com/apk/res/android”
android:layout_width=”match_parent”
android:layout_height=”match_parent”>
<Button
android:id=”@+id/button”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_centerHorizontal=”true”
android:text=”点击加载” />
<RelativeLayout
android:id=”@+id/root”
android:layout_width=”300dp”
android:layout_height=”300dp”
android:layout_centerInParent=”true”/>
</RelativeLayout>

protected View onCreateView(String name, AttributeSet attrs)
            throws ClassNotFoundException {
        // return createView(name, "android.view.", attrs);
        return createView(name, "com.xxx.xxxx.viewanalysis.view.VA", attrs);  //比如com.xx.view.VATextView 项目中自定义view 继承自TextView
}

MainActivity.java

布局中的控件代码编写,可以是TextView等原生的,也可以是自定义过的,因为TextView经过加前缀,再通过后面的反射方法,也会跑到相应的自定义方法,返回一个自定义View对象。不过因为最终都是要跑自定义view,所以要求我们需要事先定义好会用到的,目前我定义好了9种,比如Button,GridView,RelativeLayout等。如果没有定义的view直接用在文件中,会导致编译出错(ClassNotFoundException)。

public class MainActivity extends Activity {
RelativeLayout rootView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
rootView = (RelativeLayout) findViewById(R.id.root);
findViewById(R.id.button).setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view) {
inflateView();
}
});
}
private void inflateView() {
View insideView =
LayoutInflater.from(MainActivity.this).inflate(R.layout.demo_layout,
null);
rootView.addView(insideView);
}
}

VAButton类实现:

编译运行,点击点击加载按钮,结果如下:

public class VAButton extends android.widget.Button {

    public VAButton(Context context, AttributeSet attrs) {
        super(context);
        setAttributeSet(attrs);
    }

    @SuppressWarnings("deprecation")
    public void setAttributeSet(AttributeSet attrs) {

        HashMap<String, ParamValue> map = YDResource.getInstance().getViewMap();

        int count = attrs.getAttributeCount();
        for (int i = 0; i < count; i++) {
            ParamValue key = map.get(attrs.getAttributeName(i));
            if (key == null) {
                continue;
            }
            switch (key) {
            case id:
                this.setTag(attrs.getAttributeValue(i));
                break;
            case text:
                String value = YDResource.getInstance().getString(
                        attrs.getAttributeValue(i));
                this.setText(value);
                break;
            //case...  
            default:
                break;
            }
        }
}

点击按钮后

Ps: view id通过设置标志在外面可以获取到(具体可以看demo)

可以看到,我们成功把demo_layout.xml对应布局中的TextView加载进来了。

属性方面的解析,我就不多说了,网上的资料很多,有兴趣的可以去了解下。

但遗憾的是,加载进来的TextView宽高并不是我们期望的300×300大小╮(╯▽╰)╭。

demo效果图:

那么问题来了:

澳门新葡萄京官网注册 1

    为什么我们在布局文件中给TextView设置的宽高属性失效了呢?
    LayoutInflater又是如何把xml解析加载成为View的呢?

项目中的效果图:

而且,inflate有多个不同的重载方法:

澳门新葡萄京官网注册 2 澳门新葡萄京官网注册 3

inflate(int resource, ViewGroup root)
inflate(int resource, ViewGroup root, boolean attachToRoot)
inflate(XmlPullParser parser, ViewGroup root)
inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)

接下来看看demo中的代码结构:

上面的例子只是使用了第一个方法,并且root的传参还是null。

澳门新葡萄京官网注册 4

这些方法又有什么不同的地方呢?

这里的代码主体在于YDLayoutInflater与ParamValue的实现,以及9种自定义view。
YDLayoutInflater主要是仿照LayoutInflater实现的,做了一些适应布局文件的修改处理,上面已经说过了。

我们从源码入手,去寻找这两个问题的答案。
源码解析

具体的代码下载地址

上文有提到,我们获取的LayoutInflater实例其实是PhoneLayoutInflater,但PhoneLayoutInflater并没有重写inflate的几个方法,所以我们的分析还是在LayoutInflater这个类展开。

动态解析布局的思路讲完了,应用到项目中,效果也不错,虽然有一些限制规范,但是对于总体的功能设计是无关大雅的。关于主题呈现动态更新,如果有大神有更好的方式,请私信我哦!

首先比对下这几个重载方法:

public View inflate(int resource, ViewGroup root) {
// root不为空时,attachToRoot默认为true
return inflate(resource, root, root != null);
}
public View inflate(int resource, ViewGroup root, boolean attachToRoot)
{
XmlResourceParser parser =
getContext().getResources().getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
public View inflate(XmlPullParser parser, ViewGroup root) {
// root不为空时,attachToRoot默认为true
return inflate(parser, root, root != null);
}
public View inflate(XmlPullParser parser, ViewGroup root, boolean
attachToRoot) {

}

原来,前三个方法最终调用的都是:

public View inflate(XmlPullParser parser, ViewGroup root, boolean
attachToRoot) {

}

而且,root不为空时,attachToRoot默认为true。布局id会被通过调用getLayout方法生成一个XmlResourceParser对象。

Android中布局文件都是使用xml编写的,所以解析过程自然涉及xml的解析。常用的xml解析方式有DOM,SAX和PULL三种方式。DOM不适合xml文档较大,内存较小的场景,所以不适用于手机这样内存有限的移动设备上。SAX和PULL类似,都具有解析速度快,占用内存少的优点,而相对之下,PULL的操作方式更为简单易用,所以,Android系统内部在解析各种xml时都用的是PULL解析器。

这里解析布局xml文件时使用的就是Android系统提供的PULL方式。

我们继续分析inflate方法。
inflate方法

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

        final AttributeSet attrs = Xml.asAttributeSet(parser);
        // 首先注意result初值为root
        View result = root;
        try {
            // 尝试找到布局文件的根节点
            int type;
            while ((type = parser.next()) != XmlPullParser.START_TAG &&
                    type != XmlPullParser.END_DOCUMENT) {
                // Empty
            }
            ...
            // 获取当前节点名称,如merge,RelativeLayout等
            final String name = parser.getName();

            ...
            // 处理merge节点
            if (TAG_MERGE.equals(name)) {
                // merge必须依附在一个根View上
                if (root == null || !attachToRoot) {
                    throw new InflateException("<merge /> can be used only with a valid "
                            + "ViewGroup root and attachToRoot=true");
                }
                rInflate(parser, root, attrs, false);
            } else {

                View temp;

                // 根据当前信息生成一个View
                temp = createViewFromTag(root, name, attrs);
                ...
                ViewGroup.LayoutParams params = null;
                if (root != null) {

                    // 如果指定了root参数的话,根据节点的布局参数生成合适的LayoutParams
                    params = root.generateLayoutParams(attrs);
                    // 若指定了attachToRoot为false,会将生成的布局参数应用于上一步生成的View
                    if (!attachToRoot) {
                        temp.setLayoutParams(params);
                    }
                }
                // 由上至下,递归加载xml内View,并添加到temp里
                rInflate(parser, temp, attrs, true);
                // 如果root不为空且指定了attachToRoot为true时,会将temp作为子View添加到root中
                if (root != null && attachToRoot) {
                    root.addView(temp, params);
                }
                // 如果指定的root为空,或者attachToRoot为false的时候,返回的是加载出来的View,
                // 否则返回root
                if (root == null || !attachToRoot) {
                    result = temp;
                }
            }
        } ... // 异常处理
        return result;
    }
}

首先定义布局根View这一个概念,注意与root并不是同一个东西:

root是我们传进来的第二个参数
布局根View则是传递进来的布局文件的根节点所对应的View

这个方法主要有下面几个步骤:

首先查找根节点,如果整个xml文件解析完毕也没看到根节点,会抛出异常;
如果查找到的根节点名称是merge标签,会调用rInflate方法继续解析布局,最终返回root;
如果是其他标签(View、TextView等),会调用createViewFromTag生成布局根View,并调用rInflate递归解析余下的子View,添加至布局根View中,最后视root和attachToRoot参数的情况最终返回view或者root。

从这里我们可以理清root和attachToRoot参数的关系了:

root != null, attachToRoot == true:
传进来的布局会被加载成为一个View并作为子View添加到root中,最终返回root;
而且这个布局根节点的android:layout_参数会被解析用来设置View的大小。

root == null, attachToRoot无用:
当root为空时,attachToRoot是什么都没有意义,此时传进来的布局会被加载成为一个View并直接返回;
布局根View的android:layout_xxx属性会被忽略。

root != null, attachToRoot == false:
传进来的布局会被加载成为一个View并直接返回。
布局根View的android:layout_xxx属性会被解析成LayoutParams并保留。(root只用来参与生成布局根View的LayoutParams)

现在可以解答文章开始留下的疑问了:
为何在布局文件中给TextView设置的android:layout属性失效了?

回到例子中的代码,我们加载布局的代码是:

View insideView =
LayoutInflater.from(MainActivity.this).inflate(R.layout.demo_layout,
null);
rootView.addView(insideView);

即root传参为空,与上面第2种情况对应,所以此时布局根View的android:layout_xx属性都被忽略了。也就是相当于并没有给TextView设置宽高,所以只能按默认的TextView大小显示了。

稍微改变下代码:

LayoutInflater.from(MainActivity.this).inflate(R.layout.demo_layout,
rootView);

注意这段代码等同于:

LayoutInflater.from(MainActivity.this).inflate(R.layout.demo_layout,
rootView, true);

inflate方法在root不为空时,默认会将attachToRoot置为true。

这时等同于我们上面的情况1,由于此时infalte会将加载出来的View自动添加到root中,我们要把rootView.addView(insideView)一句移除,否则会遇到这样的报错:

java.lang.IllegalStateException: The specified child already has a
parent. You must call removeView() on the child’s parent first.
at android.view.ViewGroup.addViewInner(ViewGroup.java:4454)
at android.view.ViewGroup.addView(ViewGroup.java:4295)
at android.view.ViewGroup.addView(ViewGroup.java:4235)
at android.view.ViewGroup.addView(ViewGroup.java:4208)

再来运行看看:

终于达到我们想要的效果了!这也验证了上面的第一个结论。

顺便再用这个例子拓展一下,验证我们的情况3,即root != null, attachToRoot
== false时的情况:

View insideView =
LayoutInflater.from(MainActivity.this).inflate(R.layout.demo_layout,
rootView, false);
rootView.addView(insideView);

结果是一样的,图就不贴了,即root != null, attachToRoot ==
false时,root只是用来参与布局根View的大小、位置设置的。

好了,关于这两个参数的疑问的解答就告一段落了,我们接着回到代码,寻找另一个问题的答案。
继续跟进rInflate和createViewFromTag方法。
rInflate方法

void rInflate(XmlPullParser parser, View parent, final AttributeSet
attrs,
boolean finishInflate) throws XmlPullParserException, IOException {

while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT)
{

final String name = parser.getName();

    // 解析“requestFocus”标签,让父View调用requestFocus()获取焦点
    if (TAG_REQUEST_FOCUS.equals(name)) {
        ...
    } else if (TAG_INCLUDE.equals(name)) {
        ...
    } else if (TAG_MERGE.equals(name)) {
        ...
    } else if (TAG_1995.equals(name)) {
        ...      
    } else {
        // 调用createViewFromTag生成一个View
        final View view = createViewFromTag(parent, name, attrs);
        // 逐层递归调用rInflate,解析view嵌套的子View
        rInflate(parser, view, attrs, true);
        // 将解析生成子View添加到上一层View中
        viewGroup.addView(view, params);
    }
}
// 内层子View被解析出来后,将调用其父View的“onFinishInflate()”回调
if (finishInflate) parent.onFinishInflate();

}

首先是几个特殊标签的处理,如requestFocus、include等,为了把握住主要脉络,我们不做展开,直接看最后一个else的内容。

原来,rInflate主要是调用了createViewFromTag生成当前解析到的View节点,并递归调用rInflate逐层生成子View,添加到各自的上层View节点中。
当某个节点下面的所有子节点View解析生成完成后,才会调起onFinishInflate回调。

所以createViewFromTag才是真正生成View的地方啊。
createViewFromTag方法

View createViewFromTag(View parent, String name, AttributeSet attrs) {

try {
View view;
if (mFactory2 != null) view = mFactory2.onCreateView(parent, name,
mContext, attrs);
else if (mFactory != null) view = mFactory.onCreateView(name, mContext,
attrs);
else view = null;
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, mContext, attrs);
}

    // 三个Factory都不存在调用LayoutInflater自己的onCreateView或者createView
    // 
    // 如果View标签中没有".",则代表是系统的widget,则调用onCreateView,
    // 这个方法会通过"createView"方法创建View
    // 不过前缀字段会自动补"android.view."前缀。
    if (view == null) {
        if (-1 == name.indexOf('.')) {
            view = onCreateView(parent, name, attrs);
        } else {
            view = createView(name, null, attrs);
        }
    }
    return view;
} catch (InflateException e) {
    ...
} ...

}
public interface Factory {
public View onCreateView(String name, Context context, AttributeSet
attrs);
}

首先会依次调用mFactory2、mFactory和mPrivateFactory三者之一的onCreateView方法去创建一个View。
如果这几个Factory都为null,会调用LayoutInflater自己的onCreateView或者createView来实例化View。

自定义Factory一个十分有用的使用场景就是实现应用换肤,有兴趣的读者可以参考我开源的Android-Skin-Loader中的具体细节。

通常情况下,自定义工厂mFactory2、mFactory和私有工厂mPrivateFactory是空的,当Activity继承自AppCompatActivity时,才会存在自定义Factory。

所以,生成View的重任就落在了onCreateView和createView身上。

onCreateView调用的其实是createView,即View的节点名称没有.时,将自动补上android.view.前缀(即完整类名):

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

继续关注的createView实现。
createView方法

public final View createView(String name, String prefix, AttributeSet
attrs)
throws ClassNotFoundException, InflateException {
Constructor<? extends View> constructor =
sConstructorMap.get(name);
Class<? extends View> clazz = null;
try {
if (constructor == null) {
// 缓存中不存在某View的构造方法,先new出来放缓存中
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);

            ...
            constructor = clazz.getConstructor(mConstructorSignature);
            sConstructorMap.put(name, constructor);
        } else {
            ...
        }
        Object[] args = mConstructorArgs;
        args[1] = attrs;
        return constructor.newInstance(args);
    } catch (NoSuchMethodException e) {
        ...
    }
}

这就是最后一步了,十分容易理解:
通过传进来的全类名,调用newInstance来创建一个这个类的实例并返回,返回的这个实例就是我们需要的View了。

结合上面的递归解析过程,每个层级的节点都会被生成一个个的View,并根据View的层级关系add到对应的直接父View(上层节点)中,最终返回一个包含了所有解析好的子View的布局根View。

至此,通过xml来加载View的整个原理就分析完成了。
总结

最后,我们再次回顾下上面的分析结果:
inflate方法的参数关系

root != null, attachToRoot == true
传进来的布局会被加载成为一个View并作为子View添加到root中,最终返回root;
而且这个布局根节点的android:layout_xxx参数会被解析用来设置View的大小;

root == null, attachToRoot无意义
当root为空时,attachToRoot无论是什么都没有意义。此时传进来的布局会被加载成为一个View并直接返回;
布局根View的android:layout_xxx属性会被忽略,即android:layout_xx属性只有依附在某个ViewGroup中才能生效;

root != null, attachToRoot == false
传进来的布局会被加载成为一个View并直接返回。
布局根View的android:layout_xxx属性会被解析成LayoutParams并设置在View上,此时root只用于设置布局根View的大小和位置。

加载xml布局的原理

其实就是从根节点开始,递归解析xml的每个节点,每一步递归的过程是:通过节点名称(全类名),使用ClassLoader创建对应类的实例,也就是View,然后,将这个View添加到它的上层节点(父View)。并同时会解析对应xml节点的属性作为View的属性。每个层级的节点都会被生成一个个的View,并根据View的层级关系add到对应的直接父View(上层节点)中,最终返回一个包含了所有解析好的子View的布局根View。

发表评论

电子邮件地址不会被公开。 必填项已用*标注