Android应用程序窗口Activity的测量Measure布局Layout和绘制Draw过程分析.docx
《Android应用程序窗口Activity的测量Measure布局Layout和绘制Draw过程分析.docx》由会员分享,可在线阅读,更多相关《Android应用程序窗口Activity的测量Measure布局Layout和绘制Draw过程分析.docx(87页珍藏版)》请在冰豆网上搜索。
Android应用程序窗口Activity的测量Measure布局Layout和绘制Draw过程分析
Android应用程序窗口(Activity)的测量(Measure)、布局(Layout)和绘制(Draw)过程分析
在前面一篇文章中,我们分析了Android应用程序窗口的绘图表面的创建过程。
Android应用程序窗口的绘图表面在创建完成之后,我们就可以从上到下地绘制它里面的各个视图了,即各个UI元素了。
不过在绘制这些UI元素之前,我们还需要从上到下地测量它们实际所需要的大小,以及对它们的位置进行合适的安排,即对它们进行合适的布局。
在本文中,我们就将详细地分析Android应用程序窗口的测量、布局以及绘制过程。
从前面这一系列的文章可以知道,Android应用程序窗口请求SurfaceFlinger服务创建了一个绘图表面之后,就可以接着请求为该绘图表面创建图形缓冲区,而当Android应用程序窗口往这些图形缓冲区填充好UI数据之后,就可以请求SurfaceFlinger服务将它们渲染到硬件帧缓冲区中去,这样我们就可以看到应用程序窗口的UI了。
Android应用程序窗口一般不会直接去操作分配给它的图形缓冲区,而是通过一些图形库API来操作。
例如,在前面一文中,使用C++来开发的开机动画应用程序bootanimation,它是通过OpenGL提供的API来绘制UI的。
对于使用Java来开发的Android应用程序来说,它们一般是使用Skia图形库提供的API来绘制UI的。
在Skia图库中,所有的UI都是绘制在画布(Canvas)上的,因此,Android应用程序窗口需要将它的图形缓冲区封装在一块画布里面,然后才可以使用Skia库提供的API来绘制UI。
我们知道,一个Android应用程序窗口里面包含了很多UI元素,这些UI元素是以树形结构来组织的,即它们存在着父子关系,其中,子UI元素位于父UI元素里面,因此,在绘制一个Android应用程序窗口的UI之前,我们首先要确定它里面的各个子UI元素在父UI元素里面的大小以及位置。
确定各个子UI元素在父UI元素里面的大小以及位置的过程又称为测量过程和布局过程。
因此,Android应用程序窗口的UI渲染过程可以分为测量、布局和绘制三个阶段,如图1所示:
从前面一文可以知道,Android应用程序窗口的顶层视图是一个类型为DecorView的UI元素,而从前面一文的Step3又可以知道,这个顶层视图最终是由ViewRoot类的成员函数performTraversals来启动测量、布局和绘制操作的,这三个操作分别由DecorView类的成员函数measure和layout以及ViewRoot类的成员函数draw来实现的。
接下来,我们就分别从DecorView类的成员函数measure和layout以及ViewRoot类的成员函数draw开始,分析Android应用程序窗口的测量、布局和绘制过程。
1.Android应用程序窗口的测量过程
DecorView类的成员函数measure是从父类View继承下来的,因此,我们就从View类的成员函数measure开始分析应用程序窗口的测量过程,如图2所示:
这个过程可以分为3个步骤,接下来我们就详细分析每一个步骤。
Step1.View.measure
[java]viewplaincopy在CODE上查看代码片派生到我的代码片
publicclassViewimplementsDrawable.Callback,KeyEvent.Callback,AccessibilityEventSource{
......
intmPrivateFlags;
......
intmOldWidthMeasureSpec=Integer.MIN_VALUE;
......
intmOldHeightMeasureSpec=Integer.MIN_VALUE;
......
publicfinalvoidmeasure(intwidthMeasureSpec,intheightMeasureSpec){
if((mPrivateFlags&FORCE_LAYOUT)==FORCE_LAYOUT||
widthMeasureSpec!
=mOldWidthMeasureSpec||
heightMeasureSpec!
=mOldHeightMeasureSpec){
//firstclearsthemeasureddimensionflag
mPrivateFlags&=~MEASURED_DIMENSION_SET;
......
//measureourselves,thisshouldsetthemeasureddimensionflagback
onMeasure(widthMeasureSpec,heightMeasureSpec);
//flagnotset,setMeasuredDimension()wasnotinvoked,weraise
//anexceptiontowarnthedeveloper
if((mPrivateFlags&MEASURED_DIMENSION_SET)!
=MEASURED_DIMENSION_SET){
thrownewIllegalStateException("onMeasure()didnotsetthe"
+"measureddimensionbycalling"
+"setMeasuredDimension()");
}
mPrivateFlags|=LAYOUT_REQUIRED;
}
mOldWidthMeasureSpec=widthMeasureSpec;
mOldHeightMeasureSpec=heightMeasureSpec;
}
......
}
这个函数定义在文件frameworks/base/core/java/android/view/View.java中。
参数widthMeasureSpec和heightMeasureSpec用来描述当前正在处理的视图可以获得的最大宽度和高度。
对于应用程序窗口的顶层视图来说,我们也可以认为这两个参数是用来描述应用程序窗口的宽度和高度。
ViewRoot类的成员变量mPrivateFlags的类型为int,如果它的某一个位的值不等于0,那么就隐含着当前视图有一个相应的操作在等待执行中。
ViewRoot类的另外两个成员变量mOldWidthMeasureSpec和mOldHeightMeasureSpec用来保存当前视图上一次可以获得的最大宽度和高度。
当ViewRoot类的成员变量mPrivateFlags的FORCE_LAYOUT位不等于0时,就表示当前视图正在请求执行一次布局操作,这时候函数就需要重新测量当前视图的宽度和高度。
此外,当参数widthMeasureSpec和heightMeasureSpec的值不等于ViewRoot类的成员变量mldWidthMeasureSpec和mOldHeightMeasureSpec的值时,就表示当前视图上一次可以获得的最大宽度和高度已经失效了,这时候函数也需要重新测量当前视图的宽度和高度。
当View类的成员函数measure决定要重新测量当前视图的宽度和高度之后,它就会首先将成员变量mPrivateFlags的MEASURED_DIMENSION_SET位设置为0,接着再调用另外一个成员函数onMeasure来真正执行测量宽度和高度的操作。
View类的成员函数onMeasure执行完成之后,需要再调用另外一个成员函数setMeasuredDimension来将测量好的宽度和高度设置到View类的成员变量mMeasuredWidth和mMeasuredHeight中,并且将成员变量mPrivateFlags的EASURED_DIMENSION_SET位设置为1。
这个操作是强制的,因为当前视图最终就是通过View类的成员变量mMeasuredWidth和mMeasuredHeight来获得它的宽度和高度的。
为了保证这个操作是强制的,View类的成员函数measure再接下来就会检查成员变量mPrivateFlags的EASURED_DIMENSION_SET位是否被设置为1了。
如果不是的话,那么就会抛出一个类型为IllegalStateException的异常来。
View类的成员函数measure最后就会把参数widthMeasureSpec和heightMeasureSpec的值保存在成员变量mldWidthMeasureSpec和mOldHeightMeasureSpec中,以便可以记录当前视图上一次可以获得的最大宽度和高度。
View类的成员函数onMeasure一般是由其子类来重写的。
例如,对于用来应用程序窗口的顶层视图的DecorView类来说,它是通过父类FrameLayout来重写祖父类View的成员函数onMeasure的。
因此,接下来我们就分析FrameLayout类的成员函数onMeasure的实现。
Step2.rameLayout.onMeasure
[java]viewplaincopy在CODE上查看代码片派生到我的代码片
publicclassFrameLayoutextendsViewGroup{
......
@Override
protectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec){
finalintcount=getChildCount();
intmaxHeight=0;
intmaxWidth=0;
//Findrightmostandbottommostchild
for(inti=0;ifinalViewchild=getChildAt(i);
if(mMeasureAllChildren||child.getVisibility()!
=GONE){
measureChildWithMargins(child,widthMeasureSpec,0,heightMeasureSpec,0);
maxWidth=Math.max(maxWidth,child.getMeasuredWidth());
maxHeight=Math.max(maxHeight,child.getMeasuredHeight());
}
}
//Accountforpaddingtoo
maxWidth+=mPaddingLeft+mPaddingRight+mForegroundPaddingLeft+mForegroundPaddingRight;
maxHeight+=mPaddingTop+mPaddingBottom+mForegroundPaddingTop+mForegroundPaddingBottom;
//Checkagainstourminimumheightandwidth
maxHeight=Math.max(maxHeight,getSuggestedMinimumHeight());
maxWidth=Math.max(maxWidth,getSuggestedMinimumWidth());
//Checkagainstourforeground'sminimumheightandwidth
finalDrawabledrawable=getForeground();
if(drawable!
=null){
maxHeight=Math.max(maxHeight,drawable.getMinimumHeight());
maxWidth=Math.max(maxWidth,drawable.getMinimumWidth());
}
setMeasuredDimension(resolveSize(maxWidth,widthMeasureSpec),
resolveSize(maxHeight,heightMeasureSpec));
}
......
}
这个函数定义在文件frameworks/base/core/java/android/widget/FrameLayout.java中。
FrameLayout类是从ViewGroup类继承下来的,后者用来描述一个视图容器,它有一个类型为View的数组mChildren,里面保存的就是它的各个子视图。
ViewGroup类所供了两个成员函数getChildCount和getChildAt,它们分别用来获得一个视图容器所包含的子视图的个数,以及获得每一个子视图。
FrameLayout类的成员函数onMeasure首先是调用另一个成员函数measureChildWithMargins来测量每一个子视图的宽度和高度,并且找到这些子视图的最大宽度和高度值,保存在变量maxWidth和maxHeight中。
FrameLayout类的成员函数onMeasure接着再将前面得到的宽度maxWidth和高度maxHeight分别加上当前视图所设置的Padding值,其中,(mPaddingLeft,mPaddingRight,mPaddingTop,mPaddingBottom)表示当前视图的内容区域的左右上下四条边分别到当前视图的左右上下四条边的距离,它们是父类View的四个成员变量,(mForegroundPaddingLeft,mForegroundPaddingRight,mForegroundPaddingTop,mForegroundPaddingBottom)表示当前视图的各个子视图所围成的区域的左右上下四条边到当前视视的前景区域的左右上下四条边的距离。
从这里就可以看出,当前视图的内容区域的大小就等于前景区域的大小,而前景区域的大小大于等于各个子视图的所围成的区域,这是因为前景区域本来就是用来覆盖各个子视图所围成的区域的。
加上各个Padding值之后,得到的宽度maxWidth和高度maxHeight还不是最终的宽度和高度,还需要考虑以下两个因素:
1.当前视图是否设置有最小宽度和高度。
如果设置有的话,并且它们比前面计算得到的宽度maxWidth和高度maxHeight还要大,那么就将它们作为当前视图的宽度和高度值。
2.当前视图是否设置有前景图。
如果设置有的话,并且它们比前面计算得到的宽度maxWidth和高度maxHeight还要大,那么就将它们作为当前视图的宽度和高度值。
经过上述两步检查之后,FrameLayout类的成员函数onMeasure就得到了当前视图的宽度maxWidth和高度maxHeight。
由于得到的宽度和高度又必须要限制在参数widthMeasureSpec和heightMeasureSpec所描述的宽度和高度规范之内,因此,FrameLayout类的成员函数onMeasure就会调用从View类继承下来的成员函数resolveSize来获得正确的大小。
得到了当前视图的正确大小之后,FrameLayout类的成员函数onMeasure就可以调用从父类View继承下来的成员函数setMeasuredDimension来将它们为当前视图的大小了。
为了理解参数widthMeasureSpec和heightMeasureSpec的含义,我们继续分析View类的成员函数resolveSize的实现,如下所示:
[java]viewplaincopy在CODE上查看代码片派生到我的代码片
publicclassViewimplementsDrawable.Callback,KeyEvent.Callback,AccessibilityEventSource{
......
publicstaticintresolveSize(intsize,intmeasureSpec){
intresult=size;
intspecMode=MeasureSpec.getMode(measureSpec);
intspecSize=MeasureSpec.getSize(measureSpec);
switch(specMode){
caseMeasureSpec.UNSPECIFIED:
result=size;
break;
caseMeasureSpec.AT_MOST:
result=Math.min(size,specSize);
break;
caseMeasureSpec.EXACTLY:
result=specSize;
break;
}
returnresult;
}
......
}
这个函数定义在文件rameworks/base/core/java/android/view/View.java中。
参数measureSpec的值其实是由两部分内容来组成的,最高2位表示一个测量规范,而低30位表示一个宽度值或者高度值。
测量规范有三种,分别是0、1和2,使用常量MeasureSpec.UNSPECIFIED、MeasureSpec.EXACTLY和MeasureSpec.AT_MOST来表示。
当参数measureSpec描述的规范是MeasureSpec.UNSPECIFIED时,就表示当前视图没有指定它的大小测量模式,这时候就使用参数size的值;当参数measureSpec描述的规范是MeasureSpec.AT_MOST时,就表示当前视图的大小等于参数size和参数measureSpec所指定的值中的较小值;当参数measureSpec描述的规范是MeasureSpec.EXACTLY时,就表示当前视图的大小等于参数measureSpec中所指定的值。
回到FrameLayout类的成员函数onMeasure中,我们再来看一下View类的成员函数setMeasuredDimension是如何设置当前视图的大小的,如下所示:
[java]viewplaincopy在CODE上查看代码片派生到我的代码片
publicclassViewimplementsDrawable.Callback,KeyEvent.Callback,AccessibilityEventSource{
......
protectedfinalvoidsetMeasuredDimension(intmeasuredWidth,intmeasuredHeight){
mMeasuredWidth=measuredWidth;
mMeasuredHeight=measuredHeight;
mPrivateFlags|=MEASURED_DIMENSION_SET;
}
......
}
这个函数定义在文件rameworks/base/core/java/android/view/View.java中。
View类的成员函数setMeasuredDimension首先将参数measuredWidth和measuredHeight的值保存在成员变量mMeasuredWidth和mMeasuredHeight中,用来作为当前视图的宽度和高度,并且将成员变量mPrivateFlags的位MEASURED_DIMENSION_SET设置为1,这样返回到前面的Step1时,就不会抛出一个类型为IllegalStateException的异常了。
FrameLayout类的另一个成员函数measureChildWithMargins是从父类ViewGroup继承下来的,接下来我们就继续分析它的实现,以便可以了解一个视图容器的各个子视图的大小的测量过程。
Step3.ViewGroup.measureChildWithMargins
[java]viewplaincopy在CODE上查看代码片派生到我的代码片
publicabstractclassViewGroupextendsViewimplementsViewParent,ViewManager{
......
protectedvoidmeasureChildWithMargins(Viewchild,
intparentWidthMeasureSpec,intwidthUsed,
intparentHeightMeasureSpec,intheightUsed){
finalMarginLayoutParamslp=(MarginLayoutParams)child.getLayoutParams();
finalintchildWidthMeasureSpec=getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft+mPaddingRight+lp.leftMargin+lp.rightMargin
+widthUsed,lp.width);
finalintchildHeightMeasureSpec=getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop+mPaddingBottom+lp.topMargin+lp.bottomMargin
+heightUsed,lp.height);
child.measure(childWidthMeasureSpec,childHeightMeasureSpec);
}
......
}
这个函数定义在文件rameworks/base/core/java/android/view/ViewGroup.java中。
参数child用来描述当前要测量大小的子视图,参数parentWidthMeasureSpec和parentHeightMeasureSpec用来描述当前子视图可以获得的最大宽度和高度,参数widthUsed和heightUsed用来描述父窗口已经使用了的宽度和高度。
ViewGroup类的成员函数measureChildWithMargi