在上一篇博客《Android中Activity启动过程探究》中,已经从ActivityThread.main()开始,一路摸索到ViewRootImpl.performTraversals()了。本篇就来探究UI的绘制过程。

performTraversals()方法非常长,其中关键性的三个步骤是依次调用了performMeasure(), performLayout(), performDraw()。分别来看这三个步骤吧!

Measure过程(测量过程)

直接来看performMeasure()方法。

该方法非常直接了当的调用了mViewmeasure()方法。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设置好它所需要的宽度和高度,在程序代码中的体现,实际上就是mMeasuredWidthmMeasuredHeight两个成员变量,调用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步骤,大家体会一下。只实现了垂直布局的部分。

  1. /**
  2. * 自己实现的LinearLayout
  3. * @author kross(krossford@foxmail.com)
  4. * @update 2014-10-16 19:42:47 第一次编写,实现垂直布局
  5. * */
  6. public class KLinearLayout extends ViewGroup {
  7.  
  8. private static final String TAG = "KLinearLayout";
  9.  
  10. /** 垂直布局 */
  11. public static final byte ORITENTATION_VERTICAL = 0x1;
  12. /** 水平布局 */
  13. public static final byte ORITENTATION_HORIZONTAL = 0x0;
  14.  
  15. /** 线性布局的方向,默认值为水平
  16. * @see #ORITENTATION_HORIZONTAL
  17. * @see #ORITENTATION_VERTICAL */
  18. private int mOritentation = ORITENTATION_HORIZONTAL;
  19.  
  20. /** 最终的宽度 */
  21. private int mWidth;
  22. /** 最终的高度 */
  23. private int mHeight;
  24.  
  25. public KLinearLayout(Context context) {
  26. super(context);
  27. mOritentation = ORITENTATION_HORIZONTAL;
  28. }
  29.  
  30. /**
  31. * 设置线性布局的方向:垂直或水平
  32. * @param oritentation
  33. * @see #ORITENTATION_HORIZONTAL
  34. * @see #ORITENTATION_VERTICAL
  35. * */
  36. public void setOritentation(byte oritentation) {
  37. mOritentation = oritentation;
  38. }
  39.  
  40. @Override
  41. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  42. Log.i(TAG, "onMeasure");
  43. if (mOritentation == ORITENTATION_HORIZONTAL) {
  44. measureHorizontal(widthMeasureSpec, heightMeasureSpec);
  45. } else {
  46. measureVertical(widthMeasureSpec, heightMeasureSpec);
  47. }
  48. }
  49.  
  50. @Override
  51. protected void onLayout(boolean changed, int l, int t, int r, int b) {
  52. Log.i(TAG, "onLayout l:" + l + " t:" + t + " r:" + r + " b:" + b);
  53.  
  54. if (mOritentation == ORITENTATION_HORIZONTAL) {
  55. layoutHorizontal(l, t, r, b);
  56. } else {
  57. layoutVertical(l, t, r, b);
  58. }
  59. }
  60.  
  61. /**
  62. * 垂直测量
  63. * */
  64. private void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
  65. Log.i(TAG, "measureVertical");
  66.  
  67. int widthMode = MeasureSpec.getMode(widthMeasureSpec);
  68. int widthSize = MeasureSpec.getSize(widthMeasureSpec);
  69.  
  70. int heightMode = MeasureSpec.getMode(heightMeasureSpec);
  71. int heightSize = MeasureSpec.getSize(heightMeasureSpec);
  72.  
  73. /**
  74. * 已经使用了的高度,容器是空的,已经使用的高度为0,如果已经存在一个高度为x的子控件,这个值为x。
  75. * 这个值也表示,所有的子控件所需要的高度总值。
  76. */
  77. int heightUsed = 0;
  78. View childTemp = null;
  79. for (int index = 0; index < getChildCount(); index++) { //遍历子控件
  80. childTemp = getChildAt(index);
  81. measureChildWithMargins(childTemp, widthMeasureSpec, 0, heightMeasureSpec, heightUsed); //获取子控件并测量它的大小
  82. LinearLayout.LayoutParams childLp = (LinearLayout.LayoutParams)childTemp.getLayoutParams();
  83.  
  84. //子控件的高度,包括子控件的上下外边距一起累加到heightUsed值中
  85. heightUsed = heightUsed + childTemp.getMeasuredHeight() + childLp.topMargin + childLp.bottomMargin;
  86. //因为是垂直布局,所以宽度直选最大的一个
  87. mWidth = Math.max(mWidth, childTemp.getMeasuredWidth() + childLp.leftMargin + childLp.rightMargin);
  88. }
  89.  
  90. mWidth = mWidth + getPaddingLeft() + getPaddingRight(); //加上左右内边距
  91.  
  92. switch (widthMode) {
  93. case MeasureSpec.AT_MOST: //wrap_parent
  94. mWidth = Math.min(widthSize, mWidth); //因为是包裹内容,所以宽度应该是尽可能的小
  95. break;
  96. case MeasureSpec.EXACTLY: //match_parent
  97. mWidth = widthSize; //与父控件一样大,那么宽度应该是父控件给的,也就是参数所给的
  98. break;
  99. case MeasureSpec.UNSPECIFIED:
  100. break;
  101. }
  102.  
  103. mHeight = heightUsed + getPaddingTop() + getPaddingBottom(); //所有子控件的高度和 + 上下内边距
  104.  
  105. switch (heightMode) {
  106. case MeasureSpec.AT_MOST: //wrap_parent
  107. mHeight = Math.min(heightSize, mHeight);
  108. break;
  109. case MeasureSpec.EXACTLY: //match_parent
  110. mHeight = heightSize;
  111. break;
  112. case MeasureSpec.UNSPECIFIED:
  113. break;
  114. }
  115.  
  116. setMeasuredDimension(mWidth, mHeight);
  117. }
  118.  
  119. /**
  120. * 水平测量
  121. * */
  122. private void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) {
  123. Log.i(TAG, "measureHorizontal");
  124. setMeasuredDimension(100, 100);
  125. }
  126.  
  127. /**
  128. * 垂直布局
  129. * */
  130. private void layoutVertical(int l, int t, int r, int b) {
  131. int avaliableLeft = getPaddingLeft();
  132. int avaliableTop = getPaddingTop();
  133.  
  134. View childTemp = null;
  135. for (int i = 0; i < getChildCount(); i++) {
  136. childTemp = getChildAt(i);
  137. LinearLayout.LayoutParams childLp = (LinearLayout.LayoutParams)childTemp.getLayoutParams();
  138. //layout()方法会确切的限制View的显示大小,真正显示到屏幕上的矩形区域,是由layout的四个参数所决定的。
  139. childTemp.layout(avaliableLeft + childLp.leftMargin,
  140. avaliableTop + childLp.topMargin,
  141. childTemp.getMeasuredWidth() + avaliableLeft + childLp.rightMargin,
  142. childTemp.getMeasuredHeight() + avaliableTop + childLp.bottomMargin);
  143. avaliableTop = avaliableTop + childTemp.getMeasuredHeight() + childLp.topMargin + childLp.bottomMargin;
  144. }
  145. }
  146.  
  147. /**
  148. * 水平布局
  149. * */
  150. private void layoutHorizontal(int l, int t, int r, int b) {
  151.  
  152. }
  153. }

然后再贴上一个使用它的代码:

  1. public class MainActivity extends Activity {
  2.  
  3. @SuppressLint("ServiceCast") @Override
  4. protected void onCreate(Bundle savedInstanceState) {
  5. super.onCreate(savedInstanceState);
  6. LinearLayout root = (LinearLayout)LayoutInflater.from(this).inflate(R.layout.activity_main, null);
  7. setContentView(root);
  8.  
  9. KLinearLayout myLinearLayout = new KLinearLayout(this);
  10. myLinearLayout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
  11. myLinearLayout.setPadding(10, 20, 30, 40);
  12. myLinearLayout.setOritentation(KLinearLayout.ORITENTATION_VERTICAL);
  13.  
  14. root.addView(myLinearLayout);
  15.  
  16. TextView tv3 = new TextView(this);
  17. tv3.setText("abcd哈哈你好");
  18. tv3.setTextSize(50);
  19. LayoutParams tv3lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
  20. tv3lp.setMargins(10, 10, 10, 10);
  21. tv3.setLayoutParams(tv3lp);
  22.  
  23. myLinearLayout.addView(tv3);
  24.  
  25. TextView tv1 = new TextView(this);
  26. tv1.setText("adbcdsaf");
  27. tv1.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
  28.  
  29. myLinearLayout.addView(tv1);
  30.  
  31. TextView tv2 = new TextView(this);
  32. tv2.setText("abcd哈哈你好");
  33. tv2.setTextSize(100);
  34. tv2.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
  35.  
  36. myLinearLayout.addView(tv2);
  37.  
  38. TextView tv4 = new TextView(this);
  39. tv4.setText("大号大号大号大号");
  40. tv4.setTextSize(100);
  41. tv4.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
  42.  
  43. myLinearLayout.addView(tv4);
  44. }
  45. }

最后,效果如图所示:

以上。

Android UI测量、布局、绘制过程探究的更多相关文章

  1. [译]Android view 测量布局和绘制的流程

    原文链接 创造优秀的用户体验是我们开发者的主要目标之一.为此, 我们首先要了解系统是如何工作的, 这样我们才可以更好的与系统配合, 从它的优点中获益, 规避它的缺陷. 之前关于Android渲染过程的 ...

  2. Android中View的绘制过程 onMeasure方法简述 附有自定义View例子

    Android中View的绘制过程 onMeasure方法简述 附有自定义View例子 Android中View的绘制过程 当Activity获得焦点时,它将被要求绘制自己的布局,Android fr ...

  3. 【转】Android中View的绘制过程 onMeasure方法简述 附有自定义View例子

    Android中View的绘制过程 当Activity获得焦点时,它将被要求绘制自己的布局,Android framework将会处理绘制过程,Activity只需提供它的布局的根节点. 绘制过程从布 ...

  4. android 中view的绘制过程

    view的绘制过程中分别会执行:onMeasure(会多次)计算view的大小,OnLayout(),确定控件的大小和位置 onDraw()绘制view 当Activity获得焦点时,它将被要求绘制自 ...

  5. Android UI 绘制过程浅析(五)自定义View

    前言 这已经是Android UI 绘制过程浅析系列文章的第五篇了,不出意外的话也是最后一篇.再次声明一下,这一系列文章,是我在拜读了csdn大牛郭霖的博客文章<带你一步步深入了解View> ...

  6. Android UI 绘制过程浅析(一)LayoutInflater简介

    前言 这篇blog是我在阅读过csdn大牛郭霖的<带你一步步深入了解View>一系列文章后,亲身实践并做出的小结.作为有志向的前端开发工程师,怎么可以不搞懂View绘制的基本原理——简直就 ...

  7. Android UI 绘制过程浅析(三)layout过程

    前言 上一篇blog中,了解到measure过程对View进行了测量,得到measuredWidth/measuredHeight.对于ViewGroup,则计算出全部children的宽高进行求和. ...

  8. Android UI 绘制过程浅析(二)onMeasure过程

    前言 View的绘制过程分为 measure.layout.draw三个步骤,接下来对这三个步骤逐一进行研究. measure方法的签名 public final void measure(int w ...

  9. Android view的测量及绘制

    讲真,自我感觉,我的水平真的是渣的一匹,好多东西都只停留在知道和会用的阶段,也想去研究原理和底层的实现,可是一看到代码就懵逼了,然后就看不下去了, 说自己不着急都是骗人的,我自己都不信,前两天买了本& ...

随机推荐

  1. python安装mysql

    一.MySQL是一种关系数据库管理系统,关系数据库将数据保存在不同的表中,而不是将所有数据放在一个大仓库内,这样就增加了速度并提高了灵活性. 二.最近在学习python语言,总体上面来说还是接触的挺快 ...

  2. idea中 和outline相似的功能

    ctrl+ F12 alt+7 ctrl+h 这个用来看继承关系很好

  3. linux 问题收集

    1,错误信息:bunzip2: command not found 解决方法:yum install -y bzip2 2,The X11 forwarding request was rejecte ...

  4. AJAX 过程总结

    AJAX 工作过程:(1) 创建对象(需要处理兼容性问题) 创建XMLHttpRequest对象(创建一个异步调用对象) <!-- ie6以上 --> var xhr = new XMLH ...

  5. matplotlib简单示例

    一.简介 以下引用自百度百科 Matplotlib 是一个 Python 的 2D绘图库,它以各种硬拷贝格式和跨平台的交互式环境生成出版质量级别的图形 . 通过 Matplotlib,开发者可以仅需要 ...

  6. SQL常见面试题

    1.用一条SQL 语句 查询出每门课都大于80 分的学生姓名 name   kecheng   fenshu张三    语文       81张三     数学       75李四     语文   ...

  7. react-native初体验(1) — hello world

    没有简介,直接开始干活吧. 默认阅读本文的你已经安装好 nodejs, windows用户需要升级yarn到最新版本. 并且设置安装源为国内的淘宝源: npm config set registry ...

  8. Unity扩展编辑器二

    Unity支持自行创建窗口,也支持自定义窗口布局,在Project视图中创建一个Editor文件夹,在文件夹中创建一条脚本 自定义窗口需要让脚本继承EditorWindow在设置MenuItem,此时 ...

  9. Datawhale MySQL 训练营 Task6 实战项目

    作业 项目十:行程和用户(难度:困难) Trips 表中存所有出租车的行程信息.每段行程有唯一键 Id,Client_Id 和 Driver_Id 是 Users 表中 Users_Id 的外键.St ...

  10. ansible使用1

    常用软件安装及使用目录   ansible软件2 ### ansible软件部署安装需求#### 01. 需要有epel源 系统yum源(base epel--pip gem) sshpass---e ...