首页 > 代码库 > Android UI测量、布局、绘制过程探究
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步骤,大家体会一下。只实现了垂直布局的部分。
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_parent107 mHeight = Math.min(heightSize, mHeight);108 break;109 case MeasureSpec.EXACTLY: //match_parent110 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 4 @SuppressLint("ServiceCast") @Override 5 protected void onCreate(Bundle savedInstanceState) { 6 super.onCreate(savedInstanceState); 7 LinearLayout root = (LinearLayout)LayoutInflater.from(this).inflate(R.layout.activity_main, null); 8 setContentView(root); 9 10 KLinearLayout myLinearLayout = new KLinearLayout(this);11 myLinearLayout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));12 myLinearLayout.setPadding(10, 20, 30, 40);13 myLinearLayout.setOritentation(KLinearLayout.ORITENTATION_VERTICAL);14 15 root.addView(myLinearLayout);16 17 TextView tv3 = new TextView(this);18 tv3.setText("abcd哈哈你好");19 tv3.setTextSize(50);20 LayoutParams tv3lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);21 tv3lp.setMargins(10, 10, 10, 10);22 tv3.setLayoutParams(tv3lp);23 24 myLinearLayout.addView(tv3);25 26 TextView tv1 = new TextView(this);27 tv1.setText("adbcdsaf");28 tv1.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));29 30 myLinearLayout.addView(tv1);31 32 33 TextView tv2 = new TextView(this);34 tv2.setText("abcd哈哈你好");35 tv2.setTextSize(100);36 tv2.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));37 38 myLinearLayout.addView(tv2);39 40 TextView tv4 = new TextView(this);41 tv4.setText("大号大号大号大号");42 tv4.setTextSize(100);43 tv4.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));44 45 myLinearLayout.addView(tv4); 46 }47 }
最后,效果如图所示:
以上。
Android UI测量、布局、绘制过程探究