Android UI测量、布局、绘制过程探究
在上一篇博客《Android中Activity启动过程探究》中,已经从ActivityThread.main()开始,一路摸索到ViewRootImpl.performTraversals()了。本篇就来探究UI的绘制过程。
performTraversals()方法非常长,其中关键性的三个步骤是依次调用了performMeasure(), performLayout(), performDraw()。分别来看这三个步骤吧!
Measure过程(测量过程)
直接来看performMeasure()方法。
该方法非常直接了当的调用了mView的measure()方法。mView是一个android.view.View对象,在ViewRootImpl类中的mView是整个UI的根节点,实际上也就是PhoneWindow中的mDecor对象,再说具体点,就是一个Activity所对应的一个屏幕(不包括顶部的系统状态条)中的视图,包括可能存在也可能不存在的ActionBar。
接着来看View.measure()方法。请注意红色标记的部分。
该方法中调用了View.onMeasure()方法。View.onMeasure()非常的简单,就是直接调用了一个setMeasureDimension()的方法。
而setMeasureDimension()方法中最关键的步骤是对View的两个成员变量进行一次赋值,如下图所示,请注意红色标记的部分:
显然,View的onMeasure()方法似乎有点过于简单了,都没有明白所谓的measure步骤到底做了什么事情。经过笔者这两天的研究,已经大致明白了measure步骤是干了什么,这里我先将结论拿出来说:(以下内容非常关键,请仔细阅读(⊙o⊙))
View.onMeasure()方法的作用是:设置自己所需要的大小。
对于单一的简单一个View对象来说,就是这么简单,比如说一个TextView,要显示一段文字,那么当调用onMeasure()方法,这个方法的目的就是要给这个TextView设置好它所需要的宽度和高度,在程序代码中的体现,实际上就是mMeasuredWidth和mMeasuredHeight两个成员变量,调用setMeasuredDimension()为这两个变量赋值。
这时问题来了,挖掘机技术到底哪家强?\(^o^)/~
这时问题来了,TextView的高度和宽度是多少呢?我们回想到我们使用TextView的时候有两个很重要的参数,在xml中叫layout_width, layout_height。在代码中是LayoutParams对象的两个属性,也分别表示宽度和高度。
而这个数值有如下几种情况:
1.match_parent:填充父控件,与父控件一样大
2.wrap_content:包裹内容,仅仅将自己的内容包裹起来那么大就足够了
3.指定的具体数值,30dp,55px之类的。
接下来,TextView需要给自己设置大小,
如果是match_parent的情况,那么就需要和父控件一样大。
如果是wrap_content的情况,那么我需要知道自己显示出的文字的高度和宽度是多少。
如果是具体的数值,我就直接显示好了,当然,不能超过父控件的大小。
很显然,TextView想要准确的计算出自己所需要的大小需要得知两个信息,一个是我的宽度高度的属性是什么,另一个是父控件有多大,或者说是我能够使用多大的空间。
而这一两组数据在宽度和高度两个值中都需要得到体现,常理来说我们需要四个参数。但是Android很巧妙的用两个参数就解决了问题。
请注意onMeasure()的两个整型参数。widthMeasureSpec, heightMeasureSpec。
这两个数值都是int型的,在Java中,int型的变量是32位的,而Android的设计者将这个整型的最高两位当做type来用,低30位当做数值来用。
高2位总共可以组成三种情况,分别为:AT_MOST、UNSPECIFIED、EXACTLY。
低30位所表示的整型数值,表示着,当前控件可用的最大宽度与高度。再详细解释下所谓可用的最大宽度和高度:对于ViewRoot来说,那么它的可用宽度和可用高度就是屏幕的尺寸,假如它设置了一个padding = 10,上下左右都为10px。那么内部的东西就不能放置在padding中,所以挨个调用子控件的onMeasure()方法的时候,给它的可用尺寸就是(屏幕宽度 - 20,屏幕高度 - 20)(左右两边都有padding嘛~)。就是这么回事。
这两个特殊的整型参数不需要我们手动的去做位移操作来取有意义的数值,可以使用MeasureSpec.getMode()、MeasureSpec.getSize()来获取。
当onMeasure()执行完毕后,就可以调用该View的getMeasuredWidth()和getMeasuredHeight()来获取这个控件的高度与宽度了。
所以总结下onMeasure()的作用:
1.onMeasure()方法是measure()调用的。
2.onMeasure()方法的作用是要计算出当前控件自身所需要的大小是多少,计算的根据是在xml或者代码中设置的宽度和高度的参数,参数指明了要求你是填充父控件(match_parent)还是包裹内容(wrap_content)还是精确的一个大小,但最终你的大小不应该超过父控件给你提供的空间。
3.onMeasure()方法结束之前必须调用setMeasuredDimension()来设置View.mMeasuredWidth和View.mMeasuredHeight两个参数。这个方法的两个整型是单纯的表示宽度与高度,整个32位都是用来表示数值。
4.onMeasure()方法执行完毕后,该View的尺寸已经得到确认,需要使用的话,调用View.getMeasuredWidth()和View.getMeasuredHeight()来获取。
以上就是measure过程的内容。接下来measure过程一路递归调用子类的onMeasure(),一路退栈返回,终于把所有的measure全部执行完了,所有的控件都已经知道了自己的大小,就开始调用ViewRootImpl.performLayout()方法了。
Layout过程(布局过程)
在ViewRootImpl.performLayout()方法中,调用了根视图的layout()方法,也就是View.layout()方法。
接下来看View.layout()方法
layout()方法有四个参数,分别是left, top, right, bottom,它们是相对于父控件的位移距离。哎,我还是画个图吧~
如上图所示,left指的是该View的左边到其父控件左边的距离,top也是类似的意思,而right是left加上该控件的宽度,总结起来的话:就是该控件四条边到父控件左上角顶点的距离。
再回到代码,注意红色标记的部分,先调用了setFrame()方法。
setFrame()方法是个很重要的方法!
注意上面用红色标记的部分,其中newWidth并不是mMeasuredWidth,而是用right - left。难道我(View)的宽度值都不算数吗?要通过所谓的右边减去左边来确定我的宽度?
没错就是这样,setFrame()的这个Frame,可以理解为一个View真正渲染到屏幕上的矩形区域。而四个参数left, top, right, bottom就是指定了这个矩形区域的四个顶点。
可以想象一下这样的情况,父控件的宽度是500,padding值为0,那么其子控件可用的宽度自然就是500了,假如有一个控件已经占满了300px的宽度,而另一个控件同样需要300px的宽度,而父控件只剩下了200px的宽度,Android是怎么处理这件事情的呢?
首先父控件调用onMeasure()方法,遍历子控件,调用子控件的onMeasure()方法,这样一来大家都知道了自己有多大了。
然后父控件调用了onLayout()方法,onLayout()方法实际上是给自己的子控件布局,假如一个控件是View的话,它就没有子控件了,onLayout实际上就没什么作用。回到这个情景,onLayout()遍历的调用子控件的layout()方法,指定子控件的上下左右的位置,layout()方法中调用setFrame()方法,根据参数值设置自己实际渲染的区域。那么当第一个控件占了300px的宽度,这个时候父控件已经知道了剩下的可用宽度只有200px了,那么它就会根据这个值来进行调整,将计算好,根据剩下的空间把第二个子控件的上下左右四个参数交给它的layout方法,让他去设置自己的frame。也就是说,第二个空间的Frame的宽度只有200px了,并不会超出这个范围。
这里得出一个事实:measure出来的宽度与高度,是该控件期望得到的尺寸,但是真正显示到屏幕上的位置与大小是由layout()方法来决定的。left, top决定位置,right,bottom决定frame渲染尺寸。
回到源代码,以上的代码是View的,所以onLayout()中是空的。在具体的ViewGroup中有更加具体的实现。
接下来是对layout步骤的总结。(以下内容非常重要哦)
1.要设置一个View的位置与实际渲染的大小需要调用View.layout()方法。
2.layout()方法中的setFrame()方法是设置该控件的位置与实际渲染的大小。这是layout过程中最关键,最重要的步骤。
3.接下来就是递归,遍历子控件,并调用他们的onLayout()方法。
也就是说你需要实现一个ViewGroup的话,你只需要在onLayout()方法中遍历的调用子控件的onLayout()方法就行了,需要做的事情就是把left, top, right, bottom这四个值算好。
以上就是Layout过程。
Draw过程(绘制过程)
绘制过程没有研究的太详细,实际上它就是调用Canvas的接口了,然后Canvas又是和OPENGLES什么的相关,具体的绘图方法都是native的,没有看到,但onDraw()方法在我的了解下是这样的。就以TextView来解释。
TextView里面有很多属性,比如文字大小,文字颜色等等,根据这些属性设置Paint对象,然后在onDraw()方法里用这个paint对象把text的内容都画出来。当我们每次调用一个public的方法的时候,比如setText(),它就会先更改自身的属性,然后要求重新绘制一次,于是乎,onDraw()就又调用了一次。也就是说,onDraw()是已经将所有的属性都考虑了进来,并不是我们改变什么它就绘制什么,而是从头到尾都绘制一遍。
这就是我对Draw过程的理解。
最后再贴一个自己实现的LinearLayout,再来巩固下上面的measure和layout步骤,大家体会一下。只实现了垂直布局的部分。
/**
* 自己实现的LinearLayout
* @author kross(krossford@foxmail.com)
* @update 2014-10-16 19:42:47 第一次编写,实现垂直布局
* */
public class KLinearLayout extends ViewGroup { private static final String TAG = "KLinearLayout"; /** 垂直布局 */
public static final byte ORITENTATION_VERTICAL = 0x1;
/** 水平布局 */
public static final byte ORITENTATION_HORIZONTAL = 0x0; /** 线性布局的方向,默认值为水平
* @see #ORITENTATION_HORIZONTAL
* @see #ORITENTATION_VERTICAL */
private int mOritentation = ORITENTATION_HORIZONTAL; /** 最终的宽度 */
private int mWidth;
/** 最终的高度 */
private int mHeight; public KLinearLayout(Context context) {
super(context);
mOritentation = ORITENTATION_HORIZONTAL;
} /**
* 设置线性布局的方向:垂直或水平
* @param oritentation
* @see #ORITENTATION_HORIZONTAL
* @see #ORITENTATION_VERTICAL
* */
public void setOritentation(byte oritentation) {
mOritentation = oritentation;
} @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Log.i(TAG, "onMeasure");
if (mOritentation == ORITENTATION_HORIZONTAL) {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
} else {
measureVertical(widthMeasureSpec, heightMeasureSpec);
}
} @Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
Log.i(TAG, "onLayout l:" + l + " t:" + t + " r:" + r + " b:" + b); if (mOritentation == ORITENTATION_HORIZONTAL) {
layoutHorizontal(l, t, r, b);
} else {
layoutVertical(l, t, r, b);
}
} /**
* 垂直测量
* */
private void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
Log.i(TAG, "measureVertical"); int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec); /**
* 已经使用了的高度,容器是空的,已经使用的高度为0,如果已经存在一个高度为x的子控件,这个值为x。
* 这个值也表示,所有的子控件所需要的高度总值。
*/
int heightUsed = 0;
View childTemp = null;
for (int index = 0; index < getChildCount(); index++) { //遍历子控件
childTemp = getChildAt(index);
measureChildWithMargins(childTemp, widthMeasureSpec, 0, heightMeasureSpec, heightUsed); //获取子控件并测量它的大小
LinearLayout.LayoutParams childLp = (LinearLayout.LayoutParams)childTemp.getLayoutParams(); //子控件的高度,包括子控件的上下外边距一起累加到heightUsed值中
heightUsed = heightUsed + childTemp.getMeasuredHeight() + childLp.topMargin + childLp.bottomMargin;
//因为是垂直布局,所以宽度直选最大的一个
mWidth = Math.max(mWidth, childTemp.getMeasuredWidth() + childLp.leftMargin + childLp.rightMargin);
} mWidth = mWidth + getPaddingLeft() + getPaddingRight(); //加上左右内边距 switch (widthMode) {
case MeasureSpec.AT_MOST: //wrap_parent
mWidth = Math.min(widthSize, mWidth); //因为是包裹内容,所以宽度应该是尽可能的小
break;
case MeasureSpec.EXACTLY: //match_parent
mWidth = widthSize; //与父控件一样大,那么宽度应该是父控件给的,也就是参数所给的
break;
case MeasureSpec.UNSPECIFIED:
break;
} mHeight = heightUsed + getPaddingTop() + getPaddingBottom(); //所有子控件的高度和 + 上下内边距 switch (heightMode) {
case MeasureSpec.AT_MOST: //wrap_parent
mHeight = Math.min(heightSize, mHeight);
break;
case MeasureSpec.EXACTLY: //match_parent
mHeight = heightSize;
break;
case MeasureSpec.UNSPECIFIED:
break;
} setMeasuredDimension(mWidth, mHeight);
} /**
* 水平测量
* */
private void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) {
Log.i(TAG, "measureHorizontal");
setMeasuredDimension(100, 100);
} /**
* 垂直布局
* */
private void layoutVertical(int l, int t, int r, int b) {
int avaliableLeft = getPaddingLeft();
int avaliableTop = getPaddingTop(); View childTemp = null;
for (int i = 0; i < getChildCount(); i++) {
childTemp = getChildAt(i);
LinearLayout.LayoutParams childLp = (LinearLayout.LayoutParams)childTemp.getLayoutParams();
//layout()方法会确切的限制View的显示大小,真正显示到屏幕上的矩形区域,是由layout的四个参数所决定的。
childTemp.layout(avaliableLeft + childLp.leftMargin,
avaliableTop + childLp.topMargin,
childTemp.getMeasuredWidth() + avaliableLeft + childLp.rightMargin,
childTemp.getMeasuredHeight() + avaliableTop + childLp.bottomMargin);
avaliableTop = avaliableTop + childTemp.getMeasuredHeight() + childLp.topMargin + childLp.bottomMargin;
}
} /**
* 水平布局
* */
private void layoutHorizontal(int l, int t, int r, int b) { }
}
然后再贴上一个使用它的代码:
public class MainActivity extends Activity { @SuppressLint("ServiceCast") @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LinearLayout root = (LinearLayout)LayoutInflater.from(this).inflate(R.layout.activity_main, null);
setContentView(root); KLinearLayout myLinearLayout = new KLinearLayout(this);
myLinearLayout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
myLinearLayout.setPadding(10, 20, 30, 40);
myLinearLayout.setOritentation(KLinearLayout.ORITENTATION_VERTICAL); root.addView(myLinearLayout); TextView tv3 = new TextView(this);
tv3.setText("abcd哈哈你好");
tv3.setTextSize(50);
LayoutParams tv3lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
tv3lp.setMargins(10, 10, 10, 10);
tv3.setLayoutParams(tv3lp); myLinearLayout.addView(tv3); TextView tv1 = new TextView(this);
tv1.setText("adbcdsaf");
tv1.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); myLinearLayout.addView(tv1); TextView tv2 = new TextView(this);
tv2.setText("abcd哈哈你好");
tv2.setTextSize(100);
tv2.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); myLinearLayout.addView(tv2); TextView tv4 = new TextView(this);
tv4.setText("大号大号大号大号");
tv4.setTextSize(100);
tv4.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); myLinearLayout.addView(tv4);
}
}
最后,效果如图所示:
以上。
Android UI测量、布局、绘制过程探究的更多相关文章
- [译]Android view 测量布局和绘制的流程
原文链接 创造优秀的用户体验是我们开发者的主要目标之一.为此, 我们首先要了解系统是如何工作的, 这样我们才可以更好的与系统配合, 从它的优点中获益, 规避它的缺陷. 之前关于Android渲染过程的 ...
- Android中View的绘制过程 onMeasure方法简述 附有自定义View例子
Android中View的绘制过程 onMeasure方法简述 附有自定义View例子 Android中View的绘制过程 当Activity获得焦点时,它将被要求绘制自己的布局,Android fr ...
- 【转】Android中View的绘制过程 onMeasure方法简述 附有自定义View例子
Android中View的绘制过程 当Activity获得焦点时,它将被要求绘制自己的布局,Android framework将会处理绘制过程,Activity只需提供它的布局的根节点. 绘制过程从布 ...
- android 中view的绘制过程
view的绘制过程中分别会执行:onMeasure(会多次)计算view的大小,OnLayout(),确定控件的大小和位置 onDraw()绘制view 当Activity获得焦点时,它将被要求绘制自 ...
- Android UI 绘制过程浅析(五)自定义View
前言 这已经是Android UI 绘制过程浅析系列文章的第五篇了,不出意外的话也是最后一篇.再次声明一下,这一系列文章,是我在拜读了csdn大牛郭霖的博客文章<带你一步步深入了解View> ...
- Android UI 绘制过程浅析(一)LayoutInflater简介
前言 这篇blog是我在阅读过csdn大牛郭霖的<带你一步步深入了解View>一系列文章后,亲身实践并做出的小结.作为有志向的前端开发工程师,怎么可以不搞懂View绘制的基本原理——简直就 ...
- Android UI 绘制过程浅析(三)layout过程
前言 上一篇blog中,了解到measure过程对View进行了测量,得到measuredWidth/measuredHeight.对于ViewGroup,则计算出全部children的宽高进行求和. ...
- Android UI 绘制过程浅析(二)onMeasure过程
前言 View的绘制过程分为 measure.layout.draw三个步骤,接下来对这三个步骤逐一进行研究. measure方法的签名 public final void measure(int w ...
- Android view的测量及绘制
讲真,自我感觉,我的水平真的是渣的一匹,好多东西都只停留在知道和会用的阶段,也想去研究原理和底层的实现,可是一看到代码就懵逼了,然后就看不下去了, 说自己不着急都是骗人的,我自己都不信,前两天买了本& ...
随机推荐
- P4438 [HNOI/AHOI2018]道路
辣稽题目 毁我青春 耗我钱财. 设\(f[x][i][j]\)为从1号点走到x点经过i条公路j条铁路,子树的最小代价. \(f[leaf][i][j]=(A+i)(B+j)C\) \(f[x][i][ ...
- C#,清晨随手写
关于昨晚“猜拳”的博客 大家一定要记得,C#的书写规范是很严格的 很严格很严格很严格 简单的说 下面这样就没办法取值 但是这样就可以取值 插眼,开撸
- bintray 在android3.2上传遇到的问题
1.报错信息如下: Gradle DSL method not found: 'google()'Possible causes: The project 'JustTest' may be usin ...
- charles工具教程
本文的内容主要包括: Charles 的简介 如何安装 Charles 将 Charles 设置成系统代理 Charles 主界面介绍 过滤网络请求 截取 iPhone 上的网络封包 截取 Https ...
- php从入门到放弃系列-04.php页面间值传递和保持
php从入门到放弃系列-04.php页面间值传递和保持 一.目录结构 二.两次页面间传递值 在两次页面之间传递少量数据,可以使用get提交,也可以使用post提交,二者的区别恕不赘述. 1.get提交 ...
- 别再犯低级错误,带你了解更新缓存的四种Desigh Pattern
在我们使用分布式缓存Redis或者Memcached编写更新缓存数据代码时,我们总是会犯一个逻辑错误.先删除缓存,然后再更新数据库,而后续的操作会把数据再装载的缓存中.试想,两个并发操作,一个是更新操 ...
- Linux加密到K8S中
文件名字 test.conf 加密: base64 --wrap=0 aaa.conf 把得到的密钥填入配置文件当中即可
- RabbitMQ理论部分
概念 queue 队列 exchange 交换机 bind 绑定 channel 通道 一个发送消息流程包含上述四个概念.消息经过channel传递给exc ...
- Refs 和 DOM
在常规的 React 数据流中,props 是父组件与子组件交互的唯一方式.要修改子元素,你需要用新的 props 去重新渲染子元素.然而,在少数情况下,你需要在常规数据流外强制修改子元素.被修改的子 ...
- Java中&、|、&&、||详解
1.Java中&叫做按位与,&&叫做短路与,它们的区别是: & 既是位运算符又是逻辑运算符,&的两侧可以是int,也可以是boolean表达式,当&两侧 ...