Android应用程序窗口(Activity)的测量(Measure)、布局(Layout)和绘制(Draw)过程分析

在前面一篇文章中,我们分析了Android应用程序窗口的绘图表面的创建过程。Android应用程序窗口的绘图表面在创建完成之后,我们就可以从上到下地绘制它里面的各个视图了,即各个UI元素了。不过在绘制这些UI元素之前,我们还需要从上到下地测量它们实际所需要的大小,以及对它们的位置进行合适的安排,即对它们进行合适的布局。在本文中,我们就将详细地分析Android应用程序窗口的测量、布局以及绘制过程。

从前面Android应用程序与SurfaceFlinger服务的关系概述和学习计划这一系列的文章可以知道,Android应用程序窗口请求SurfaceFlinger服务创建了一个绘图表面之后,就可以接着请求为该绘图表面创建图形缓冲区,而当Android应用程序窗口往这些图形缓冲区填充好UI数据之后,就可以请求SurfaceFlinger服务将它们渲染到硬件帧缓冲区中去,这样我们就可以看到应用程序窗口的UI了。

Android应用程序窗口一般不会直接去操作分配给它的图形缓冲区,而是通过一些图形库API来操作。例如,在前面Android系统的开机画面显示过程分析一文中,使用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所示:

图1 Android应用程序窗口渲染三步曲

从前面Android应用程序窗口(Activity)的视图对象(View)的创建过程分析一文可以知道,Android应用程序窗口的顶层视图是一个类型为DecorView的UI元素,而从前面Android应用程序窗口(Activity)的绘图表面(Surface)的创建过程分析一文的Step 3又可以知道,这个顶层视图最终是由ViewRoot类的成员函数performTraversals来启动测量、布局和绘制操作的,这三个操作分别由DecorView类的成员函数measure和layout以及ViewRoot类的成员函数draw来实现的。

接下来,我们就分别从DecorView类的成员函数measure和layout以及ViewRoot类的成员函数draw开始,分析Android应用程序窗口的测量、布局和绘制过程。

1. Android应用程序窗口的测量过程

DecorView类的成员函数measure是从父类View继承下来的,因此,我们就从View类的成员函数measure开始分析应用程序窗口的测量过程,如图2所示:

图2 Android应用程序窗口的测量过程

这个过程可以分为3个步骤,接下来我们就详细分析每一个步骤。

Step 1. View.measure

[java]view plaincopy

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) {

// first clears the measured dimension flag

mPrivateFlags &= ~MEASURED_DIMENSION_SET;

......

// measure ourselves, this should set the measured dimension flag back

onMeasure(widthMeasureSpec, heightMeasureSpec);

// flag not set, setMeasuredDimension() was not invoked, we raise

// an exception to warn the developer

if((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) {

thrownewIllegalStateException("onMeasure() did not set the"

+" measured dimension by calling"

+" 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的实现。

Step 2. rameLayout.onMeasure

[java]view plaincopy

publicclassFrameLayoutextendsViewGroup {

......

@Override

protectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec) {

finalintcount = getChildCount();

intmaxHeight =0;

intmaxWidth =0;

// Find rightmost and bottommost child

for(inti =0; i < count; i++) {

finalView child = 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());

}

}

// Account for padding too

maxWidth += mPaddingLeft + mPaddingRight + mForegroundPaddingLeft + mForegroundPaddingRight;

maxHeight += mPaddingTop + mPaddingBottom + mForegroundPaddingTop + mForegroundPaddingBottom;

// Check against our minimum height and width

maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());

maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

// Check against our foreground's minimum height and width

finalDrawable drawable = 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]view plaincopy

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]view plaincopy

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,这样返回到前面的Step 1时,就不会抛出一个类型为IllegalStateException的异常了。

FrameLayout类的另一个成员函数measureChildWithMargins是从父类ViewGroup继承下来的,接下来我们就继续分析它的实现,以便可以了解一个视图容器的各个子视图的大小的测量过程。

Step 3. ViewGroup.measureChildWithMargins

[java]view plaincopy

publicabstractclassViewGroupextendsViewimplementsViewParent, ViewManager {

......

protectedvoidmeasureChildWithMargins(View child,

intparentWidthMeasureSpec,intwidthUsed,

intparentHeightMeasureSpec,intheightUsed) {

finalMarginLayoutParams lp = (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类的成员函数measureChildWithMargins必须要综合考虑上述参数,以及当前正在测量的子视图child所设置的大小和Margin值,还有当前视图容器所设置的Padding值,来得到当前正在测量的子视图child的正确宽度childWidthMeasureSpec和高度childHeightMeasureSpec,这是通过调用ViewGroup类的另外一个成员函数getChildMeasureSpec来实现的。

得到了当前正在测量的子视图child的正确宽度childWidthMeasureSpec和高度childHeightMeasureSpec之后,就可以调用它的成员函数measure来设置它的大小了,即执行前面Step 1的操作。注意,如果当前正在测量的子视图child描述的也是一个视图容器,那么它又会重复执行Step 2和Step 3的操作,直到它的所有子孙视图的大小都测量完成为止。

至此,我们就分析完成Android应用程序窗口的测量过程了,接下来我们继续分析Android应用程序窗口的布局过程。

2. Android应用程序窗口的布局过程

DecorView类的成员函数layout是从父类View继承下来的,因此,我们就从View类的成员函数layout开始分析应用程序窗口的布局过程,如图3所示:

图3 Android应用程序窗口的布局过程

这个过程可以分为5个步骤,接下来我们就详细地分析每一个步骤。

Step 1. View.layout

[java]view plaincopy

publicclassViewimplementsDrawable.Callback, KeyEvent.Callback, AccessibilityEventSource {

......

intmPrivateFlags;

......

publicfinalvoidlayout(intl,intt,intr,intb) {

booleanchanged = setFrame(l, t, r, b);

if(changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {

......

onLayout(changed, l, t, r, b);

mPrivateFlags &= ~LAYOUT_REQUIRED;

}

mPrivateFlags &= ~FORCE_LAYOUT;

}

......

}

这个函数定义在文件frameworks/base/core/java/android/view/View.java中。

参数l、t、r和b分别用来描述当前视图的左上右下四条边与其父视图的左上右下四条边的距离,这样当前视图通过这四个参数就可以知道它在父视图中的位置以及大小。

View类的成员函数layout首先调用另外一个成员函数setFrame来设置当前视图的位置以及大小。设置完成之后,如果当前视图的大小或者位置与上次相比发生了变化,那么View类的成员函数setFrame的返回值changed就会等于true。在这种情况下, View类的成员函数layout就会继续调用另外一个成员函数onLayout重新布局当前视图的子视图。此外,如果此时View类的成员变量mPrivateFlags的LAYOUT_REQUIRED位不等于0,那么也表示当前视图需要重新布局它的子视图,因此,这时候View类的成员函数layout也会调用另外一个成员函数onLayout。

当前视图的子视图都重新布局完成之后,View类的成员函数layout就可以将成员变量mPrivateFlags的LAYOUT_REQUIRED位设置为0了,因为此时当前视图及其子视图都已经执行了一次布局操作了。

View类的成员函数layout最后还会将成员变量mPrivateFlags的FORCE_LAYOUT位设置为0,也是因为此时当前视图及其子视图的布局已经是最新的了。

接下来,我们就继续分析View类的成员函数setFrame和onLayout的实现,以便可以了解当前视图及其子视图是如何执行布局操作的。

Step 2. View.setFrame

[java]view plaincopy

publicclassViewimplementsDrawable.Callback, KeyEvent.Callback, AccessibilityEventSource {

......

intmPrivateFlags;

......

intmViewFlags;

......

protectedintmLeft;

......

protectedintmRight;

......

protectedintmTop;

......

protectedintmBottom;

......

privatebooleanmBackgroundSizeChanged;

......

protectedbooleansetFrame(intleft,inttop,intright,intbottom) {

booleanchanged =false;

......

if(mLeft != left || mRight != right || mTop != top || mBottom != bottom) {

changed =true;

// Remember our drawn bit

intdrawn = mPrivateFlags & DRAWN;

// Invalidate our old position

invalidate();

intoldWidth = mRight - mLeft;

intoldHeight = mBottom - mTop;

mLeft = left;

mTop = top;

mRight = right;

mBottom = bottom;

mPrivateFlags |= HAS_BOUNDS;

intnewWidth = right - left;

intnewHeight = bottom - top;

if(newWidth != oldWidth || newHeight != oldHeight) {

onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);

}

if((mViewFlags & VISIBILITY_MASK) == VISIBLE) {

// If we are visible, force the DRAWN bit to on so that

// this invalidate will go through (at least to our parent).

// This is because someone may have invalidated this view

// before this call to setFrame came in, therby clearing

// the DRAWN bit.

mPrivateFlags |= DRAWN;

invalidate();

}

// Reset drawn bit to original value (invalidate turns it off)

mPrivateFlags |= drawn;

mBackgroundSizeChanged =true;

}

returnchanged;

}

......

}

这个函数定义在文件frameworks/base/core/java/android/view/View.java中。

View类的成员变量mLeft、mRight、mTop和mBottom分别用来描述当前视图的左右上下四条边与其父视图的左右上下四条边的距离,如果它们的值与参数left、right、top和bottom的值不相等,那么就说明当前视图的大小或者位置发生变化了。这时候View类的成员函数setFrame就需要将参数left、right、top和bottom的值分别记录在成员变量mLeft、mRight、mTop和mBottom中。在记录之前,还会执行两个操作:

1. 将成员变量mPrivateFlags的DRAWN位记录在变量drawn中,并且调用另外一个成员函数invalidate来检查当前视图上次请求的UI绘制操作是否已经执行。如果已经执行了的话,那么就会再请求执行一个UI绘制操作,以便可以在修改当前视图的大小和位置之前,将当前视图在当前位置按照当前大小显示一次。在接下来的Step 3中,我们再详细分析View类的成员函数invalidate的实现。

2. 计算当前视图上一次的宽度oldWidth和oldHeight,以便接下来可以检查当前视图的大小是否发生了变化。

当前视图距离父视图的边距一旦设置好之后,它就是一个具有边界的视图了,因此,View类的成员函数setFrame接着还会将成员变量mPrivateFlags的HAS_BOUNDS设置为1。

View类的成员函数setFrame再接下来又会计算当前视图新的宽度newWidth和高度newHeight,如果它们与上一次的宽度oldWidth和oldHeight的值不相等,那么就说明当前视图的大小发生了变化,这时候就会调用另外一个成员函数onSizeChanged来让子类有机会处理这个变化事件。

View类的成员函数setFrame接下来继续判断当前视图是否是可见的,即成员变量mViewFlags的VISIBILITY_MASK位的值是否等于VISIBLE。如果是可见的话,那么就需要将成员变量mPrivateFlags的DRAWN位设置为1,以便接下来可以调用另外一个成员函数invalidate来成功地执行一次UI绘制操作,目的是为了将当前视图马上显示出来。

View类的成员变量mPrivateFlags的DRAWN位描述的是当前视图上一次请求的UI绘制操作是否已经执行过了。如果它的值等于1,就表示已经执行过了,否则的话,就表示还没在等待执行。前面第一次调用View类的成员函数invalidate来检查当前视图上次请求的UI绘制操作是否已经执行时,如果发现已经执行了,那么就会重新请求执行一次新的UI绘制操作,这时候会导致当前视图的成员变量mPrivateFlags的DRAWN位重置为0。注意,新请求执行的UI绘制只是为了在修改当前视图的大小以及大小之前,先将它在上一次设置的大小以及位置中绘制出来,这样就可以使得当前视图的大小以及位置出现平滑的变换。换句话说,新请求执行的UI绘制只是为了获得一个中间效果,它不应该影响当前视图的绘制状态,即不可以修改当前视图的成员变量mPrivateFlags的DRAWN位。因此,我们就需要在前面第一次调用View类的成员函数invalidate前,先将当前视图的成员变量mPrivateFlags的DRAWN位保存下来,即保存在变量drawn中,然后等到调用之后,再将变量drawn的值恢复到当前视图的成员变量mPrivateFlags的DRAWN位中去。

另一方面,如果当前视图的大小和位置发生了变化,View类的成员函数setFrame还会将成员变量mBackgroundSizeChanged的值设置为true,以便可以表示当前视图的背景大小发生了变化。

最后,View类的成员函数setFrame将变量changed的值返回给调用者,以便调用者可以知道当前视图的大小和位置是否发生了变化。

接下来,我们继续分析View类的成员函数invalidate的实现,以便可以了解当前视图是如何执行一次UI绘制操作的。

Step 3. View.invalidate

[java]view plaincopy

publicclassViewimplementsDrawable.Callback, KeyEvent.Callback, AccessibilityEventSource {

......

protectedViewParent mParent;

......

intmPrivateFlags;

......

publicvoidinvalidate() {

......

if((mPrivateFlags & (DRAWN | HAS_BOUNDS)) == (DRAWN | HAS_BOUNDS)) {

mPrivateFlags &= ~DRAWN & ~DRAWING_CACHE_VALID;

finalViewParent p = mParent;

finalAttachInfo ai = mAttachInfo;

if(p !=null&& ai !=null) {

finalRect r = ai.mTmpInvalRect;

r.set(0,0, mRight - mLeft, mBottom - mTop);

// Don't call invalidate -- we don't want to internally scroll

// our own bounds

p.invalidateChild(this, r);

}

}

}

......

}

这个函数定义在文件frameworks/base/core/java/android/view/View.java中。

View类的成员函数invalidate首先检查成员变量mPrivateFlags的DRAWN位和HAS_BOUNDS位是否都被设置为1。如果是的话,那么就说明当前视图上一次请求执行的UI绘制操作已经执行完成了,这时候View类的成员函数invalidate才可以请求执行新的UI绘制操作。

View类的成员函数invalidate在请求新的UI绘制操作之前,会将成员变量mPrivateFlags的DRAWN位和DRAWING_CACHE_VALID位重置为0,其中,后者表示当前视图正在缓存的一些绘图对象已经失效了,这是因为接下来就要重新开始绘制当前视图的UI了。

请求绘制当前视图的UI是通过调用View类的成员变量mParent所描述的一个ViewParent接口的成员函数invalidateChild来实现的。前面我们假设当前视图是应用程序窗口的顶层视图,即它是一个类型为DecoreView的视图,它的成员变量mParent指向的是与其所关联的一个ViewRoot对象。因此,绘制当前视图的UI的操作实际上是通过调用ViewRoot类的成员函数invalidateChild来实现的。

注意,在调用ViewRoot类的成员函数invalidateChild的成员函数invalidateChild来绘制当前视图的UI之前,会将当前视图即将要绘制的区域记录在View类的成员变量mAttachInfo所描述的一个AttachInfo对象的成员变量mTmpInvalRect中。

接下来,我们就继续分析ViewRoot类的成员函数invalidateChild的实现。

Step 4. ViewRoot.invalidateChild

[java]view plaincopy

publicfinalclassViewRootextendsHandlerimplementsViewParent,

View.AttachInfo.Callbacks {

......

publicvoidinvalidateChild(View child, Rect dirty) {

checkThread();

......

if(mCurScrollY !=0|| mTranslator !=null) {

mTempRect.set(dirty);

dirty = mTempRect;

if(mCurScrollY !=0) {

dirty.offset(0, -mCurScrollY);

}

if(mTranslator !=null) {

mTranslator.translateRectInAppWindowToScreen(dirty);

}

if(mAttachInfo.mScalingRequired) {

dirty.inset(-1, -1);

}

}

mDirty.union(dirty);

if(!mWillDrawSoon) {

scheduleTraversals();

}

}

......

}

这个函数定义在文件frameworks/base/core/java/android/view/ViewRoot.java中。

ViewRoot类的成员函数invalidateChild首先调用另外一个成员函数checkThread来检查当前正在执行的是否是一个UI线程。如果不是的话,ViewRoot类的成员函数checkThread就会抛出一个异常出来。这是因为所有的UI操作都必须要在UI线程中执行。

ViewRoot类的成员函数invalidateChild接下来还会检查当前正在处理的应用程序窗口在Y轴上是否出现有滚动条,即成员变量mCurScrollY的值不等于0, 或者前正在处理的应用程序窗口是否运行在兼容模式之下,即成员变量mTranslator的值不等于null。当一个应用程序窗口运行在兼容模式时,它显示出来的大小和它实际被设置的大小是不一样的,要经过相应的转换处理。对于上述这两种情况,ViewRoot类的成员函数invalidateChild都需要调整参数dirty所描述的一个需要重新绘制的区域的大小和位置。

调整好参数dirty所描述的一个需要重新绘制的区域之后, ViewRoot类的成员函数invalidateChild就将它所描述的一个区域与成员变量mDirty所描述的一区域执行一个合并操作,并且将得到的新区域保存在成员变量mDirty中。从这个操作就可以看出,ViewRoot类的成员变量mDirty描述的就是当前正在处理的应用程序窗口下一次所要重新绘制的总区域。

设置好当前正在处理的应用程序窗口下一次所要重新绘制的总区域之后,ViewRoot类的成员函数invalidateChild最后就检查成员变量mWillDrawSoon的值是否不等于true。如果ViewRoot类的成员mWillDrawSoon的值等于true的话,那么就说明UI线程的消息队列中已经有一个DO_TRAVERSAL消息在等待执行了,这时候就不需要调用ViewRoot类的成员函数scheduleTraversals来向UI线程的消息队列发送一个DO_TRAVERSAL消息了,否则的话,就需要调用ViewRoot类的成员函数scheduleTraversals来向UI线程的消息队列发送一个DO_TRAVERSAL消息。

ViewRoot类的成员函数scheduleTraversals在前面Android应用程序窗口(Activity)的绘图表面(Surface)的创建过程分析一文中已经分析过了,这里不再详述。

这一步执行完成之后,返回到前面的Step 1中,即View类的成员函数layout中,接下来它就会调用另外一个成员函数onLayout来重新布局当前视图的子视图的布局了。View类的成员函数onLayout是由子类来重写的,并且只有当该子类描述的是一个容器视图时,它才会重写父类View的成员函数onLayout。前面我们已经假设当前正在处理的是应用程序窗口的顶层视图,它的类型为DecorView,并且它描述的是一个容器视图,因此,接下来我们就会继续分析DecorView类的成员函数onLayout的实现。

事实上,DecorView类是通过FrameLayout类来间接继承View类的,并且它的成员函数onLayout是从FrameLayout类继承下来的,因此,接下来我们实际上要分析的是FrameLayout类的成员函数onLayout的实现。

Step 5. FrameLayout.onLayout

[java]view plaincopy

publicclassFrameLayoutextendsViewGroup {

......

protectedvoidonLayout(booleanchanged,intleft,inttop,intright,intbottom) {

finalintcount = getChildCount();

finalintparentLeft = mPaddingLeft + mForegroundPaddingLeft;

finalintparentRight = right - left - mPaddingRight - mForegroundPaddingRight;

finalintparentTop = mPaddingTop + mForegroundPaddingTop;

finalintparentBottom = bottom - top - mPaddingBottom - mForegroundPaddingBottom;

mForegroundBoundsChanged =true;

for(inti =0; i < count; i++) {

finalView child = getChildAt(i);

if(child.getVisibility() != GONE) {

finalLayoutParams lp = (LayoutParams) child.getLayoutParams();

finalintwidth = child.getMeasuredWidth();

finalintheight = child.getMeasuredHeight();

intchildLeft = parentLeft;

intchildTop = parentTop;

finalintgravity = lp.gravity;

if(gravity != -1) {

finalinthorizontalGravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK;

finalintverticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

switch(horizontalGravity) {

caseGravity.LEFT:

childLeft = parentLeft + lp.leftMargin;

break;

caseGravity.CENTER_HORIZONTAL:

childLeft = parentLeft + (parentRight - parentLeft - width) /2+

lp.leftMargin - lp.rightMargin;

break;

caseGravity.RIGHT:

childLeft = parentRight - width - lp.rightMargin;

break;

default:

childLeft = parentLeft + lp.leftMargin;

}

switch(verticalGravity) {

caseGravity.TOP:

childTop = parentTop + lp.topMargin;

break;

caseGravity.CENTER_VERTICAL:

childTop = parentTop + (parentBottom - parentTop - height) /2+

lp.topMargin - lp.bottomMargin;

break;

caseGravity.BOTTOM:

childTop = parentBottom - height - lp.bottomMargin;

break;

default:

childTop = parentTop + lp.topMargin;

}

}

child.layout(childLeft, childTop, childLeft + width, childTop + height);

}

}

}

......

}

这个函数定义在文件frameworks/base/core/java/android/widget/FrameLayout.java中。

FrameLayout类的成员变量mPaddingLeft、mPaddingRight、mPaddingTop、mPaddingBottom和mForegroundPaddingLeft、mForegroundPaddingRight、mForegroundPaddingTop、mForegroundPaddingBottom的含义我们在前面分析Android应用程序窗品的测量过程时已经解释过了,它们描述的是当前视图的内边距,而参数left、top、right和bottom描述的是当前视图的外边距,即它与父窗口的边距。通过上述这些参数,我们就可以得到当前视图的子视图所能布局在的区域。

FrameLayout类的成员函数onLayout通过一个for循环来布局当前视图的每一个子视图。如果一个子视图child是可见的,那么FrameLayout类的成员函数onLayout就会根据当前视图可以用来显示子视图的区域以及它所设置的gravity属性来得到它在应用程序窗口中的左上角位置(childeLeft,childTop)。

当一个子视图child在应用程序窗口中的左上角位置确定了之后,再结合它在前面的测量过程中所确定的宽度width和高度height,我们就可以完全地确定它在应用程序窗口中的布局了,即可以调用它的成员函数layout来设置它的位置和大小了,这刚好就是前面的Step 1所执行的操作。注意,如果当前正在布局的子视图child描述的也是一个视图容器,那么它又会重复执行Step 5的操作,直到它的所有子孙视图都布局完成为止。

至此,我们就分析完成Android应用程序窗口的布局过程了,接下来我们继续分析Android应用程序窗口的绘制过程。

3. Android应用程序窗口的绘制过程

ViewRoot类的成员函数draw首先会创建一块画布,接着再在画布上绘制Android应用程序窗口的UI,最后再将画布的内容交给SurfaceFlinger服务来渲染,这个过程如图4所示:

图4 Android应用程序窗口的绘制过程

这个过程可以分为14个步骤,接下来我们就详细分析每一个步骤。

Step 1. ViewRoot.draw

[java]view plaincopy

publicfinalclassViewRootextendsHandlerimplementsViewParent,

View.AttachInfo.Callbacks {

......

privatevoiddraw(booleanfullRedrawNeeded) {

Surface surface = mSurface;

......

intyoff;

finalbooleanscrolling = mScroller !=null&& mScroller.computeScrollOffset();

if(scrolling) {

yoff = mScroller.getCurrY();

}else{

yoff = mScrollY;

}

if(mCurScrollY != yoff) {

mCurScrollY = yoff;

fullRedrawNeeded =true;

}

floatappScale = mAttachInfo.mApplicationScale;

booleanscalingRequired = mAttachInfo.mScalingRequired;

Rect dirty = mDirty;

......

if(mUseGL) {

if(!dirty.isEmpty()) {

Canvas canvas = mGlCanvas;

if(mGL !=null&& canvas !=null) {

......

intsaveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);

try{

canvas.translate(0, -yoff);

if(mTranslator !=null) {

mTranslator.translateCanvas(canvas);

}

canvas.setScreenDensity(scalingRequired

? DisplayMetrics.DENSITY_DEVICE :0);

mView.draw(canvas);

......

}finally{

canvas.restoreToCount(saveCount);

}

......

}

}

if(scrolling) {

mFullRedrawNeeded =true;

scheduleTraversals();

}

return;

}

if(fullRedrawNeeded) {

......

dirty.union(0,0, (int) (mWidth * appScale +0.5f), (int) (mHeight * appScale +0.5f));

}

......

if(!dirty.isEmpty() || mIsAnimating) {

Canvas canvas;

try{

......

canvas = surface.lockCanvas(dirty);

......

}catch(Surface.OutOfResourcesException e) {

......

return;

}catch(IllegalArgumentException e) {

......

return;

}

try{

if(!dirty.isEmpty() || mIsAnimating) {

.....

mView.mPrivateFlags |= View.DRAWN;

......

intsaveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);

try{

canvas.translate(0, -yoff);

if(mTranslator !=null) {

mTranslator.translateCanvas(canvas);

}

canvas.setScreenDensity(scalingRequired

? DisplayMetrics.DENSITY_DEVICE :0);

mView.draw(canvas);

}finally{

......

canvas.restoreToCount(saveCount);

}

......

}

}finally{

surface.unlockCanvasAndPost(canvas);

}

}

......

if(scrolling) {

mFullRedrawNeeded =true;

scheduleTraversals();

}

}

......

}

这个函数定义在文件frameworks/base/core/java/android/view/ViewRoot.java中。

ViewRoot类的成员函数draw的执行流程如下所示:

1. 将成员变量mSurface所描述的应用程序窗口的绘图表面保存在变量surface中,以便接下来可以通过变量surface来操作应用程序窗口的绘图表面。

2. 调用成员变量mScroller所描述的一个Scroller对象的成员函数computeScrollOffset来计算应用程序窗口是否处于正在滚动的状态中。如果是的话,那么得到的变量scrolling就会等于true,这时候调用成员变量mScroller所描述的一个Scroller对象的成员函数getCurrY就可以得到应用程序窗口在Y轴上的即时滚动位置yoff。

3. 成员变量mScrollY用来描述应用程序窗口下一次绘制时在Y轴上应该滚动到的位置,因此,如果应用程序窗口不是处于正在滚动的状态,那么它在下一次绘制时,就应该直接将它在Y轴上的即时滚动位置yoff设置为mScrollY。

4. 成员变量mCurScrollY用来描述应用程序窗口上一次绘制时在Y轴上的滚动位置,如果它的值不等变量yoff的值,那么就表示应用程序窗口在Y轴上的滚动位置发生变化了,这时候就需要将变量yoff的值保存在成员变量mCurScrollY中,并且将参数fullRedrawNeeded的设置为true,表示要重新绘制应用程序窗口的所有区域。

5. 成员变量mAttachInfo所描述的一个AttachInfo对象的成员变量mScalingRequired表示应用程序窗口是否正在请求进行大小缩放,如果是的话,那么所请求的大小缩放因子就保存在这个AttachInfo对象的另外一个成员变量mApplicationScale中。函数将这两个值保存在变量scalingRequired和appScale中,以便接下来可以使用。

6. 成员变量mDirty描述的是一个矩形区域,表示应用程序窗口的脏区域,即需要重新绘制的区域。函数将这个脏区域保存变量dirty中,以便接下来可以使用。

7. 成员变量mUseGL用来描述应用程序窗口是否直接使用OpenGL接口来绘制UI。当应用程序窗口的绘图表面的内存类型等于WindowManager.LayoutParams.MEMORY_TYPE_GPU时,那么就表示它需要使用OpenGL接口来绘制UI,以便可以利用GPU来绘制UI。当应用程序窗口需要直接使用OpenGL接口来绘制UI时,另外一个成员变量mGlCanvas就表示应用程序窗口的绘图表面所使用的画布,这块画布同样是通过OpenGL接口来创建的。

8. 当应用程序窗口需要直接使用OpenGL接口来绘制UI时,函数接下来就会将它的UI绘制在成员变量mGlCanvas所描述的一块画布上,这是通过调用成员变量mView所描述的一个类型为DecorView的顶层视图的成员函数draw来实现的。注意,在绘制之前,还需要对画布进行适当的转换:A. 设置画布在Y轴上的偏移值yoff,以便可以正确反映应用程序窗口的滚动状态;B. 如果成员变量mTranslator的值不等于null,即它指向了一个Translator对象,那么就说明应用程序窗口运行在兼容模式下,这时候就需要相应对画布进行变换,以便可以正确反映应用程序窗口的大小;C. 当变量scalingRequired的值等于true时,同样说明应用程序窗口是运行在兼容模式下,这时候就需要修改画布在兼容模式下的点密度,以便可以正确地反映应用程序窗口的分辨率,注意,这时候屏幕在兼容模式下的点密度保存在DisplayMetrics类的静态成员变量DENSITY_DEVICE中。由于上述画布的转换操作只针对当前的这一次绘制操作有效,因此,函数就需要在绘制之后,调用画布的成员函数save来保存它在转换前的矩阵变换堆栈状态,以便在绘制完成之后,可以调用画布的成员函数restoreToCount来恢复之前的矩阵变换堆栈状态。

9. 使用OpenGL接口来绘制完成UI后,如果变量scrolling的值等于true,即应用程序窗口是处于正在滚动的状态,那么就意味着应用程序窗口接下来还需要马上进行下一次重绘,而且是所有的区域都需要重绘,因此,函数接下来就会将成员变量mFullRedrawNeeded的值设置为true,并且调用另外一个成员函数scheduleTraversals来请求执行下一次的重绘操作。

10. 以下的步骤针适用于使用非OpenGL接口来绘制UI的情况,也是本文所要关注的重点。

11. 参数fullRedrawNeeded用来描述是否需要绘制应用程序窗口的所有区域。如果需要的话,那么就会将应用程序窗口的脏区域的大小设置为整个应用程序窗口的大小(0,0,mWidth,mHeight),其中,成员变量mWidth和mHeight表示应用程序窗口的宽度和高度。注意,如果应用程序窗口的大小被设置了一个缩放因子,即变量appScale的值不等于1,那么就需要将应用程序窗口的宽度mWidth和高度mHeight乘以这个缩放因子,然后才可以得到应用程序窗口的实际大小。

12. 经过前面的一系列计算之后,如果应用程序窗口的脏区域dirty不等于空,或者应用程序窗口在正处于动画状态,即成员变量mIsAnimating的值等于true,那么函数接下来就需要重新绘制应用程序窗口的UI了。在绘制之前,首先会调用用来描述应用程序窗口的绘图表面的一个Surface对象surface的成员函数lockCanvas来创建一块画布canvas。有了这块画布之后,接下来就可以调用成员变量mView所描述的一个类型为DecorView的顶层视图的成员函数draw来在上面绘制应用程序窗口的UI了。 与前面的第8步一样,在绘制之前,还需要对画布进行适当的A、B和C转换,以及需要在绘制之后恢复画布在绘制之前的矩阵变换堆栈状态。

13. 绘制完成之后,应用程序窗口的UI就都体现在前面所创建的画布canvas上了,因此,这时候就需要将它交给SurfaceFlinger服务来渲染,这是通过调用用来描述应用程序窗口的绘图表面的一个Surface对象surface的成员函数unlockCanvasAndPost来实现的。

14. 在请求SurfaceFlinger服务渲染应用程序窗口的UI之后,函数同样是需要判断变量scrolling的值是否等于true。如果等于的话,那么就与前面的第9步一样,函数需要将成员变量mFullRedrawNeeded的值设置为true,并且调用另外一个成员函数scheduleTraversals来请求执行下一次的重绘操作。

在本文中,我们只关注使用非OpenGL接口来绘制应用程序窗口的UI的步骤,其中,第12步和第13步是关键所在。第12步调用了Java层的Surface类的成员函数lockCanvas来为应用程序窗口的绘图表面创建了一块画布,并且调用了DecorView类的成员函数draw来在这块画布上绘制了应用程序窗口的UI,而第13步调用了Java层的Surface类的成员函数unlockCanvasAndPost来将前面已经绘制了应用程序窗口UI的画布交给SurfaceFlinger服务来渲染。接下来,我们就分别分析Java层的Surface类的成员函数lockCanvas、DecorView类的成员函数draw和Java层的Surface类的成员函数unlockCanvasAndPost的实现。

Step 2. Surface.lockCanvas

[java]view plaincopy

publicclassSurfaceimplementsParcelable {

......

publicCanvas lockCanvas(Rect dirty)throwsOutOfResourcesException, IllegalArgumentException

{

/* the dirty rectangle may be expanded to the surface's size, if

* for instance it has been resized or if the bits were lost, since

* the last call.

*/

returnlockCanvasNative(dirty);

}

privatenativeCanvas lockCanvasNative(Rect dirty);

......

}

这个函数定义在文件frameworks/base/core/java/android/view/Surface.java中。

Surface类的成员函数lockCanvas调用另外一个成员函数lockCanvasNative来创建一块画布。Surface类的成员函数lockCanvasNative是一个JNI方法,它是由C++层的函数Surface_lockCanvas来实现的,如下所示:

[cpp]view plaincopy

staticjobject Surface_lockCanvas(JNIEnv* env, jobject clazz, jobject dirtyRect)

{

constsp& surface(getSurface(env, clazz));

......

// get dirty region

Region dirtyRegion;

if(dirtyRect) {

Rect dirty;

dirty.left  = env->GetIntField(dirtyRect, ro.l);

dirty.top   = env->GetIntField(dirtyRect, ro.t);

dirty.right = env->GetIntField(dirtyRect, ro.r);

dirty.bottom= env->GetIntField(dirtyRect, ro.b);

if(!dirty.isEmpty()) {

dirtyRegion.set(dirty);

}

}

......

Surface::SurfaceInfo info;

status_t err = surface->lock(&info, &dirtyRegion);

......

// Associate a SkCanvas object to this surface

jobject canvas = env->GetObjectField(clazz, so.canvas);

......

SkCanvas* nativeCanvas = (SkCanvas*)env->GetIntField(canvas, no.native_canvas);

SkBitmap bitmap;

......

if(info.w > 0 && info.h > 0) {

bitmap.setPixels(info.bits);

}else{

// be safe with an empty bitmap.

bitmap.setPixels(NULL);

}

......

SkRegion clipReg;

if(dirtyRegion.isRect()) {// very common case

constRect b(dirtyRegion.getBounds());

clipReg.setRect(b.left, b.top, b.right, b.bottom);

}else{

size_tcount;

Rectconst* r = dirtyRegion.getArray(&count);

while(count) {

clipReg.op(r->left, r->top, r->right, r->bottom, SkRegion::kUnion_Op);

r++, count--;

}

}

nativeCanvas->clipRegion(clipReg);

intsaveCount = nativeCanvas->save();

env->SetIntField(clazz, so.saveCount, saveCount);

if(dirtyRect) {

constRect& bounds(dirtyRegion.getBounds());

env->SetIntField(dirtyRect, ro.l, bounds.left);

env->SetIntField(dirtyRect, ro.t, bounds.top);

env->SetIntField(dirtyRect, ro.r, bounds.right);

env->SetIntField(dirtyRect, ro.b, bounds.bottom);

}

returncanvas;

}

这个函数定义在文件frameworks/base/core/jni/android_view_Surface.cpp中。

参数clazz指向的是一个Java层的Surface对象。从前面Android应用程序窗口(Activity)的绘图表面(Surface)的创建过程分析一文可以知道,每一个Java层的Surface对象在C++层都对应有一个Surface对象。因此,函数首先调用另外一个函数getSurface来获得与参数clazz所对应的C++层的Surface对象surface。

参数dirtyRect指向的是一个Java层的Rect对象,它描述的是应用程序窗口即将要重绘的一块矩形区域,函数接下来就将它所描述的矩形区域转换成一个C++层的Region对象dirtyRegion来表示。

函数接下来就调用前面所获得的C++层的Surface对象surface的成员函数lock来获得一个图形缓冲区,这个图形缓冲区使用一个SurfaceInfo对象info来描述,其中,图形缓冲区的地址就保存在它的成员变量bits中。

获得图形缓冲区之后,我们就可以在上面绘制应用程序窗口的UI了。由于Java层的应用程序窗口是通Skia图形库来绘制应用程序窗口的UI的,而Skia图形库在绘制UI时,是需要一块画布的,因此,函数接下来就会将前面所获得的图形缓冲区封装在一块画布中。

从前面Android应用程序窗口(Activity)的绘图表面(Surface)的创建过程分析一文还可以知道,每一个Java层的Surface对象内部都有一块画布,这块画布是通过它的成员变量mCanvas所指向的一个Java层的CompatibleCanvas对象来描述的。so是一个类型为so_t的结构体,它的成员变量canvas描述的是Java层的Surface类的成员变量mCanva在类中的偏移量,因此,通过这个偏移量就可以获得参数clazz所指向的一个Java层的Surface对象的内部的一块类型为CompatibleCanvas的画布canvas。

画布canvas的类型为Java层的CompatibleCanvas,它是从Canvas类继承下来的。Canvas类有一个成员变量mNativeCanvas,它指向的是一个C++层的SkCanvas对象,这个C++层的SkCanvas对象描述的就是Skia图形库绘制应用程序窗口UI时所需要的画布。no是一个类型为no_t的结构体,它的成员变量native_canvas描述的是Java层的Canvas类的成员变量mNativeCanvas在类中的偏移量,因此,通过这个偏移量就可以获得变量canvas所指向的一个Java层的CompatibleCanvas对象的内部的一块类型为SkCanvas的画布nativeCanvas。

获得了Skia图形库所需要的画布nativeCanvas之后,函数就可以将前面所获得的图形缓冲区的地址,即SurfaceInfo对象info的成员变量bits封装到它内部去了,这是通过调用它的成员函数setPixels来实现的。

函数接下来还会设置画布nativeCanvas的裁剪区域。这个裁剪区域是通过Region对象dirtyRegion来描述的,不过Skia图形库需要使用另外一个类型为SkRegion的对象clipReg来描述它。Region对象dirtyRegion所描述的区域有可能是一个矩形区域,也可能是一个不规则的区域。如果Region对象dirtyRegion描述的是一个矩形区域,那么就可以直接将这个矩形区域设置到SkRegion的对象clipReg里面去。如果Region对象dirtyRegion描述的是一个不规则区域,那么这个不规则区域就是由一系列的矩形小区域来描述的,这时候就将这些矩形小区域合并起来,并且设置到kRegion的对象clipReg里面去。

设置好SkRegion的对象clipReg所包含的区域之后,函数就可以调用前面得到的SkCanvas画布nativeCanvas的成员函数clipRegion来将它设置为自己的裁剪区域了,接下来函数还会将该裁剪区域所围成的一个矩形区域的位置和大小设置到参数dirtyRect所描述的一个Java层的Rect对象中去,以便调用者可以知道现在正在创建的画布的大小。

函数在将与C++层的SkCanvas画布nativeCanvas所关联的一个Java层的CompatibleCanvas画布canvas返回给调用者之前,还会将画布的当前堆栈状态保存下来,以便在绘制完成应用程序窗口的UI之后,可以恢复回来,这是通过调用C++层的SkCanvas画布nativeCanvas的成员函数save来实现的。画布的当前堆栈状态是通过一个整数来描述的,这个整数即为C++层的SkCanvas画布nativeCanvas的成员函数save的返回值saveCount,它会被保存在参数clazz所描述的一个Java层的Surface对象的成员变量mSaveCount中,等到应用程序窗口的UI绘制完成之后,就可以通过这个整数来恢复画布的堆栈状态了。

接下来,我们继续分析C++层的Surface类的成员函数lock的实现,以便可以了解用来创建绘制应用程序窗口UI所需要的画布的图形缓冲区是如何获得的。

Step 3. Surface.lock

[cpp]view plaincopy

status_t Surface::lock(SurfaceInfo* other, Region* dirtyIn,boolblocking)

{

......

if(mApiLock.tryLock() != NO_ERROR) {

......

returnWOULD_BLOCK;

}

/* Here we're holding mApiLock */

if(mLockedBuffer != 0) {

......

mApiLock.unlock();

returnINVALID_OPERATION;

}

// we're intending to do software rendering from this point

setUsage(GRALLOC_USAGE_SW_READ_OFTEN | GRALLOC_USAGE_SW_WRITE_OFTEN);

android_native_buffer_t* out;

status_t err = dequeueBuffer(&out);

......

if(err == NO_ERROR) {

sp backBuffer(GraphicBuffer::getSelf(out));

err = lockBuffer(backBuffer.get());

......

if(err == NO_ERROR) {

constRect bounds(backBuffer->width, backBuffer->height);

constRegion boundsRegion(bounds);

Region scratch(boundsRegion);

Region& newDirtyRegion(dirtyIn ? *dirtyIn : scratch);

newDirtyRegion &= boundsRegion;

// figure out if we can copy the frontbuffer back

constsp& frontBuffer(mPostedBuffer);

constboolcanCopyBack = (frontBuffer != 0 &&

backBuffer->width  == frontBuffer->width &&

backBuffer->height == frontBuffer->height &&

backBuffer->format == frontBuffer->format &&

!(mFlags & ISurfaceComposer::eDestroyBackbuffer));

......

if(canCopyBack) {

// copy the area that is invalid and not repainted this round

constRegion copyback(mOldDirtyRegion.subtract(newDirtyRegion));

if(!copyback.isEmpty())

copyBlt(backBuffer, frontBuffer, copyback);

}else{

// if we can't copy-back anything, modify the user's dirty

// region to make sure they redraw the whole buffer

newDirtyRegion = boundsRegion;

}

......

void* vaddr;

status_t res = backBuffer->lock(

GRALLOC_USAGE_SW_READ_OFTEN | GRALLOC_USAGE_SW_WRITE_OFTEN,

newDirtyRegion.bounds(), &vaddr);

......

mLockedBuffer = backBuffer;

other->w      = backBuffer->width;

other->h      = backBuffer->height;

other->s      = backBuffer->stride;

other->usage  = backBuffer->usage;

other->format = backBuffer->format;

other->bits   = vaddr;

}

}

mApiLock.unlock();

returnerr;

}

这个函数定义在文件frameworks/base/libs/surfaceflinger_client/Surface.cpp中。

Surface类的成员变量mApiLock是一个类型为Mutex的互斥锁,它是用来保证Surface类的成员函数lock是线程安全的。如果调用Surface类的成员变量mApiLock所描述的一个Mutex对象的成员函数tryLock的返回值不等于NO_ERROR,那么就说明这个Mutex对象已经被另外一个线程获得了,因此,这时候函数就直接返回一个错误码WOULD_BLOCK给调用者了。

Surface类的成员变量mLockedBuffer的类型为GraphicBuffer,如果它的值不等于0,那么它指向的就是应用程序窗口当前正在使用的图形缓冲区。如果应用程序窗口正在使用一个图形缓冲区,那么它是不可以再请求分配另一个图形缓冲区的,因此,当Surface类的成员变量mLockedBuffer的值不等于0时,函数就直接返回一个错误码INVALID_OPERATION给调用者了。

通过了前面的检查之后,Surface类的成员函数lock接下来就开始要分配一个图形缓冲区了,不过在分配之后,首先调用另外一个成员函数setUsage来将当前正在处理的Surface对象所描述的应用程序窗口的绘图表面的属性设置为(GRALLOC_USAGE_SW_READ_OFTEN | GRALLOC_USAGE_SW_WRITE_OFTEN),表示该应用程序窗口的UI是需要通过软件方式来渲染的,这是相对于使用GPU来渲染而言的。

Surface类的成员函数lock接下来就调用另外一个成员函数dequeueBuffer来获得一个新的图形缓冲区了,这个新的图形缓冲区使用一个android_native_buffer_t对象out来描述的。在前面Android应用程序请求SurfaceFlinger服务渲染Surface的过程分析一文中,我们已经分析过Surface类的成员函数dequeueBuffer的实现了,它主要就是请求SurfaceFlinger服务来分配一个图形缓冲区。

前面获得的android_native_buffer_t对象out接下来又会被封装成一个GraphicBuffer对象backBuffer,这样,Surface类的成员函数lock接下来就会通过GraphicBuffer对象backBuffer来访问前面所获得的图形缓冲区。

Surface类是使用一种称为双缓冲的技术来渲染应用程序窗口的UI的。这种双缓冲技术需要两个图形缓冲区,其中一个称为前端缓冲区,另外一个称为后端缓冲区。前端缓冲区是正在渲染的图形缓冲区,而后端缓冲区是接下来要渲染的图形缓冲区,它们分别通过Surface类的成员变量mPostedBuffer和mLockedBuffer所指向的两个GraphicBuffer对象来描述。前面所获得的图形缓冲区backBuffer是作为后端缓冲区来使用的,即接下来它所指向的图形缓冲区也需要保存在Surface类的成员变量mLockedBuffer中。

在将图形缓冲区backBuffer返回给调用者之前,Surface类的成员函数lock还需要对它进行进一步的处理,即判断是否需要前端缓冲区mPostedBuffer的内容拷贝回它里面去,以便可以支持部分更新应用程序窗口UI的功能。在满足以下三个条件下,Surface类的成员函数lock可以将前端缓冲区的内容拷贝到后端缓冲区中去:

1. 前端缓冲区的内容拷贝到后端缓冲区所描述的区域的宽度和高度相同。

2. 前端缓冲区和后端缓冲区的像素格式相同。

3. 应用程序窗口绘图表面的属性值mFlags的ISurfaceComposer::eDestroyBackbuffer位等于0,即在渲染了应用程序窗口的UI之后,应该保留正在渲染的图形缓冲区的内容。

如果能将前端缓冲区的内容拷贝到后端缓冲区中去,那么就不用重新绘制应用程序窗口的所有区域,而只需要绘制那些脏的区域,即Region对象newDirtyRegion所描述的区域。注意,参数dirtyIn所描述的区域是原先指定的脏区域,但是在分配了新的后端缓冲区backBuffer之后,我们需要将新的图形缓冲区backBuffer所描述的区域boundsRegion与原先指定的脏区域作一个与操作,得到才是最后需要重绘的脏区域newDirtyRegion。由于在这种情况下,我们只在后端缓冲区backBuffer绘制中绘制应用程序窗口的脏区域,因此,就需要将那些干净的区域从前端缓冲区frontBuffer拷贝到图形缓冲区backBuffer的对应位置去,这是通过调用函数copyBlt来实现的。应用程序窗口的干净区域使用Region对象copyback来描述,它是从应用程序窗口上一次所重绘的区域减去接下来需要重绘的脏区域newDirtyRegion得到的,而应用程序窗口上一次所重绘的区域是保存在Surface类的成员变量mOldDirtyRegion中的。

如果不能将前端缓冲区的内容拷贝到后端缓冲区中去,那么接下来就需要重新绘制应用程序窗口的所有区域了,这时候应用程序窗口的脏区域newDirtyRegion就会被修改为后端缓冲区backBuffer所描述的区域boundsRegion。

Surface类的成员函数lock处理完成前后端缓冲区的拷贝问题之后,接下来就会调用后端缓冲区backBuffer所指向的一个GraphicBuffer对象的成员函数lock来获得它的地址vaddr,以便接下来保存在参数other所描述的一个SurfaceInfo对象的成员变量bits中,这样调用者就获得后端缓冲区backBuffer的地址值了。注意,同时保存在SurfaceInfo对象中的信息还包括后端缓冲区backBuffer的宽度width、高度height、每行像素点stride、用途usage和像素格式format。

Surface类的成员函数lock还会将接下来要重绘的脏缓冲区newDirtyRegion保存在Surface类的成员变量mOldDirtyRegion中,以便再下一次为应用程序窗口分配图形缓冲区时,可以知道应用程序窗口的上一次重绘区域,即上一次干净区域。

此外,Surface类的成员函数lock还会将后端缓冲区backBuffer保存在Surface类的成员变量mLockedBuffer,这样就可以知道应用程序窗口当前正在使用的图形缓冲区,即下一次要请求SurfaceFlinger服务渲染的图形缓冲区。

最后,Surface类的成员函数lock首先调用成员变量mApiLock所指向的一个Mutex对象的成员函数unlock,以便中可以释放前面所获得的锁,然后再返回到上一步去。

接下来,我们继续分析GraphicBuffer类的成员函数lock的实现,以便可以了解一个图形缓冲区的地址是如何获得的。

Step 4. GraphicBuffer.lock

[cpp]view plaincopy

status_t GraphicBuffer::lock(uint32_t usage,constRect& rect,void** vaddr)

{

......

status_t res = getBufferMapper().lock(handle, usage, rect, vaddr);

returnres;

}

这个函数定义在文件frameworks/base/libs/ui/GraphicBuffer.cpp中。

GraphicBuffer类的成员变量handle是从父类android_native_buffer_t继承下来的,它的类型为buffer_handle_t,用来作为一个图形缓冲区的句柄,这个知识点可以参考前面Android应用程序请求SurfaceFlinger服务渲染Surface的过程分析一文。

GraphicBuffer类的成员函数lockGraphicBuffer首先调用另外一个成员函数getBufferMapper来获得一个GraphicBufferMapper对象,然后再调用这个GraphicBufferMapper对象的成员函数lock来获得成员变量handle所描述的一个图形缓冲区的地址,并且保存在输出参数vaddr中。

接下来,我们就继续分析GraphicBufferMapper类的成员函数lock的实现。

Step 5. GraphicBufferMapper.lock

[cpp]view plaincopy

status_t GraphicBufferMapper::lock(buffer_handle_t handle,

intusage,constRect& bounds,void** vaddr)

{

status_t err;

if(sw_gralloc_handle_t::validate(handle) < 0) {

err = mAllocMod->lock(mAllocMod, handle, usage,

bounds.left, bounds.top, bounds.width(), bounds.height(),

vaddr);

}else{

err = sw_gralloc_handle_t::lock((sw_gralloc_handle_t*)handle, usage,

bounds.left, bounds.top, bounds.width(), bounds.height(),

vaddr);

}

LOGW_IF(err,"lock(...) failed %d (%s)", err, strerror(-err));

returnerr;

}

这个函数定义在文件frameworks/base/libs/ui/GraphicBufferMapper.cpp。

GraphicBufferMapper类的成员函数lock首先调用sw_gralloc_handle_t类的静态成员函数validate来验证参数andle所描述的一个图形缓冲区是否是在w_gralloc_handle_t模块中分配的。如果是的话,那么就需要调用sw_gralloc_handle_t类的静态成员函数lock来获得参数andle所描述的一个图形缓冲区的地址,否则的话,就需要调用GraphicBufferMapper类的成员变量mAllocMod所描述的一个HAL模块Gralloc的成员函数lock来获得参数andle所描述的一个图形缓冲区的地址。

从前面Android应用程序请求SurfaceFlinger服务渲染Surface的过程分析一文可以知道,应用程序窗口所使用的图形缓冲区一般都是在HAL模块Gralloc中分配的,因此,GraphicBufferMapper类的成员函数lock接下来就会调用成员变量mAllocMod所描述的一个HAL模块Gralloc的成员函数lock来获得参数andle所描述的一个图形缓冲区的地址,并且保存在输出参数vaddr中。HAL模块Gralloc的成员函数lock的实现可以参考前面Android帧缓冲区(Frame Buffer)硬件抽象层(HAL)模块Gralloc的实现原理分析一文,这里不再详述。

这一步执行完成之后,返回到前面的Step 1中,即ViewRoot类的成员函数draw中,接下来就会继续调用其成员变量mView所描述的一个DecorView对象的成员函数draw来在前面所获得一块画布上面绘制应用程序窗口的UI。

Step 6. DecorView.draw

[java]view plaincopy

publicclassPhoneWindowextendsWindowimplementsMenuBuilder.Callback {

......

privatefinalclassDecorViewextendsFrameLayoutimplementsRootViewSurfaceTaker {

......

privateDrawable mMenuBackground;

......

@Override

publicvoiddraw(Canvas canvas) {

super.draw(canvas);

if(mMenuBackground !=null) {

mMenuBackground.draw(canvas);

}

}

......

}

......

}

这个函数定义在文件frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindow.java中。

DecorView类的成员函数draw首先调用父类FrameLayout的成员函数draw来绘制它的UI内容,然后再检查它是否设置了菜单背景,即成员变量mMenuBackground的值是否不等于null。如果不等于null的话,那么就会调用它所指向的一个Drawable对象的成员函数draw来在画布canvas上绘制这个菜单背景。

接下来,我们就继续分析FrameLayout的成员函数draw的实现,以便可以了解应用程序窗口UI的绘制过程。

Step 7. FrameLayout.draw

[java]view plaincopy

publicclassFrameLayoutextendsViewGroup {

......

privateDrawable mForeground;

......

publicvoiddraw(Canvas canvas) {

super.draw(canvas);

if(mForeground !=null) {

finalDrawable foreground = mForeground;

if(mForegroundBoundsChanged) {

mForegroundBoundsChanged =false;

finalRect selfBounds = mSelfBounds;

finalRect overlayBounds = mOverlayBounds;

finalintw = mRight-mLeft;

finalinth = mBottom-mTop;

if(mForegroundInPadding) {

selfBounds.set(0,0, w, h);

}else{

selfBounds.set(mPaddingLeft, mPaddingTop, w - mPaddingRight, h - mPaddingBottom);

}

Gravity.apply(mForegroundGravity, foreground.getIntrinsicWidth(),

foreground.getIntrinsicHeight(), selfBounds, overlayBounds);

foreground.setBounds(overlayBounds);

}

foreground.draw(canvas);

}

}

......

}

这个函数定义在文件frameworks/base/core/java/android/widget/FrameLayout.java中。

FrameLayout类的成员函数draw首先调用父类View的成员函数draw来绘制它的UI内容,然后再检查它是否设置了一个前景图,即成员变量mForeground的值是否等于null。如果不等于null的话,那么就会先设置这个前景图的大小和位置,然后再调用用成员变量mForeground所指向的一个Drawable对象的成员函数draw来在画布canvas上绘制这个前景图。

接下来,我们就继续分析View类的成员函数draw的实现,以便可以了解应用程序窗口UI的绘制过程。

Step 8. View.draw

这个函数定义在文件frameworks/base/core/java/android/view/View.java中,它主要是完成以下六个操作:

1. 绘制当前视图的背景。

2. 保存当前画布的堆栈状态,并且在在当前画布上创建额外的图层,以便接下来可以用来绘制当前视图在滑动时的边框渐变效果。

3. 绘制当前视图的内容。

4. 绘制当前视图的子视图的内容。

5. 绘制当前视图在滑动时的边框渐变效果。

6. 绘制当前视图的滚动条。

在上面六个操作中,有些是可以优化的。例如,如果当前视图的某一个子视图是不透明的,并且覆盖了当前视图的内容,那么当前视图的背景以及内容就不会绘制了,即不用执行第1和第3个操作。又如,如果当前视图不是处于滑动的状态,那么第2和第5个操作也是不用执行的。

接下来我们就分段来阅读View类的成员函数draw的代码:

[java]view plaincopy

publicclassViewimplementsDrawable.Callback, KeyEvent.Callback, AccessibilityEventSource {

......

publicvoiddraw(Canvas canvas) {

......

finalintprivateFlags = mPrivateFlags;

finalbooleandirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&

(mAttachInfo ==null|| !mAttachInfo.mIgnoreDirtyState);

mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;

这段代码检查View类的成员变量mPrivateFlags的DIRTY_OPAQUE位是否等于1。如果等于1的话,那么就说明当前视图的某一个子视图请求了一个不透明UI绘制操作。在这种情况下,当前视图会被子视图覆盖,因此,就不需要执行前面所说的第1和第3个操作了。不过,不用执行第1和第3个操作还有一个前提,那就是View类的成员变量mAttachInfo所指向的一个AttachInfo对象的成员变量mIgnoreDirtyState的值等于false,这表示当前视图不可以忽略成员变量mPrivateFlags的DIRTY_OPAQUE位。满足了上述两个条件之后,变量dirtyOpaque的值就会等于true。

View类的成员函数在继续往下执行之前,还会将成员变量mPrivateFlags的DIRTY_MASK位重置为0,以及将DRAWN位设置为1,因为接下来就要开始绘制当前视图的UI了。

我们继续往下阅读代码:

[java]view plaincopy

// Step 1, draw the background, if needed

intsaveCount;

if(!dirtyOpaque) {

finalDrawable background = mBGDrawable;

if(background !=null) {

finalintscrollX = mScrollX;

finalintscrollY = mScrollY;

if(mBackgroundSizeChanged) {

background.setBounds(0,0,  mRight - mLeft, mBottom - mTop);

mBackgroundSizeChanged =false;

}

if((scrollX | scrollY) ==0) {

background.draw(canvas);

}else{

canvas.translate(scrollX, scrollY);

background.draw(canvas);

canvas.translate(-scrollX, -scrollY);

}

}

}

这段代码用来执行上述的第1个操作,但是它只会变量dirtyOpaque的值等于false的情况下才会执行。当前视图的背景是通过成员变量mBGDrawable所指向的一个Drawable对象来描述的。在绘制当前视图的背景之前,还会先设置它的大小和位置。

我们继续往下阅读代码:

[java]view plaincopy

// skip step 2 & 5 if possible (common case)

finalintviewFlags = mViewFlags;

booleanhorizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) !=0;

booleanverticalEdges = (viewFlags & FADING_EDGE_VERTICAL) !=0;

if(!verticalEdges && !horizontalEdges) {

// Step 3, draw the content

if(!dirtyOpaque) onDraw(canvas);

// Step 4, draw the children

dispatchDraw(canvas);

// Step 6, draw decorations (scrollbars)

onDrawScrollBars(canvas);

// we're done...

return;

}

这段代码检查是否可以跳过上述的第2和第5个操作。当View类的成员变量mViewFlags的FADING_EDGE_HORIZONTAL位等于1的时候,就说明当前视图正在处于水平滑动状态,并且需要绘制水平边框的渐变效果。同样,当View类的成员变量mViewFlags的FADING_EDGE_VERTICAL位等于1的时候,就说明当前视图正在处于垂直滑动状态,并且需要绘制垂直边框的渐变效果。但是,如果当前视图不是处于滑动状态,即变量horizontalEdges和verticalEdges的值均等于false的时候,那么就不需要执行上述的第2和第5个操作了,而只需要执行第3、第4和第6个操作。注意,当变量dirtyOpaque的值等于true的时候,第3个操作也是不需要执行的。

我们继续往下分析代码:

[java]view plaincopy

booleandrawTop =false;

booleandrawBottom =false;

booleandrawLeft =false;

booleandrawRight =false;

floattopFadeStrength =0.0f;

floatbottomFadeStrength =0.0f;

floatleftFadeStrength =0.0f;

floatrightFadeStrength =0.0f;

// Step 2, save the canvas' layers

intpaddingLeft = mPaddingLeft;

intpaddingTop = mPaddingTop;

finalbooleanoffsetRequired = isPaddingOffsetRequired();

if(offsetRequired) {

paddingLeft += getLeftPaddingOffset();

paddingTop += getTopPaddingOffset();

}

intleft = mScrollX + paddingLeft;

intright = left + mRight - mLeft - mPaddingRight - paddingLeft;

inttop = mScrollY + paddingTop;

intbottom = top + mBottom - mTop - mPaddingBottom - paddingTop;

if(offsetRequired) {

right += getRightPaddingOffset();

bottom += getBottomPaddingOffset();

}

finalScrollabilityCache scrollabilityCache = mScrollCache;

intlength = scrollabilityCache.fadingEdgeLength;

// clip the fade length if top and bottom fades overlap

// overlapping fades produce odd-looking artifacts

if(verticalEdges && (top + length > bottom - length)) {

length = (bottom - top) /2;

}

// also clip horizontal fades if necessary

if(horizontalEdges && (left + length > right - length)) {

length = (right - left) /2;

}

if(verticalEdges) {

topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));

drawTop = topFadeStrength >=0.0f;

bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));

drawBottom = bottomFadeStrength >=0.0f;

}

if(horizontalEdges) {

leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));

drawLeft = leftFadeStrength >=0.0f;

rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));

drawRight = rightFadeStrength >=0.0f;

}

saveCount = canvas.getSaveCount();

intsolidColor = getSolidColor();

if(solidColor ==0) {

finalintflags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;

if(drawTop) {

canvas.saveLayer(left, top, right, top + length,null, flags);

}

if(drawBottom) {

canvas.saveLayer(left, bottom - length, right, bottom,null, flags);

}

if(drawLeft) {

canvas.saveLayer(left, top, left + length, bottom,null, flags);

}

if(drawRight) {

canvas.saveLayer(right - length, top, right, bottom,null, flags);

}

}else{

scrollabilityCache.setFadeColor(solidColor);

}

这段代码用来检查是否需要保存参数canvas所描述的一块画布的堆栈状态,并且创建额外的图层来绘制当前视图在滑动时的边框渐变效果。视图的边框是绘制在内容区域的边界位置上的,而视图的内容区域是需要排除成员变量mPaddingLeft、mPaddingRight、mPaddingTop和mPaddingBottom所描述的视图内边距的。此外,视图的边框有四个,分别位于视图的左、右、上以及下内边界上。因此,这段代码首先需要计算出当前视图的左、右、上以及下内边距的大小,以便得到边框所要绘制的区域。

本来通过View类的成员变量mPaddingLeft、mPaddingRight、mPaddingTop和mPaddingBottom就可以得到当视图的左、右、上以及下内边距的大小的,但是有时候我们在定制一个视图的时候,可能会需要在视图的内边距上绘制其它额外的东西,这时候就有扩展视图的内边距的需求。如果有扩展视图的内边距的需求,那么就需要重写View类的成员函数isPaddingOffsetRequired,即将它的返回值设置为true,并且重载另外四个成员函数getLeftPaddingOffset、getRightPaddingOffset、getTopPaddingOffset和getBottomPaddingOffset来提供额外的左、右、上以及下内边距。

这段代码经过计算后,就得到四个值left、right、top和bottom,它们分别表示当前视图可以用来绘制的内容区域,这个区域已经将内置的和扩展的内边距排除之外。

计算好left、right、top和bottom这四个值之后,就相当于得到左、右、上以及下边框的起始位置了,但是我还需要知道边框的长度,才能确定左、右、上以及下边框所要绘制的区域。

边框的长度length设置在View类的成员变量mScrollCache所指向的一个ScrollabilityCache对象的成员变量fadingEdgeLength中。但是,这个预先设置的边框长度length不一定适合当前视图使用。这是因为视图的大小是可以随时改变的,一旦发生了改变之后,原先设置的边框长度length可能就会显得过长。具体来说,就是当上下两个边框或者左右两个边框发生重叠时,就说明原先设置的边框长度过长了。在这种情况下,就要将边框长度length修改为当前视图的内容区域的高度和宽度的较小者的一半,以便可以保证上下两个边框或者左右两个边框不会发生重叠。

左、右、上以及下边框都对应有一个强度值,强度值的取值范围为[0.0, 1.0]。如果一个边框的强度值等于0.0,那么它就是不可见的,这时候就不需要绘制它的渐变效果。另一方面,如果一个边框的强度值等于1.0,那么它的长度等于原来设置的长度。我们可以将这个强度值理解为一个缩放因子。左、右、上以及下边框的强度值可以分别通过调用View类的成员函数getLeftFadingEdgeStrength、getRightFadingEdgeStrength、getTopFadingEdgeStrength以及getBottomFadingEdgeStrength来获得。注意,只有在变量verticalEdges的值等于true的时候,这段代码才会计算上下两个边框的强度值topFadeStrength和bottomFadeStrength;同样,只有变量horizontalEdges的值等于true的时候,这代码才会计算左右两个边框的强度值leftFadeStrength和rightFadeStrength。

计算好左、右、上以及下边框的强度值leftFadeStrength、rightFadeStrength、topFadeStrength以及bottomFadeStrength之后,这段代码就会判断它们的值是否大于0。如果大于0,那么与它们所对应的四个变量drawLeft、drawRight、drawTop以及drawBottom的值就会等于true,表示需要绘制左、右、上以及下四个边框的渐变效果。

View类的成员函数getSolidColor返回的是当前视图的背景颜色。如果当前视图的背景颜色是纯色的,即变量solidColor的值不等于0,那么这时候就会使用这个背景颜色来绘制边框的渐变效果,即调用变量scrollabilityCache所指向的一个ScrollabilityCache对象的成员函数setFadeColor来将将边框的渐变效果颜色设置为solidColor,这种情况是比较简单的。如果当前视图的背景颜色不是纯色的,即变量solidColor的值等于0,这种情况就比较复杂了,我们需要创建在参数canvas所描述的一块画布上来创建额外的图层来绘制边框的渐变效果,这样做是为了能够使用背景颜色来绘制边框的渐变效果。

参数canvas所描述的一块画布上来创建额外的图层是通过调用它的成员函数saveLayer来实现的。我们注意到在调用参数canvas所指向的一个Canvas对象的成员函数saveLayer的时候,最后一个参数指定为Canvas.HAS_ALPHA_LAYER_SAVE_FLAG,这表示在将额外创建的图层合成到参数canvas所描述的一块画布上去,要给额外创建的图层设置一个透明度值。同时,我们还可以看出,当前视图的左、右、上和下边框所占据的范围分别为(left, top, left + length, bottom)、(right - length, top, right, bottom)、(left, top, right, top + length)和(left, bottom - length, right, bottom)。还有另外一个地方需要注意的是,在参数canvas所描述的一块画布上来创建额外的图层之前,这段代码首先会获得画布的当前堆栈状态,这是通过一个整数saveCount来描述的,并且这个整数是通过调用参数canvas所指向的一个Canvas对象的成员函数getSaveCount来获得的。这样,后面在额外创建的图层上绘制了边框的渐变效果之后,就可以通过前面得到的整数saveCount将恢复画布的堆栈状态,也就是将前面额外创建的图层合成到画布上来。

我们接着往下阅读代码:

[java]view plaincopy

// Step 3, draw the content

if(!dirtyOpaque) onDraw(canvas);

// Step 4, draw the children

dispatchDraw(canvas);

这段代码用来执行上述的第3和第4个操作,即绘制当前视图的内容,以及当前视图的子视图的内容,它们分别是通过调用View类的成员函数onDraw和dispatchDraw来实现的。本文主要关注当前视图的子视图的绘制过程,因此,在接下来的Step 9中,我们再详细分析View类的成员函数dispatchDraw的实现。

我们接着往下阅读代码:

[java]view plaincopy

// Step 5, draw the fade effect and restore layers

finalPaint p = scrollabilityCache.paint;

finalMatrix matrix = scrollabilityCache.matrix;

finalShader fade = scrollabilityCache.shader;

finalfloatfadeHeight = scrollabilityCache.fadingEdgeLength;

if(drawTop) {

matrix.setScale(1, fadeHeight * topFadeStrength);

matrix.postTranslate(left, top);

fade.setLocalMatrix(matrix);

canvas.drawRect(left, top, right, top + length, p);

}

if(drawBottom) {

matrix.setScale(1, fadeHeight * bottomFadeStrength);

matrix.postRotate(180);

matrix.postTranslate(left, bottom);

fade.setLocalMatrix(matrix);

canvas.drawRect(left, bottom - length, right, bottom, p);

}

if(drawLeft) {

matrix.setScale(1, fadeHeight * leftFadeStrength);

matrix.postRotate(-90);

matrix.postTranslate(left, top);

fade.setLocalMatrix(matrix);

canvas.drawRect(left, top, left + length, bottom, p);

}

if(drawRight) {

matrix.setScale(1, fadeHeight * rightFadeStrength);

matrix.postRotate(90);

matrix.postTranslate(right, top);

fade.setLocalMatrix(matrix);

canvas.drawRect(right - length, top, right, bottom, p);

}

canvas.restoreToCount(saveCount);

这段代码是用来绘制当前视图的左、右、上以及下边框的渐变效果。注意,只有左、右、上以及下边框所对应的四个变量drawLeft、drawRight、drawTop以及drawBottom的值等于true时,左、右、上以及下边框的渐变效果才需要绘制。同时,左、右、上以及下边框在绘制的时候,都会被设置一个缩放因子,即前面计算得到的左、右、上以及下边框的强度值leftFadeStrength、rightFadeStrength、topFadeStrength以及bottomFadeStrength。

由于当前视图的左、右、上以及下边框的渐变效果是在参数canvas所描述的一块画布的额外创建的图层上绘制的,因此,在绘制完之后,这段代码需要调用参数canvas所指向的一个Canvas对象来恢复参数canvas所描述的一块画布在创建额外图层时的堆栈状态,即相当于是将前面所绘制的边框渐变效果合成到参数canvas所描述的一块画布来。

我们继续往下阅读最后一段代码:

[java]view plaincopy

// Step 6, draw decorations (scrollbars)

onDrawScrollBars(canvas);

}

......

}

这段代码用来执行上述的第6个操作,即调用View类的成员函数onDrawScrollBars来绘制当前视图的滚动条。

接下来,我们就主要关注当前视图的子视图的绘制过程,即View类的成员函数dispatchDraw的实现。注意,View类的成员函数dispatchDraw是一个空实现,它是由子类ViewGroup来重写的,也就是说,只有当一个视图描述的是一个视图容器时,它才会重写父类View的成员函数dispatchDraw。

前面我们已经假设当前正在处理的视图是应用程序窗口的顶层视图,即它是一个类型为DecorView视图。DecorView类是从ViewGroup类继承下来的,并且在ViewGroup类中重写了父类View类的成员函数dispatchDraw。因此,接下来我们就继续分析ViewGroup的成员函数dispatchDraw的实现。

Step 9. ViewGroup.dispatchDraw

这个函数定义在文件frameworks/base/core/java/android/view/ViewGroup.java中,它的实现比较长,我们分段来阅读:

[java]view plaincopy

publicabstractclassViewGroupextendsViewimplementsViewParent, ViewManager {

......

@Override

protectedvoiddispatchDraw(Canvas canvas) {

finalintcount = mChildrenCount;

finalView[] children = mChildren;

intflags = mGroupFlags;

ViewGroup类的成员变量mChildrenCount描述的是当前视图组的子视图的个数,另外一个成员变量mChildren是一个类型为View的数组,用来保存当前视图组的子视图。此外,ViewGroup类的成员变量mGroupFlags用来描述当前视图组的标志位。这段代码将上述ViewGroup类的三个成员变量分别保存在变量count、children和flags中,以便接下来可以访问。

我们继续往下阅读代码:

[java]view plaincopy

if((flags & FLAG_RUN_ANIMATION) !=0&& canAnimate()) {

......

for(inti =0; i < count; i++) {

finalView child = children[i];

if((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {

......

bindLayoutAnimation(child);

......

}

}

finalLayoutAnimationController controller = mLayoutAnimationController;

......

controller.start();

......

if(mAnimationListener !=null) {

mAnimationListener.onAnimationStart(controller.getAnimation());

}

}

这段代码用来检查当前视图组的子视图是否需要显示动画。如果变量flags的FLAG_RUN_ANIMATION位等于1,并且ViewGroup类的成员函数canAnimate的返回值等于true,即当前当前视图组允许其子视图显示动画,那么这段代码接下来就要开始显示动画了。

这段代码首先检查当前视图组的每一个子视图child,如果它是可见的,那么就会调用ViewGroup类的另外一个成员函数bindLayoutAnimation来设置它的动画。设置完成子视图的动画之后,这段代码接下来再调用ViewGroup类的成员变量mLayoutAnimationController所指向的一个LayoutAnimationController对象的成员函数start来启动动画,并且调用ViewGroup类的成员变量mAnimationListener所指向的一个AnimationListener对象的成员函数onAnimationStart来通知那些注册到当前视图组的动画监听者,当前视图组开始显示动画了。

我们继续往下阅读代码:

[java]view plaincopy

intsaveCount =0;

finalbooleanclipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;

if(clipToPadding) {

saveCount = canvas.save();

canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,

mScrollX + mRight - mLeft - mPaddingRight,

mScrollY + mBottom - mTop - mPaddingBottom);

}

这段代码检查变量flags的CLIP_TO_PADDING_MASK位是否不等于1。如果不等于1的话,那么就说明需要设置参数canvas所描述的一块画布的剪裁区域,使得这个裁剪区域不包含当前视图组的内边距。注意,当前视图组的内边距是通过从父类View继承下来的四个成员变量mPaddingLeft、mPaddingRight、mPaddingTop和mPaddingBottom来描述的。此外,当前视图组的区域是通过从父类继承下来的四个成员变量量mLeft、mRight、mTop和mBottom描述的。再结合当前视图的当前滚动位置mScrollX的mScrollY,就可以计算出参数canvas所描述的一块画布的剪裁区域。

在设置参数canvas所描述的一块画布的剪裁区域之前,这段代码会先调用参数canvas所指向的一个Canvas对象的成员函数save来保存它的堆栈状态,以便在绘制完成当前视图组的UI之后,可以恢复canvas所描述的一块画布的堆栈状态。

我们继续往下阅读代码:

[java]view plaincopy

booleanmore =false;

finallongdrawingTime = getDrawingTime();

if((flags & FLAG_USE_CHILD_DRAWING_ORDER) ==0) {

for(inti =0; i < count; i++) {

finalView child = children[i];

if((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() !=null) {

more |= drawChild(canvas, child, drawingTime);

}

}

}else{

for(inti =0; i < count; i++) {

finalView child = children[getChildDrawingOrder(count, i)];

if((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() !=null) {

more |= drawChild(canvas, child, drawingTime);

}

}

}

这段代码用来绘制当前视图组的子视图。如果一个子视图child是可见的,即它的成员变量mViewFlags的VISIBLE位等于1,或者它有一个动画需要显示,即它的成员函数getAnimation的返回值不等于null,那么这个子视图就是需要绘制的。绘制一个子视图是通过调用ViewGroup类的成员函数drawChild来实现的。ViewGroup类的成员函数drawChild在绘制一个子视图的时候,如果这个子视图的动画还没有结束,那么它的返回值就等于true,并且会被设置到变量more中去。

注意,当变量flags的FLAG_USE_CHILD_DRAWING_ORDER位等于0的时候,就表示当前视图组的子视图按照它们在数组children中的位置从小到在三类绘制,否则的话,就需要通过ViewGroup类的成员函数getChildDrawingOrder来决定这些子视图的绘制顺序。

我们接着往下阅读代码:

[java]view plaincopy

// Draw any disappearing views that have animations

if(mDisappearingChildren !=null) {

finalArrayList disappearingChildren = mDisappearingChildren;

finalintdisappearingCount = disappearingChildren.size() -1;

// Go backwards -- we may delete as animations finish

for(inti = disappearingCount; i >=0; i--) {

finalView child = disappearingChildren.get(i);

more |= drawChild(canvas, child, drawingTime);

}

}

ViewGroup类的成员变量mDisappearingChildren用来保存那些正在消失的子视图,但是这些子视图正在显示动画的过程中,因此,这些子视图也是需要绘制的,这段代码同样是通过调用ViewGroup类的成员函数drawChild来绘制它们。

我们继续往下阅读最后一段代码:

[java]view plaincopy

if(clipToPadding) {

canvas.restoreToCount(saveCount);

}

// mGroupFlags might have been updated by drawChild()

flags = mGroupFlags;

if((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) {

invalidate();

}

if((flags & FLAG_ANIMATION_DONE) ==0&& (flags & FLAG_NOTIFY_ANIMATION_LISTENER) ==0&&

mLayoutAnimationController.isDone() && !more) {

// We want to erase the drawing cache and notify the listener after the

// next frame is drawn because one extra invalidate() is caused by

// drawChild() after the animation is over

mGroupFlags |= FLAG_NOTIFY_ANIMATION_LISTENER;

finalRunnable end =newRunnable() {

publicvoidrun() {

notifyAnimationListener();

}

};

post(end);

}

}

......

这段代码执行以下三个操作:

1. 检查变量clipToPadding的值是否等于true。如果是的话,那么就说明前面设置过参数canvas所描述的一块画布的裁剪区域。由于现在已经在这块画布上绘制完成当前视图组的UI了,因此,就需要恢复参数canvas所描述的一块画布堆栈状态。这是通过调用参数canvas所指向的一个Canvas对象的成员函数restoreToCount来实现的。

2. 前面在绘制当前视图组的子视图的UI的时候,有可能会需要修改当前视图组的标志位,即修改ViewGroup类的成员变量mGroupFlags的值。如果修改后的mGroupFlags的FLAG_INVALIDATE_REQUIRED位等于1,那么就说明当前视图组需要重新发起一个绘制UI的请求。这是通过调用ViewGroup类的另外一个成员函数invalidate来实现的。

3. 如果当前视图组的动画已经显示完成,并且当前视图组的子视图的动画也已经显示完成,再并且当前视图组注册有动画监听者,那么就是会调用ViewGroup类的另外一个成员函数notifyAnimationListener来通知这些动画监听者,当前视图组的动画已经显示结束。注意,ViewGroup类的成员函数notifyAnimationListener是以消息的形式来调用的,即ViewGroup类的成员函数dispatchDraw不是在动画一显示结束,就马上通知那些动画监听者。

接下来,我们就继续分析ViewGroup类的成员函数drawChild的实现,以便可以了解一个视图组的子视图的绘制过程。

Step 10. ViewGroup.drawChild

这个函数定义在文件frameworks/base/core/java/android/view/ViewGroup.java中,它的实现比较长,我们分段来阅读:

[java]view plaincopy

publicabstractclassViewGroupextendsViewimplementsViewParent, ViewManager {

......

protectedbooleandrawChild(Canvas canvas, View child,longdrawingTime) {

booleanmore =false;

finalintcl = child.mLeft;

finalintct = child.mTop;

finalintcr = child.mRight;

finalintcb = child.mBottom;

finalintflags = mGroupFlags;

这段代码首先获得子视图child的区域(cl, ct, cr, cb),以及当前视图组的标志位flags,以便接下来可以使用。另外,变量more的值用来表示子视图child是否还在显示动画。

我们接着往下阅读代码:

[java]view plaincopy

Transformation transformToApply =null;

finalAnimation a = child.getAnimation();

......

if(a !=null) {

......

if(mChildTransformation ==null) {

mChildTransformation =newTransformation();

}

more = a.getTransformation(drawingTime, mChildTransformation);

transformToApply = mChildTransformation;

......

}elseif((flags & FLAG_SUPPORT_STATIC_TRANSFORMATIONS) ==

FLAG_SUPPORT_STATIC_TRANSFORMATIONS) {

if(mChildTransformation ==null) {

mChildTransformation =newTransformation();

}

finalbooleanhasTransform = getChildStaticTransformation(child, mChildTransformation);

if(hasTransform) {

finalinttransformType = mChildTransformation.getTransformationType();

transformToApply = transformType != Transformation.TYPE_IDENTITY ?

mChildTransformation :null;

......

}

}

这段代码用来获得子视图child的变换矩阵transformToApply。获得了子视图child的变换矩阵transformToApply之后,我们就可以知道如何来显示它了。 在两种情况下,子视图child会被设置一个变换矩阵。第一种情况子视图child正在显示动画的过程中,第二种情况是当前视图组给每一个子视图设置了一个变换矩阵。下面我们就分别讨论这两种情况。

对于第一种情况,子视图child的成员函数getAnimation的返回值a不等于null,并且它所指向的一个Animation对象就是用来描述子视图child的动画的。获得了子视图的动画对象a之后,我们就可以调用它的成员函数getTransformation来继续执行它的动画了。如果该动画还需要继续执行,那么调用Animation对象a的成员函数getTransformation的返回值more就会等于true,并且会返回子视图child的接下来需要使用的变换矩阵,保存在ViewGroup类的成员变量mChildTransformation中。ViewGroup类的成员变量mChildTransformation最后又会保存在变量transformToApply中。

对于第二种情况,变量flags的FLAG_SUPPORT_STATIC_TRANSFORMATIONS位等于1,这时候调用ViewGroup类的成员函数getChildStaticTransformation就可以知道子视图child是否被设置了一个变换矩阵。如果设置了的话,那么ViewGroup类的成员函数getChildStaticTransformation的返回值hasTransform就会等于true。在这种情况下,ViewGroup类的成员变量mChildTransformation所描述的变换矩阵就是要应用在子视图child中的。不过有一个前提,即ViewGroup类的成员变量mChildTransformation所描述的变换矩阵不是一个单位矩阵,这是因为单位矩阵是没有变换效果的。如果ViewGroup类的成员变量mChildTransformation所描述的变换矩阵不是一个单位矩阵,那么它同样会被保存在变量transformToApply中。

我们继续往下阅读代码:

[java]view plaincopy

// Sets the flag as early as possible to allow draw() implementations

// to call invalidate() successfully when doing animations

child.mPrivateFlags |= DRAWN;

......

child.computeScroll();

finalintsx = child.mScrollX;

finalintsy = child.mScrollY;

booleanscalingRequired =false;

Bitmap cache =null;

if((flags & FLAG_CHILDREN_DRAWN_WITH_CACHE) == FLAG_CHILDREN_DRAWN_WITH_CACHE ||

(flags & FLAG_ALWAYS_DRAWN_WITH_CACHE) == FLAG_ALWAYS_DRAWN_WITH_CACHE) {

cache = child.getDrawingCache(true);

if(mAttachInfo !=null) scalingRequired = mAttachInfo.mScalingRequired;

}

finalbooleanhasNoCache = cache ==null;

这段代码执行以下三个操作:

1. 将子视图child的标志值mPrivateFlags的DRAWN设置为1,因为接下来它就会被绘制了。

2. 计算子视图child的滚动位置,这是通过调用子视图child的成员函数computeScroll来实现的。计算好子视图child的滚动位置之后,我们就可以通过它的成员变量mScrollX和mScrollY来获得它在X轴和Y轴上的偏移了。这两个偏移值保存在变量sx和sy中。

3. 检查变量flags的FLAG_CHILDREN_DRAWN_WITH_CACHE位或者FLAG_ALWAYS_DRAWN_WITH_CACHE位是否等于1。如果其中的一个等于1的话,那么就说明子视图是使用缓冲方式来绘制,即它的UI来缓冲在一个Bitmap里面,通过调用子视图child的成员函数getDrawingCache就可以获得这个Bitmap,并且保存在变量cache中。同时,通过ViewGroup类的成员变量mAttachInfo所指向的一个AttachInfo对象的成员变量mScalingRequired还可以知道子视图是否要进行缩放。如果需要缩放,那么变量scalingRequired的值就会等于true。另外,如果子视图child有一个缓冲的Bitmap,那么变量hasNoCache的值就会等于false。

我们继续往下阅读代码:

[java]view plaincopy

finalintrestoreTo = canvas.save();

if(hasNoCache) {

canvas.translate(cl - sx, ct - sy);

}else{

canvas.translate(cl, ct);

if(scalingRequired) {

// mAttachInfo cannot be null, otherwise scalingRequired == false

finalfloatscale =1.0f / mAttachInfo.mApplicationScale;

canvas.scale(scale, scale);

}

}

floatalpha =1.0f;

if(transformToApply !=null) {

......

alpha = transformToApply.getAlpha();

......

}

......

if((flags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {

if(hasNoCache) {

canvas.clipRect(sx, sy, sx + (cr - cl), sy + (cb - ct));

}else{

if(!scalingRequired) {

canvas.clipRect(0,0, cr - cl, cb - ct);

}else{

canvas.clipRect(0,0, cache.getWidth(), cache.getHeight());

}

}

}

这段代码用来设置子视图child的偏移、Alpha通道以及裁剪区域:

1. 如果子视图child不是以缓冲的方式来绘制的,那么它的偏移值就需要同时考虑它在X轴和Y轴上的起始位置(cl, ct)以及滚动值sx、sy;如果子视图child是以缓冲的方式来绘制的,那么就不需要考虑它在X轴和Y轴上的滚动位置sx和sy,而只需要考虑它在X轴和Y轴上的起始位置(cl, ct),这是因为它所缓冲的Bitmap已经包含了滚动信息。注意,在子视图child是以缓冲的方式来绘制的情况中,如果变量scalingRequired的值等于true,那么这段代码同时还需要为子视图child设置一个缩放因子,以便接下来可以同步缓冲的Bitmap的所表示的大小。

2. 如果子视图child此时被设置了一个变换矩阵transformToApply,那么一般它就会有一个Alpha值。例如,前面提到,当子视图child还处理动画显示的状态时,它就会有一个变换矩阵,而这个动画一般会有透明的效果,因此,就会有一个Alpha值。通过调用变量transformToApply所指向的一个Transformation对象的成员函数getAlpha就可以获得子视图child的Alpha值,保存在变量alpha中。如果子视图child此时没有被设置变换矩阵的话,那么它的Alpha值alpha就会等于1.0,表示不是透明的。

3. 如果变量flags的FLAG_CLIP_CHILDREN位等于1,那么就说明需要为子视图child设置一个剪裁区域。在设置子视图child的剪裁区域时,同样是需要考虑子视图child是否使用缓冲方式来绘制。如果不使用缓冲方式来会绘制,那么子视图child的剪裁区域就需要同时考虑它在X轴和Y轴上的起始位置(cl, ct)以及滚动值sx、sy;如果不是使用缓冲方式来会绘制,那么就不需要考虑它在X轴和Y轴上的滚动位置sx和sy。注意,在子视图child是以缓冲的方式来绘制的情况中,如果变量scalingRequired的值等于false,那么它的剪裁区域的宽度和高度就分别为(cr - cl)和(cb - ct),否则的话,它的剪裁区域的宽度和高度就等于上一次的缓冲Bitmap的宽度和宽度,这是因为对于后者来说,前面在设置子视图child的偏移时,已经同时设置过它的缩放因子了。

注意,在子视图child的偏移、Alpha通道以及裁剪区域之前,这段代码首先会保存在参数canvas所描述的一块画布的堆栈状态,以便在绘制完成子视图child的UI之后,可以恢复这块画布的堆栈状态来绘制其它子视图的UI。保存在参数canvas所描述的一块画布的堆栈状态是通过调用参数canvas所指向的一个Canvas对象的成员函数save来实现的。

我们继续往下阅读代码:

[java]view plaincopy

if(hasNoCache) {

// Fast path for layouts with no backgrounds

if((child.mPrivateFlags & SKIP_DRAW) == SKIP_DRAW) {

......

child.dispatchDraw(canvas);

}else{

child.draw(canvas);

}

}else{

finalPaint cachePaint = mCachePaint;

if(alpha <1.0f) {

cachePaint.setAlpha((int) (alpha *255));

mGroupFlags |= FLAG_ALPHA_LOWER_THAN_ONE;

}elseif((flags & FLAG_ALPHA_LOWER_THAN_ONE) == FLAG_ALPHA_LOWER_THAN_ONE) {

cachePaint.setAlpha(255);

mGroupFlags &= ~FLAG_ALPHA_LOWER_THAN_ONE;

}

......

canvas.drawBitmap(cache,0.0f,0.0f, cachePaint);

}

这段代码就是用来绘制子视图child的UI的了,分为两种情况来考虑:

1. 以非缓冲的方式来绘制。这时候需要检查子视图child的标志值mPrivateFlags的SKIP_DRAW位是否等于1。如果等于1的话,那么就说明需要跳过子视图child的绘制,但是需要绘制子视图child的子视图,否则的话,就需要先绘制子视图child的UI,再绘制它的子视图的UI。绘制子视图child的子视图是通过调用它的成员函数dispatchDraw来实现的,而绘制子视图child本身及其子视图是通过调用它的成员函数draw来实现的。

2. 以缓冲的方式来绘制。这时候只需要将上一次的缓冲的Bitmap对象cache绘制到参数canvas所描述的一块画布上面就行了。在绘制之前,需要先设置用来绘制Bitmap对象cache的一个Paint对象的Alpha值,这个Paint对象保存在ViewGroup类的成员变量mCachePaint中。这个Alpha值保存在变量alpha中,如果它的值小于1.0,那么就说明子视图child有一个透明值,因此,就需要将它设置到ViewGroup类的成员变量mCachePaint所指向的一个Paint对象中去,并且将ViewGroup类的成员变量mGroupFlags的FLAG_ALPHA_LOWER_THAN_ONE位设置为1。另一方面,如果变量alpha的值大于等于1.0,那么就说明不需要设置子视图child的透明值,但是如果之前设置过子视图child的透明值,即ViewGroup类的成员变量mGroupFlags的FLAG_ALPHA_LOWER_THAN_ONE位等于1,那么还需要修改ViewGroup类的成员变量mCachePaint所指向的一个Paint对象是不透明的,即将它的透明值设置为255,并且将ViewGroup类的成员变量mGroupFlags的FLAG_ALPHA_LOWER_THAN_ONE位重置为0。最后,就可以调用参数canvas所指向的一个Canvas对象的成员函数drawBitmap来绘制子视图child的UI了。

我们继续往下阅读最后一段代码:

[java]view plaincopy

canvas.restoreToCount(restoreTo);

......

returnmore;

}

......

绘制完成子视图child的UI之后,就可以恢复参数canvas所描述的一块画布的堆栈状态了,这是通过调用参数canvas所指向的一个Canvas对象的成员函数restoreToCount来实现的。

ViewGroup类的成员函数drawChild最后就将变量more的值返回给调用者了,以便调用者可以知道当前正在绘制的子视图child是否还处于动画状态中。

从上面的分析就可以知道,当子视图child不是以非缓冲模式来绘制,并且它需要绘制自己及其子视图时,它的成员函数draw就会被调用,这时候就会重复执行Step 8、Step 9和Step 10,直到所有子孙视图都绘制完成为止。

这一步执行完成之后,应用程序窗口的UI就全部绘制到在前面的Step 2中所获得的一块画布上面去了,返回到前面的Step 1中,即ViewRoot类的成员函数draw中,接下来就会Java层的Surface类的成员函数unlockCanvasAndPost来请求SurfaceFlinger服务渲染这块画布里面所包含的一个图形缓冲区了。

接下来,我们就继续分析Java层的Surface类的成员函数unlockCanvasAndPost的实现。

Step 11. Surface.unlockCanvasAndPost

[java]view plaincopy

publicclassSurfaceimplementsParcelable {

......

/** unlock the surface and asks a page flip */

publicnativevoidunlockCanvasAndPost(Canvas canvas);

......

}

这个函数定义在文件frameworks/base/core/java/android/view/Surface.java中。

Surface类的成员函数unlockCanvasAndPost是一个JNI方法,它是由C++层的函数Surface_unlockCanvasAndPost来实现的,如下所示:

[cpp]view plaincopy

staticvoidSurface_unlockCanvasAndPost(

JNIEnv* env, jobject clazz, jobject argCanvas)

{

jobject canvas = env->GetObjectField(clazz, so.canvas);

if(canvas != argCanvas) {

doThrow(env,"java/lang/IllegalArgumentException", NULL);

return;

}

constsp& surface(getSurface(env, clazz));

if(!Surface::isValid(surface))

return;

// detach the canvas from the surface

SkCanvas* nativeCanvas = (SkCanvas*)env->GetIntField(canvas, no.native_canvas);

intsaveCount = env->GetIntField(clazz, so.saveCount);

nativeCanvas->restoreToCount(saveCount);

nativeCanvas->setBitmapDevice(SkBitmap());

env->SetIntField(clazz, so.saveCount, 0);

// unlock surface

status_t err = surface->unlockAndPost();

if(err < 0) {

doThrow(env,"java/lang/IllegalArgumentException", NULL);

}

}

这个函数定义在文件frameworks/base/core/jni/android_view_Surface.cpp中。

参数clazz指向的是一个Java层的Surface对象,用来描述当前正在绘制的应用程序窗口的绘图表面,而参数argCanvas指向的是一个Java层的Canvas对象。

参数clazz指向的是一个Java层的Surface对象的成员变量mCanvas所指向的一个Canvas对象canvas应当等于参数argCanvas指向的是一个Java层的Canvas对象,否则的话,函数就会抛出一个异常。

从前面Android应用程序窗口(Activity)的绘图表面(Surface)的创建过程分析一文可以知道,每一个Java层的Surface对象在C++层都有一个对应的Surface对象。因此,函数t就可以通过调用另外一个函数getSurface来获得与参数clazz所指向的是一个Java层的Surface对象所对应的C++层的Surface对象surface。C++层的Surface类的静态成员函数isValid用来验证Surface对象surface是否已经连接到SurfaceFlinger服务中。如果还没有连接到,那么函数就会出错返回了。

通过了上述的合法性检查之后,函数接下来主要就是做两件事情:

1. 恢复变量canvas所描述的一块画布的堆栈状态。变量canvas所描述的一块画布是在前面的Step 2中开始初始化的,每次应用程序窗口在上面绘制完成UI之后,我们都应该恢复它的堆栈状态,以便下一次使用时不会受上一次影响。变量canvas指向的是一个Java层的Canvas对象,函数首先找到与它所对应的一个C++层的SkCanvas对象nativeCanvas,然后就可以调用这个SkCanvas对象nativeCanvas的成员函数restoreToCount来恢复它所描述的一块画布的堆栈状态了。这块画布在绘制应用程序窗口UI前的堆栈状态保存在参数clazz所指向的一个Java层的Surface对象的成员变量mSaveCount中。因此,函数就先获得参数clazz所指向的一个Java层的Surface对象的成员变量mSaveCount的值,然后再以它为参数来调用SkCanvas对象nativeCanvas的成员函数restoreToCount,这样就可以恢复画布的堆栈状态了。

2. 请求SurfaceFlinger服务渲染Surface对象surface所描述的应用程序窗口的绘图表面。应用程序窗口的UI是绘制在SkCanvas对象nativeCanvas所描述的一块画布上的,而这块画布所使用的图形缓冲区是保存在Surface对象surface的内部的,因此,函数就调用Surface对象surface的成员函数unlockAndPost来请求SurfaceFlinger服务渲染这块图形缓冲区。

接下来,我们就继续分析C++层的Surface类的成员函数unlockAndPost的实现,以便可以了解用来绘制应用程序窗口UI的图形缓冲区是如何渲染的。

Step 12. Surface.unlockAndPost

[cpp]view plaincopy

status_t Surface::unlockAndPost()

{

if(mLockedBuffer == 0) {

LOGE("Surface::unlockAndPost failed, no locked buffer");

returnINVALID_OPERATION;

}

status_t err = mLockedBuffer->unlock();

LOGE_IF(err,"failed unlocking buffer (%p)", mLockedBuffer->handle);

err = queueBuffer(mLockedBuffer.get());

LOGE_IF(err,"queueBuffer (idx=%d) failed (%s)",

getBufferIndex(mLockedBuffer), strerror(-err));

mPostedBuffer = mLockedBuffer;

mLockedBuffer = 0;

returnerr;

}

这个函数定义在文件frameworks/base/libs/surfaceflinger_client/Surface.cpp中。

从前面的Step 3可以知道,应用程序窗口当前正在使用的图形缓冲区保存在Surface类的成员变量mLockedBuffer中,因此,Surface类的成员函数unlockAndPost的目标就是要将它交给SurfaceFlinger服务来渲染,这是通过调用另外一个成员函数queueBuffer来实现的。在前面Android应用程序请求SurfaceFlinger服务渲染Surface的过程分析一文中,我们已经分析过Surface类的成员函数queueBuffer的实现了,它主要就是向应用程序窗口的待渲染图形缓冲区队列中添加一个图形缓冲区,然后再请请求SurfaceFlinger服务来渲染这个图形缓冲区。

在渲染成员变量mLockedBuffer所描述的一个图形缓冲区之前,Surface类的成员函数unlockAndPost还会调用它的成员函数unlock来执行一个“解锁”操作。从前面的Step 3可以知道,成员变量mLockedBuffer所描述的一个图形缓冲区在交给应用程序窗口使用之前,它会被执行一个“锁定”的操作,即它的成员函数lock会被调用,因此,这里执行的“解锁”操作是与前面的“锁定”操作相对应的。事实上,对成员变量mLockedBuffer所描述的一个图形缓冲区进行锁定,主要是为了获得这个图形缓冲区的地址,是否真的要对个图形缓冲区进行锁定,是由HAL层模块Gralloc的实现来决定的。

在请求SurfaceFlinger服务渲染了成员变量mLockedBuffer所描述的一个图形缓冲区之后,Surface类的成员函数unlockAndPost还会把成员变量mLockedBuffer所描述的一个图形缓冲区保存在另外一个成员变量mPostedBuffer中,表示这个图形缓冲区已经变成是正在渲染的图形缓冲区了,或者说是前端缓冲区了。

最后,Surface类的成员函数unlockAndPost就把成员变量mLockedBuffer的值设置为0,这样就可以将应用程序窗口下一次请求分配和使用的图形缓冲区保存在它里面。

Surface类的成员变量mLockedBuffer指向的是一个GraphicBuffer对象,接下来我们就继续分析它的成员函数unlock的实现,以便可以了解它所描述的图形缓冲区的“解锁”过程。

Step 13. GraphicBuffer.unlock

[cpp]view plaincopy

status_t GraphicBuffer::unlock()

{

status_t res = getBufferMapper().unlock(handle);

returnres;

}

这个函数定义在文件frameworks/base/libs/ui/GraphicBuffer.cpp中。

从前面的Step 4可以知道,GraphicBuffer类的成员变量handle用来描述当前正在处理的图形缓冲区的句柄,而GraphicBuffer类的成员函数getBufferMapper返回的是一个GraphicBufferMapper对象。有了这个GraphicBufferMapper对象之后,就可以调用它的成员函数unlock解锁成员变量handle所描述的一个图形缓冲区了。

接下来,我们就继续分析GraphicBufferMapper类的成员函数unlock的实现。

Step 14. GraphicBufferMapper.unlock

[cpp]view plaincopy

status_t GraphicBufferMapper::unlock(buffer_handle_t handle)

{

status_t err;

if(sw_gralloc_handle_t::validate(handle) < 0) {

err = mAllocMod->unlock(mAllocMod, handle);

}else{

err = sw_gralloc_handle_t::unlock((sw_gralloc_handle_t*)handle);

}

LOGW_IF(err,"unlock(...) failed %d (%s)", err, strerror(-err));

returnerr;

}

这个函数定义在文件frameworks/base/libs/ui/GraphicBufferMapper.cpp。

从前面的Step 5可以知道,参数handle所描述的图形缓冲区是在HAL模块Gralloc中分配的,这个HAL模块Gralloc是由GraphicBufferMapper类的成员变量mAllocMod来描述的,因此,函数就终就会调用GraphicBufferMapper类的成员变量mAllocMod所描述的一个HAL模块Gralloc的成员函数unlock来解锁参数andle所描述的一个图形缓冲区。HAL模块Gralloc的成员函数unlock的实现可以参考前面Android帧缓冲区(Frame Buffer)硬件抽象层(HAL)模块Gralloc的实现原理分析一文,这里不再详述。

至此,我们就分析完成Android应用程序窗口的渲染过程了,从中就可以看出:

1. 渲染Android应用程序窗口UI需要经过三步曲:测量、布局、绘制。

2. Android应用程序窗口UI首先是使用Skia图形库API来绘制在一块画布上,实际地是绘制在这块画布里面的一个图形缓冲区中,这个图形缓冲区最终会被交给SurfaceFlinger服务,而SurfaceFlinger服务再使用OpenGL图形库API来将这个图形缓冲区渲染到硬件帧缓冲区中。

Android应用程序窗口的渲染过程分析完成之后,Android应用程序窗口的实现框架就分析完成了,重新学习请回到Android应用程序窗口(Activity)实现框架简要介绍和学习计划一文中。

Android应用程序窗口(Activity)实现框架简要介绍和学习计划这一系列文章中,我们主要是从单个应用程序窗口的角度来分析的。但是,Android系统在运行的过程中,需要管理的是一系列的应用程序窗口,并且这些应用程序窗口的类型可能各不相同,并且相互影响。因此,Android的窗口管理系统是非常复杂的。在接下来的一个系列的文章中,我们就将详细地分析Android窗口管理服务WindowManagerService的实现,以便可以从系统的角度来分析应用程序窗口的实现。敬请关注!

本文是我转自老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!(文章仅仅为了让更多的android爱好者学习使用)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容