Android UI 绘制过程浅析(一)LayoutInflater简介
前言
这篇blog是我在阅读过csdn大牛郭霖的《带你一步步深入了解View》一系列文章后,亲身实践并做出的小结。作为有志向的前端开发工程师,怎么可以不搞懂View绘制的基本原理——简直就像做后端却对数据库一无所知一样不可原谅!
“纸上得来终觉浅,绝知此事要躬行。” 尽管自己对View的绘制仍然处于一知半解的程度,但凡事总要经过从0到1,方能从1到100。今天暂且记录下此时的理解与实践,作为千里之行中的小小一步。
综述
本篇blog先从自己平时最常用到的,在代码中引入布局文件的写法LayoutInflater.inflate讲起,探究inflate内部的机制;随后着手View绘制的三个阶段measure、layout、draw,看看一个View/ViewGroup是如何被绘制到屏幕上的;最后实现一个自定义的View。
inflate
初次接触inflate这个单词时,我在查阅词典后,知道了它的释义是“充气、膨胀”——老外定义函数名果然很恰当。想象一个没有充气的皮囊(布局文件),在充气(inflate)后变成了一件栩栩如生的女朋友立体物件(手机屏幕上的真实显示),相应地,LayoutInflater就被我翻译成了“打气筒”。布局文件我们每个人都很了解,无非是一个XXXLayout,内部再装上一些控件。那么问题来了,inflate方法内部是如何解析这一层层的View、决定它们在屏幕上显示的前后顺序、处理隐藏/显示逻辑的呢?下面是获得“打气筒inflater”的写法:
1. 通过LayoutInflater提供的静态方法,从上下文中获取
LayoutInflater inflater = LayoutInflater.from(context);
2. 通过Service获取,上面的写法最终也是通过如下方法进行调用的
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflate方法的三个参数
在使用inflate方法时,我们都会注意到它需求三个参数
public View inflate (int resource, ViewGroup root, boolean attachToRoot);
另一种inflate方法不需要第三个attachToRoot参数,从源码上看,只是在上面方法的基础上进行了一次包装而已。
public View inflate(int resource, ViewGroup root) {
return inflate(resource, root, root != null);
}
三个参数定义如下:
- resource:布局文件id;not nullable
- root:本次生成的布局外部嵌套的父布局;nullable
- attachToRoot:是否将本次生成的布局加入到父布局
不论调用何种包装后的方法,最终使用到的inflate方法如下:
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate"); 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, false, false);
} else {
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, attrs, false); 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, 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;
} Trace.traceEnd(Trace.TRACE_TAG_VIEW); return result;
}
}
方法的注释中,有几处包含重要信息重要的地方:
- 此方法依赖于编译阶段对xml文件的预处理,因此,在运行时修改XmlPullParser是行不通的
- 参数root可选,当不为null时,起到的作用是为新生成的View提供LayoutParams的限制(在下一篇blog我们会看到,View绘制过程中,最终决定其尺寸的,是“父视图的支持尺寸”与“子视图的需求尺寸”)
- 参数attachToRoot与参数root共同作用,只有 root非空&&attachToRoot==true 时,才会真正地发生attach;若 root==null && attachToRoot==true ,也不会发生attach行为
inflate方法内部
阅读上面的代码段,在inflate方法中,首先通过while循环找到开始Tag(这里使用了XmlPullParser,有兴趣的话可以深入去研究这个Xml解析接口,对于帮助理解XML文件结构很有帮助),如果发现这是一个merge节点,则调用rInflate(parser, root, attrs, false, false)。使用过merge节点的话,就会明白这是一个可以有效降低布局复杂度的技巧。这里暂时记下,后续我会专门写一篇blog研究merge(当前只需要记住一点,就是当使用merge节点时,该节点一定是根节点,且inflate的参数parent不为null,attachToRoot==true)。如果这个Tag不是merge节点,则首先根据这个Tag生成对应的View
final View temp = createViewFromTag(root, name, attrs, false);
createViewFromTag 所做的事情,主要是通过Tag的name,在当前Context的ClassLoader中加载对应的Class,拿到clazz后,通过newInstance生成目标View。可以看到不论首个Tag是否为merge,最终辗转都是走到rInflate方法中的,从方法命名开头的“r”就可以看出,这是一个递归解析xml的过程,如注释
/**
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, boolean inheritContext) 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_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, parent, attrs, inheritContext);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
} else {
final View view = createViewFromTag(parent, name, attrs, inheritContext);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflate(parser, view, attrs, true, true);
viewGroup.addView(view, params);
}
} if (finishInflate) parent.onFinishInflate();
}
方法后半部是递归调用的过程:首先使用之前提到的createViewFromTag方法生成一层View,接着以这个View作为Parent,去继续rInflate子View们,解析到最后,调用parent.onFinishInflate(),告知上层已经解析完成。
至此为止,一个完整的inflate过程已经被解析完成了。下面我们结合具体的Demo代码,进一步巩固之前理解的知识。
Demo
很多新人在最初接触inflater时,往往困惑inflate方法后两个参数要怎么传,有时为了图方便,往往把 root = null, attachToRoot= false 一传了之。尽管大部分时间,这样处理是不会暴露出什么问题的,可是一旦习惯了这种写法,在真正出问题时就会一头雾水——“之前自己这么做明明没有问题的啊,这次怎么就不行了呢?!”。那么,我们就用下面这个demo来加深对inflate的理解。
demo的布局很简单,一个空的FrameLayout,作为Activity的背景
fake_main_activity.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fake_main_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
> </FrameLayout>
一个TextView的布局,准备将其安插在上面的FrameLayout里面
textview_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="300dp"
android:layout_height="300dp"
android:background="@color/blue"
android:text="Hello View!"
android:textColor="@color/red"
android:textSize="@dimen/text_size_34"> </TextView>
在FakeMainActivity中,如下设置页面布局,并加入上面的TextView
FakeMainActivity.java
package com.leili.imhere.activity; import android.app.Activity;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup; import com.leili.imhere.R; /**
* 写blog专用测试Activity
* Created by Lei.Li on 8/23/15 2:16 PM.
*/
public class FakeMainActivity extends Activity {
private ViewGroup fakeMainLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
super.setContentView(R.layout.fake_main_activity);
fakeMainLayout = (ViewGroup) super.findViewById(R.id.fake_main_layout); // 方法 1. root不为null,无需addView
View textView = LayoutInflater.from(this).inflate(R.layout.textview_layout, fakeMainLayout); // 方法 2. root为null,手动addView
// View textView = LayoutInflater.from(this).inflate(R.layout.textview_layout, null);
// fakeMainLayout.addView(textView);
}
}
可以看到这里我们使用了parent != null的inflate方法,屏幕截图是这样的,其中TextView的宽高均为300dp,符合我们的预期(在TextView布局文件里声明的宽高)
如果我们注释掉方法1,使用方法2,即用 parent = null 来调用inflate,然后用ViewGroup.addView来将生成的View加入到外层FrameLayout中,会看到下面的屏幕截图
是不是很奇怪?明明声明了TextView的宽高为300dp,这里它却占满了整个屏幕!造成这个现象的原因就在于inflate时没有为TextView声明ParentView,导致其layout_width/layout_height两个参数生效。在刚接触android时,我们往往会直观地把 layout_width/layout_height 这两个参数理解为View的宽与高,以为在布局文件里声明了多少的数值,最终绘制后就会出现多少的数值。这是有失偏颇的,要知道,这两个参数之所以不简单地叫做width/height,就是因为需要处理layout的过程。简言之layout_width/layout_height这两个参数,是用来告诉ParentView,自己需求多大的尺寸的。而如果在inflate时没有指明ParentView(如我们在方法2中所做的),子View就不知道该把layout_width/layout_height传给谁,这两个参数也就自然被忽略了。
一个有趣的地方是,如果在上面的demo中,把最外层的FrameLayout改为LinearLayout,同样使用方法2(不指明ParentView),最终看到的是如下布局——TextView的宽度是match_parent,高度是wrap_content。究其原因,在于FrameLayout与LinearLayout这两种ViewGroup对于子View处理的方式不同。
需要强调的一点是,当我们在Activity.onCreate()中使用 setContentView(int resId) 来设置页面背景时,Android系统为我们在外层自动嵌套了一个宽高都是满屏的FrameLayout,所以我们使用的背景资源文件根节点所声明的layout_width/layout_height是有效的。
小结
本篇blog从源码角度解析了 LayoutInflater 如何根据布局文件生成布局的过程——通过XmlParser递归地对xml文件进行解析,并佐以demo,指出了日常开发中一个容易产生潜在错误的用法。
有了这里的基础,在下篇blog中,将迎来View绘制中最最核心的三个过程:measure、layout、draw,让我们一起拭目以待。
Android UI 绘制过程浅析(一)LayoutInflater简介的更多相关文章
- Android UI 绘制过程浅析(五)自定义View
前言 这已经是Android UI 绘制过程浅析系列文章的第五篇了,不出意外的话也是最后一篇.再次声明一下,这一系列文章,是我在拜读了csdn大牛郭霖的博客文章<带你一步步深入了解View> ...
- Android UI 绘制过程浅析(二)onMeasure过程
前言 View的绘制过程分为 measure.layout.draw三个步骤,接下来对这三个步骤逐一进行研究. measure方法的签名 public final void measure(int w ...
- Android UI 绘制过程浅析(三)layout过程
前言 上一篇blog中,了解到measure过程对View进行了测量,得到measuredWidth/measuredHeight.对于ViewGroup,则计算出全部children的宽高进行求和. ...
- Android UI 绘制过程浅析(四)draw过程
前言 draw是绘制View三个步骤中的最后一步.同measure.layout一样,通常不对draw本身进行重写,draw内部会调用onDraw方法,子类View需要重写onDraw(Canvas) ...
- Android UI绘制流程及原理
一.绘制流程源码路径 1.Activity加载ViewRootImpl ActivityThread.handleResumeActivity() --> WindowManagerImpl.a ...
- Android View绘制过程
Android的View绘制是从根节点(Activity是DecorView)开始,他是一个自上而下的过程.View的绘制经历三个过程:Measure.Layout.Draw.基本流程如下图: per ...
- Android View 绘制过程
Android的View绘制是从根节点(Activity是DecorView)开始,他是一个自上而下的过程.View的绘制经历三个过程:Measure.Layout.Draw.基本流程如下图: per ...
- 简单研究Android View绘制一 测量过程
2015-07-27 16:52:58 一.如何通过继承ViewGroup来实现自定义View?首先得搞清楚Android时如何绘制View的,参考Android官方文档:How Android Dr ...
- 一篇文章教你读懂UI绘制流程
最近有好多人问我Android没信心去深造了,找不到好的工作,其实我以一个他们进行回复,发现他们主要是内心比较浮躁,要知道技术行业永远缺少的是高手.建议先阅读浅谈Android发展趋势分析,在工作中, ...
随机推荐
- iOS流量监控
http://code4app.com/snippets/one/iOS%E6%B5%81%E9%87%8F%E7%9B%91%E6%8E%A7/5020ba7a6803fae325000000 1. ...
- javascript的坑
1 for in循环:使用它时,要主要遍历的是所有可枚举的属性(实例以及原型中的属性) function Person(name){ this.name = name; } Person.protot ...
- dom自定义属性 兼容 index值获取
function getIndex(Eve,obj){ for(var i = 0;i<obj.length;i++){ obj[i].setAttribute("index" ...
- 64位 ubuntu android studio gradle 权限不够 缺少文件和权限导致
安装 32位 库文件 sudo apt-get install lib32z1 给文件夹加权限 chmod 777 -R SDK chmod 777 -R android-studio -R表示所有 ...
- Python的平凡之路(4)
一.迭代器&生成器 生成器定义: 通过列表生成式,我们可以直接创建一个列表.但是,受到内存限制,列表容量肯定是有限的.而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅 ...
- Java语言概述
1.1 基础知识 ·第一代语言 打孔机--纯机器语言 ·第二代语言 汇编 ·第三代语言 C.Pascal.Fortran面向过程的语言 C++面向过程/面向对象 Java跨平台的纯面向对象的语言 .N ...
- sqlserver数据库学习(-)数据类型
ecimal 数据类型最多可存储 38 个数字,所有数字都能够放到小数点的右边.decimal 数据类型存储了一个准确(精确)的数字表达法:不存储值的近似值. 定义 decimal 的列.变量和参数的 ...
- iOS学习之GCD
多线程编程 线程定义:一个CPU执行的CPU命令 列一条无分叉的路径就叫线程. 多线程:执行多个不同的CPU命令 有多条路径. 线程的使用:主线程(又叫作UI线程)主要任务是处理UI事件,显示和刷新U ...
- HOW TO BE SINGLE 最后那段的摘录
我一直在思考我们不得不单身的时间这个时间我们需要擅长一个人独处但是有多少独处的状态是我们想要拥有的呢难道不是件很危险的事情吗当你适应状态并且如鱼得水的时候所以当你安定下来 你就会与某人擦肩而过吗 有些 ...
- C++ Daily《2》----vector容器的resize 与 reserve的区别
C++ STL 库中 vector 容器的 resize 和 reserve 区别是什么? 1. resize 改变 size 大小,而 reserve 改变 capacity, 不改变size. 2 ...