前言
这篇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发展趋势分析,在工作中, ...
随机推荐
-
js实现标准无缝滚动
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
-
动画原理——脉动(膨胀缩小)&;&;无规则运动
书籍名称:HTML5-Animation-with-JavaScript 书籍源码:https://github.com/lamberta/html5-animation 1.脉动是一种半径r来回反复 ...
-
iframe跨域动态设置主窗口宽高
Q:在A项目的a页面嵌入一个iframe,src是B项目的b页面,怎样让a页面的高度跟b页面的高度一样? A:解决跨域方案:增加一个A项目的c页面. 操作步骤: 一,a页面的iframe设置: 获取到 ...
-
springcoud feign超时的问题
配置 #开启超时控制 打开feign-hystix feign.hystrix.enabled=true ribbon.ReadTimeout= ribbon.ConnectTimeout= #如果e ...
-
【平差软件学习---科傻】四、科傻二等水准平差(参数设置和in1文件讲解)
[平差软件学习---科傻]四.科傻二等水准平差(参数设置和in1文件讲解) 这个算是最后一集了,也可能不是如果我想到不足的地方我会在补上一集视频,或者是文章页.总感觉自己操作的很熟练,到自己真正讲的时 ...
-
eclipse搭建ssm框架
新建数据库ssm 建立数据库表user CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT , `sex` varchar(255) ...
-
Linux设备驱动剖析之SPI(一)
写在前面 初次接触SPI是因为几年前玩单片机的时候,由于普通的51单片机没有SPI控制器,所以只好用IO口去模拟.最近一次接触SPI是大三时参加的校内选拔赛,当时需要用2440去控制nrf24L01, ...
-
这个PHP无解深坑,你能解出来吗?(听说能解出来的都很秀)
欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由horstxu发表于云+社区专栏 1. 问题背景 PHP Laravel框架中的db migration是比较常用的一个功能了.在每个 ...
-
Java并发编程--3.Lock
Lock接口 它提供3个常用的锁 lock() : 获不到锁就就一直阻塞 trylock() :获不到锁就立刻放回 或者 定时的,轮询的获取锁 lockInterruptibly() : 获不到锁时阻 ...
-
设计模式之装饰模式(Java实现)
“怎么了,鱼哥?” “唉,别提了,网购了一件衣服,结果发现和商家描述的差太多了,有色差就算了,质量还不好,质量不好就算了,竟然大小也不行,说好的3个X,邮的却是一个X的,不说了,退货去.你先开讲吧,你 ...