Android通过xml生成创建View的过程解析

Android的布局方式有两种,一种是通过xml布局,一种是通过java代码布局,两种布局方式各有各的好处,当然也可以相互混合使用。很多人都习惯用xml布局,那xml布局是如何转换成view的呢?本文从源码的角度来简单分析下整个过程。

首先,创建一个新的项目,默认生成一个activity,其中xml布局很简单,就一个RelativeLayout套了一个ImageView,代码及效果如下:

public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } } 
 
界面1

其中关键之处在于调用了父类Activity的setContentView方法:

/**  
 * Set the activity content from a layout resource.  The resource     will be  
 * inflated, adding all top-level views to the activity.  
 *   
 * @param layoutResID Resource ID to be inflated.  
 */  
public void setContentView(int layoutResID) { getWindow().setContentView(layoutResID); } 

getWindow返回的是PhoneWindow实例,那我们直接来看PhoneWindow中的setContentView方法:

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

我们知道每个activity实际都对应一个PhoneWindow,拥有一个顶层的DecorView,DecorView继承自FrameLayout,作为根View,其中包含了一个标题区域和内容区域,这里的mContentParent就是其内容区域。关于PhoneWindow和DecorView的具体内容,读者可自行查阅。这段代码的意思很简单,如果DecorView的内容区域为null,就先初始化,否则就先把内容区域的子View全部移除,最后再引入layout布局,所以,关键在于mLayoutInflater.inflate(layoutResID, mContentParent); 代码继续往下看:

public View inflate(int resource, ViewGroup root) { return inflate(resource, root, root != null); } 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(); } } 

这里首先根据layout布局文件的Id生成xml资源解析器,然后再调用inflate(parser, root, attachToRoot)生成具体的view。XmlResourceParser是继承自XmlPullParser和AttributeSet的接口,这里的parser其实是XmlBlock的内部类Parser的实例。

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("<merge /> can be used only with a valid " + "ViewGroup root and attachToRoot=true"); } rInflate(parser, root, attrs); } else { // Temp is the root view that was found in the xml View temp = createViewFromTag(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); 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; } } 

第21行,获取xml根节点名:

final String name = parser.getName(); 

第39行根据节点名创建临时View(temp),这个临时view(temp)也是xml布局的根view:

View temp = createViewFromTag(name, attrs);  

第61行,在临时view(temp)的节点下创建所有子View,显然这个方法里是通过遍历xml所有子view节点,调用createViewFromTag方法生成子view并加载到根view中:

rInflate(parser, temp, attrs);  

第68到76行,则是判断,如果inflate方法有父view,则把临时view(temp)加载到父view中再返回,如果没有,则直接返回临时view(temp),我们这里调用inflate方法的时候显然有父view,即mContentParent,也就是最顶层view DecorView的内容区域。这里最关键有两个方法,一个是createViewFromTag,另一个是rInflate,现在来逐一分析:createViewFromTag实际最终调用的是createView方法:

public final View createView(String name, String prefix, AttributeSet attrs)  
        throws ClassNotFoundException, InflateException {  
    Constructor constructor = sConstructorMap.get(name);  
    Class 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); if (mFilter != null && clazz != null) { boolean allowed = mFilter.onLoadClass(clazz); if (!allowed) { failNotAllowed(name, prefix, attrs); } } constructor = clazz.getConstructor(mConstructorSignature); sConstructorMap.put(name, constructor); } else { // If we have a filter, apply it to cached constructor if (mFilter != null) { // Have we seen this name before? Boolean allowedState = mFilterMap.get(name); if (allowedState == null) { // New class -- remember whether it is allowed clazz = mContext.getClassLoader().loadClass( prefix != null ? (prefix + name) : name); boolean allowed = clazz != null && mFilter.onLoadClass(clazz); mFilterMap.put(name, allowed); if (!allowed) { failNotAllowed(name, prefix, attrs); } } else if (allowedState.equals(Boolean.FALSE)) { failNotAllowed(name, prefix, attrs); } } } Object[] args = mConstructorArgs; args[1] = attrs; return (View) constructor.newInstance(args); } catch (NoSuchMethodException e) { InflateException ie = new InflateException(attrs.getPositionDescription() + ": Error inflating class " + (prefix != null ? (prefix + name) : name)); ie.initCause(e); throw ie; } catch (ClassNotFoundException e) { // If loadClass fails, we should propagate the exception. throw e; } catch (Exception e) { InflateException ie = new InflateException(attrs.getPositionDescription() + ": Error inflating class " + (clazz == null ? "<unknown>" : clazz.getName())); ie.initCause(e); throw ie; } } 

其实这个方法很简单,就是通过xml节点名,通过反射获取view的实例再返回,其中先去map中查询构造函数是否存在,如果存在则直接根据构造函数创建实例,这样做的好处是不用每次都通过class去获取构造函数再创建实例,我们看第18行通过类实例获取构造函数:

constructor = clazz.getConstructor(mConstructorSignature);

其中mConstructorSignature定义如下:

private static final Class[] mConstructorSignature = new Class[] { Context.class, AttributeSet.class}; 

很显然,这里用的是带有Context和AttributeSet两个参数的构造函数,这也就是为什么,自定义view一定要重载这个构造函数的原因。最后就是rInflate方法:

private void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs) 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)) { parseRequestFocus(parser, parent); } else if (TAG_INCLUDE.equals(name)) { if (parser.getDepth() == 0) { throw new InflateException("<include /> cannot be the root element"); } parseInclude(parser, parent, attrs); } else if (TAG_MERGE.equals(name)) { throw new InflateException("<merge /> must be the root element"); } else { final View view = createViewFromTag(name, attrs); final ViewGroup viewGroup = (ViewGroup) parent; final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs); rInflate(parser, view, attrs); viewGroup.addView(view, params); } } parent.onFinishInflate(); } 

实这个方法也很简单,就是通过parser解析xml节点再生成对应View的过程。
XML转换成View的过程就是这样了,如有错误之处,还望指正,回到本文开头,其实我们还可以这样写:

@Override  
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); View content = LayoutInflater.from(this).inflate(R.layout.activity_main, null); setContentView(content); } 
 
界面2

大家发现问题没,相较于本文开头的写法,后面的灰色布局变成全屏了,我们来看看xml代码:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    xmlns:tools="http://schemas.android.com/tools"  
    android:layout_width="300dip"  
    android:layout_height="300dip" android:background="#888888" tools:context=".MainActivity" > <ImageView android:layout_width="200dip" android:layout_height="200dip" android:background="#238712" android:contentDescription="@null" /> </RelativeLayout> 

我明明设置了RelativeLayout的宽度和高度分别为300dip,但为什么全屏了?这是因为layout_width和layout_height是相对于父布局而言的,我们这里inflate的时候设置的父布局为null,所以这个属性设置也就无效了,指定一个父布局就可以了,例如:

@Override  
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); RelativeLayout rootView = new RelativeLayout(this); View content = LayoutInflater.from(this).inflate(R.layout.activity_main, rootView); setContentView(content); } 

现在,界面显示效果就和“界面1”相同了。


转载自: https://www.jianshu.com/p/b081e3fbe4ee

转载于:https://www.cnblogs.com/linghu-java/p/9156635.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值