Android View 源码 分析 之 LayoutInFlater
来源:互联网 发布:深入理解nginx 百度云 编辑:程序博客网 时间:2024/06/06 03:38
代码基于4.1.2
导读:
View的重要性不言而喻,想起有时动态添加一个View时,不知为什么宽高总是不能随心所欲,是不是感觉虚了不少?所以把View的原理了然于胸之后就不用虚了。我会尽量在每行代码上加注释,并在要讲解的方法源码上面对其参数进行说明,方法下面进行总结,希望这样可以方便阅读和理解。如有不对的地方,还请大家多多指正。
要开始研究View就不得不从创建View来开始,那么请听我说,从前……
创建出一个View实例的步骤如下
LayoutInflater layoutInflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);View view = layoutInflater.inflate(R.layout.main,null);//第二个参数为此View的父控件,下面会有详解//或者LayoutInflater layoutInflater = LayoutInflater.from(context); View view = layoutInflater.inflate(R.layout.main,null);//第二个参数为此View的父控件,下面会有详解
两种原理都是一样,请根据个人喜好任选其一。
我们先做一个小Demo让一个简单的View加载到页面里,然后在一步一步的分析。
Activity代码
public class ViewDemoActivity extends Activity { private LinearLayout linearLayout; @Override public void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); setContentView(R.layout.viewdemo); //创建一个View LayoutInflater layoutInflater = LayoutInflater.from(this); View view = layoutInflater.inflate(R.layout.button,null); //添加到Activity的layout中,这部分将在下一章节里讲解 linearLayout = (LinearLayout)this.findViewById(R.id.viewdemoId); linearLayout.addView(view); }}Activity中layout布局,只是添加了一个id
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/viewdemoId"android:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent"></LinearLayout>
在layout文件夹中添加xml文件(例如m_button.xml),只是设定一个button
<Button xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="300dip"android:layout_height="wrap_content"android:text="Button"></Button>效果图,上面button明明设定高是300dip 为什么显示出来的是系统默认值呢?下面有详解
用户调用的inflater()都会被跳转到下面这个inflater()方法,除非直接调用这种inflater(),有三个参数:
● resource(int),布局文件资源。
● root(ViewGroup),所创建View的父布局,设置为null的话,就是告诉系统此View不知道放在哪个父控件上一切都听系统的安排,由于没有老大罩着所以我们上面设置android:layout_height的权利会被剥夺掉,在创建的时候会被系统重新生成一个默认值,所以如果想自己绘制宽高的话需把父控件添加上。
● attachToRoot(boolean),是否要把创建出来的View载入到上一个参数(父布局)中,如果设置为false了就算指定父布局也不会引用,相当于开关。
public View inflate(int resource, ViewGroup root, boolean attachToRoot) { //把布局文件加载到XmlResourceParser解析器中 XmlResourceParser parser = getContext().getResources().getLayout(resource); try { //下面有详解 return inflate(parser, root, attachToRoot); } finally { parser.close(); }}
通过上面的讲解我们知道了,创建View如果想让自己设定的宽高有效就要给他指定一个父布局,并attachToRoot参数设置为true或者不设置。那是不是这样就可以了呢,我们来做个Demo测试一下。
其他代码不变,只在Activity中为创建的View指定父布局
public class ViewDemoActivity extends Activity { private LinearLayout linearLayout; private View view; @Override public void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); setContentView(R.layout.viewdemo); linearLayout = (LinearLayout)this.findViewById(R.id.viewdemoId); LayoutInflater layoutInflater = LayoutInflater.from(this); view = layoutInflater.inflate(R.layout.button,linearLayout); linearLayout.addView(view); }}
3、2、1 运行…
哎呀?崩溃了… 我们看看什么错
Caused by: java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
意思是说它已经有一个父布局了,在调用addView()时重复添加父布局,怎么会重复添加父布局呢?好吧,只好继续往里面看看源代码是怎么写的了。我们知道上面那个inflate(),只是将布局文件加载到解析器中然后把解析器传给下面这个最终版inflate()。
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) { synchronized (mConstructorArgs) { final AttributeSet attrs = Xml.asAttributeSet(parser); Context lastContext = (Context)mConstructorArgs[0]; //这里装载的就是此View运行所在对象的Context 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(); //判断当前标签是否为"merge",如果是"merge",直接调用rInflate()来生成View if (TAG_MERGE.equals(name)) { //merge创建出来的View是需要宿主的,必须是在ViewGroup下面且attachToRoot参数为true。 if (root == null || !attachToRoot) { throw new InflateException("<merge /> can be used only with a valid " + "ViewGroup root and attachToRoot=true"); } //生成View,下面有详解 ↓ rInflate(parser, root, attrs, false); } else { // Temp is the root view that was found in the xml // 创建一个当前xml中的根View View temp; //判断当前标签是不是"blink" if (TAG_1995.equals(name)) { //如果是"blink"则生成一个有闪烁效果的View,此View每隔0.5s会invalidate一次 ↓ temp = new BlinkLayout(mContext, attrs); } else { //如果不是"blink" ↓ temp = createViewFromTag(root, name, attrs); } ViewGroup.LayoutParams params = null; //如果创建的View有父布局 if (root != null) { // Create layout params that match root, if supplied // 得到父布局的layoutParams params = root.generateLayoutParams(attrs); //如果inflate第三个参数为false则temp用父布局的layoutParams 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 // 创建temp下面所有的子view rInflate(parser, temp, attrs, true); // We are supposed to attach all the views we found (int temp) // to root. Do that now. // 如果有父布局且第三个参数不手动设置为false,无需手动调用addView(),父布局自动添加所要添加的View,最后返回的是父布局 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. // 如果没有父布局或者有父布局但是第三个参数手动设置为false则返回所创建的View,系统不会自动调用addView() 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; }}
此方法有2个分支
一、在23行中判断根节点是不是merge,如果是则在第30行直接处理当前节点的子节点,目的是为了减少UI层级(不熟悉merge属性的请google),如果不是则进入第二个分支。
二、在36行中判断当前标签是不是blink,这个对理解影响不大下面再讲,45-55行是配置LayoutParams,然后是59行创建View。接下来重点来了,第64行判断如果有父布局且attachToRoot参数为true那么系统会自动添加到父布局中,这…下知道了为什么我在创建完View之后再调用addView()方法后出报出重复添加的错误了。OK,那我把addView()去掉试试。
代码如下
public class ViewDemoActivity extends Activity { private LinearLayout linearLayout; private View view; @Override public void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); setContentView(R.layout.viewdemo); linearLayout = (LinearLayout)this.findViewById(R.id.viewdemoId); LayoutInflater layoutInflater = LayoutInflater.from(this); view = layoutInflater.inflate(R.layout.button,linearLayout);// linearLayout.addView(view); }}4、3、2、1 运行…
终于成功了。。
由此可知,动态添加View如果想控制其宽高的话就要指定其父布局,且不要自己调用addView()哦。
接下来我们先来看上面用到的BlinkLayout类和createViewFromTag方法,因为在接下来的rInflate()方法中主要用到的就是这他们俩。
首先说BlinkLayout这个类很有意思,它可以使你的控件不断闪烁。我们来做一个例子
我们只需在xml文件中操作即可
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/viewdemoId" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:textSize="25sp" android:layout_height="wrap_content" android:text="Hello Marco"/> <blink android:layout_width="wrap_content" android:layout_height="wrap_content"> <TextView android:id="@+id/textView" android:textSize="25sp" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello Marco"/> </blink></LinearLayout>效果图(字体是我在java里面手动改变的)
我们一般用不到blink,其源码如下。
private static class BlinkLayout extends FrameLayout { private static final int MESSAGE_BLINK = 0x42; private static final int BLINK_DELAY = 500; private boolean mBlink; //限制dispatchDraw()只绘制一次组件 private boolean mBlinkState; private final Handler mHandler; public BlinkLayout(Context context, AttributeSet attrs) { super(context, attrs); mHandler = new Handler(new Handler.Callback() { @Override public boolean handleMessage(Message msg) { if (msg.what == MESSAGE_BLINK) { if (mBlink) { mBlinkState = !mBlinkState; makeBlink(); } //请求重新绘制本视图 invalidate(); return true; } return false; } }); } //重新绘制的入口方法 private void makeBlink() { Message message = mHandler.obtainMessage(MESSAGE_BLINK); mHandler.sendMessageDelayed(message, BLINK_DELAY); } //默认在第一次onDraw之前调用 @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); mBlink = true; mBlinkState = true; makeBlink(); } //在销毁View时调用 @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mBlink = false; mBlinkState = true; mHandler.removeMessages(MESSAGE_BLINK); } //绘制容器组件时会调用 在onDraw之后 @Override protected void dispatchDraw(Canvas canvas) { if (mBlinkState) { super.dispatchDraw(canvas); } } }
下面我们来讲createViewFromTag()方法
View createViewFromTag(View parent, String name, AttributeSet attrs) { //如果标签为view if (name.equals("view")) { name = attrs.getAttributeValue(null, "class"); } try { View view; //如果有预先定义Factory则会初始化成我们预先设定好的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); } if (view == null) { if (-1 == name.indexOf('.')) { //如果标签中没有"."这个符号则说明是系统的view view = onCreateView(parent, name, attrs); } else { //有"."则说明是自定义组件 view = createView(name, null, attrs); } } return view; } catch (InflateException e) { throw e; } catch (ClassNotFoundException e) { InflateException ie = new InflateException(attrs.getPositionDescription() + ": Error inflating class " + name); ie.initCause(e); throw ie; } catch (Exception e) { InflateException ie = new InflateException(attrs.getPositionDescription() + ": Error inflating class " + name); ie.initCause(e); throw ie; }}
这个方法主要就是用到了两个方法onCreateView()、createView().
1.判断是否有Factory,如果有则返回加载的Factory。
2.如果没有Factory,则判断要创建的View是系统自带的还是我们自己创建的。
3.如果是系统自带的view则会调用onCreateView(),然后自动在前缀上添加”android.view.”例如”android.view.ImageView”。
4.如果是自定义组件则会调用createView(),然后在前缀上添加例如”com.example.mImageView”。
下面是onCreateView()最终调用方法的源代码,非常简洁
protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { return createView(name, "android.view.", attrs);}
接下来是createView()方法的源代码,这个是真正创建View的方法
public final View createView(String name, String prefix, AttributeSet attrs)throws ClassNotFoundException, InflateException { //缓存构造对象,指向的是一个HashMap,其中缓存着使用过的UI控件 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); //mFilter 是一个 Filter对象,为一个过滤器 if (mFilter != null && clazz != null) { //来通过 mFilter 是否同意创建由class对象所描述的UI控件 boolean allowed = mFilter.onLoadClass(clazz); //如果 mFilter 不同意创建此UI控件则抛出异常 if (!allowed) { failNotAllowed(name, prefix, attrs); } } //创建UI组件的构造对象 constructor = clazz.getConstructor(mConstructorSignature); //添加入HashMap中,供可重复利用 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); //检测mFilterMap中是否有对应的映射,没有就报错 if (allowedState == null) { // New class -- remember whether it is allowed clazz = mContext.getClassLoader().loadClass( prefix != null ? (prefix + name) : name).asSubclass(View.class); 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); } } } //创建view两要素,一个是Context,另一个是AttributeSet缺一不可。 Object[] args = mConstructorArgs; args[1] = attrs; //反射出的view final View view = constructor.newInstance(args); //判断映射出来的view是否为ViewStub的实例,ViewStub就是可以不消耗资源就可以隐藏UI的组件 if (view instanceof ViewStub) { // always use ourselves when inflating ViewStub later final ViewStub viewStub = (ViewStub) view; viewStub.setLayoutInflater(this); } //啊 return view; } catch (NoSuchMethodException e) { InflateException ie = new InflateException(attrs.getPositionDescription() + ": Error inflating class " + (prefix != null ? (prefix + name) : name)); ie.initCause(e); throw ie; } catch (ClassCastException e) { // If loaded class is not a View subclass InflateException ie = new InflateException(attrs.getPositionDescription() + ": Class is not a View " + (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; } }
此方法大致步骤如下
1.获取对应的缓存对象(缓存的目的就是防止例如多处用到TextView而创建多次TextView这种浪费资源的事。)
2.没有就创建
3.有的话看是否是ViewStub的实例,是的话设置其LayoutInflater.
4.返回View
创建View的大体框架我们已经分析完成了,具体如何从创建View的细节将在后面章节解析。
接下来我们来看今天的最后一个方法rInflate()
void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException { final int depth = parser.getDepth(); int type; //遍历parent每一行 while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { //找到START_TAG节点后开始解析 if (type != XmlPullParser.START_TAG) { continue; } //获取节点名称 final String name = parser.getName(); //接下来就是根据各个节点来创建对应的view了 if (TAG_REQUEST_FOCUS.equals(name)) { //TAG_REQUEST_FOCUS节点 parseRequestFocus(parser, parent); } else if (TAG_INCLUDE.equals(name)) { //TAG_INCLUDE节点 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 if (TAG_1995.equals(name)) { //上面有讲过 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 { //自定义节点 final View view = createViewFromTag(parent, name, attrs); final ViewGroup viewGroup = (ViewGroup) parent; final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs); rInflate(parser, view, attrs, true); viewGroup.addView(view, params); } } //当前View创建完成之后会通知其父类。 if (finishInflate) parent.onFinishInflate(); }
如果上面的内容理解的差不多了,那么再来看这个方法就好受多了,此方法的思路其实就是利用递归来将其所有的view创建出来。
总结:
这一模块最重要的一点就是,在动态添加View时,如果想设定宽高的话一定要指定它的父View,并且不要写addView()方法,因为指定父View的话,源码会自动调用addView()方法的;
- Android View 源码 分析 之 LayoutInFlater
- 源码分析之LayoutInflater
- 源码分析之LayoutInflater
- LayoutInflater源码分析之Inflate
- Android LayoutInflater原理分析,了解View(一)
- Android源码分析之---View.MeasureSpec 解析
- Android 系统源码分析之View(一)
- Android自定义View之常用工具源码分析
- Android 自定义 View 之 onLayout 源码分析
- Android自定义View之常用工具源码分析
- Android笔记之使用LayoutInflater创建View
- Android LayoutInflater 源码分析及个人总结
- Android面试题-LayoutInflater源码分析
- Android中将xml布局文件转换为View树的过程分析(下)-- LayoutInflater源码分析 - xiaoweiz
- android之LayoutInflater以及setFactory源码解读
- Android源码解析之LayoutInflater原理
- android应用程序窗口框架学习(2)-view绘制流程源代码解析-setContentView与LayoutInflater加载解析机制源码分析
- Android View与LayoutInflater
- Android4.4----Vold挂载管理分析USB挂载(四)
- cjson中的稀疏数组encode
- CrashMonkey4Android-如何进行二次开发
- C# 网页 if(!IsPostBack)的使用?
- 从今天开始,坚持写博客
- Android View 源码 分析 之 LayoutInFlater
- 针对ns3所用bake的更新
- Objective-C Runtime 运行时之二:成员变量与属性
- HttpClient模拟表单传图片
- RESTful架构理解
- 如何定位Obj-C野指针随机Crash(二):让非必现Crash变成必现
- length()代替equals()检验字符串是否为空串
- 用两个栈实现队列
- Objective-C测试题