框架分析
在之前的下拉刷新中,小结过触屏消息先到WindowManagerService(Wms)然后顺次传递给ViewRoot(派生自Handler),经decor view到Activity再传递给指定的View,这次整理View的绘制流程,通过源码可知,这个过程应该没有涉及到IPC(或者我没有发现),需要绘制时在UI线程中通过ViewRoot发送一个异步请求消息,然后ViewRoot自己接收并不处理这个消息。
在正式进入View绘制之前,首先需要明确一下Android UI的架构组成,偷图如下:

上述架构很清晰的呈现了Activity、Window、DecorView(及其组成)、ViewRoot和WMS之间的关系,我通过源码简单理了下从启动Activity到创建View的过程,大致如下

在上图中,performLaunchActivity函数是关键函数,除了新建被调用的Activity实例外,还负责确保Activity所在的应用程序启动、读取manifest中关于此activity设置的主题信息以及上图中对“6.onCreate”调用也是通过对mInstrumentation.callActivityOnCreate来实现的。图中的“8. mContentParent.addView”其实就是架构图中phoneWindow内DecorView里面的ContentViews,该对象是一个ViewGroup类实例。在调用AddView之后,最终就会触发ViewRoot中的scheduleTraversals这个异步函数,从而进入ViewRoot的performTraversals函数,在performTraversals函数中就启动了View的绘制流程。
performTraversals函数在2.3.5版本源码中就有近六百行的代码,跟我们绘制view相关的可以抽象成如下的简单流程图
流程图中的host其实就是mView,而ViewRoot中的这个mView其实就是DecorView,之所以这么说,又得具体看源码中ActivityThread的handleResumeActivity函数,在这里我就不展开了。上述流程主要调用了View的measure、layout和draw三个函数。
performTraversals(),该函数就是android系统View树遍历工作的核心。一眼看去,发现这个函数挺长的,但是逻辑是非常清晰的,其执行过程可简单概括为根据之前所有设置好的状态,判断是否需要计算视图大小(measure)、是否需要重新安置视图的位置(layout),以及是否需要重绘(draw)视图,可以用以下图来表示该流程。

- private void performTraversals() {
-
-
- final View host = mView;
- final View.AttachInfo attachInfo = mAttachInfo;
- final int viewVisibility = getHostVisibility();
- boolean viewVisibilityChanged = mViewVisibility != viewVisibility
- || mNewSurfaceNeeded;
- float appScale = mAttachInfo.mApplicationScale;
- WindowManager.LayoutParams params = null;
- if (mWindowAttributesChanged) {
- mWindowAttributesChanged = false;
- surfaceChanged = true;
- params = lp;
- }
- Rect frame = mWinFrame;
- if (mFirst) {
-
-
-
-
-
- attachInfo.mSurface = mSurface;
- attachInfo.mUse32BitDrawingCache = PixelFormat.formatHasAlpha(lp.format) ||
- lp.format == PixelFormat.RGBX_8888;
- attachInfo.mHasWindowFocus = false;
- attachInfo.mWindowVisibility = viewVisibility;
- ......
- }
-
- if (mLayoutRequested) {
-
-
- getRunQueue().executeActions(attachInfo.mHandler);
-
- if (mFirst) {
- ......
- }
- }
-
- if (attachInfo.mRecomputeGlobalAttributes) {
- ......
- }
-
- if (mFirst || attachInfo.mViewVisibilityChanged) {
- ......
- }
-
-
-
-
- host.measure(childWidthMeasureSpec, childHeightMeasureSpec);
- mLayoutRequested = true;
- }
- }
-
- final boolean didLayout = mLayoutRequested;
- boolean triggerGlobalLayoutListener = didLayout
- || attachInfo.mRecomputeGlobalAttributes;
- if (didLayout) {
- host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);
-
- }
-
-
- if (mFirst) {
- mRealFocusedView = mView.findFocus();
- }
-
- boolean cancelDraw = attachInfo.mTreeObserver.dispatchOnPreDraw();
-
- if (!cancelDraw && !newSurface) {
- mFullRedrawNeeded = false;
- draw(fullRedrawNeeded);
- }
measure过程分析
因为DecorView实际上是派生自FrameLayout的类,也即一个ViewGroup实例,该ViewGroup内部的ContentViews又是一个ViewGroup实例,依次内嵌View或ViewGroup形成一个View树。所以measure函数的作用是为整个View树计算实际的大小,设置每个View对象的布局大小(“窗口”大小)。实际对应属性就是View中的mMeasuredHeight(高)和mMeasureWidth(宽)。
在View类中measure过程主要涉及三个函数,函数原型分别为
public final void measure(int widthMeasureSpec, int heightMeasureSpec)
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight)
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
前面两个函数都是final类型的,不能重载,为此在ViewGroup派生的非抽象类中我们必须重载onMeasure函数,实现measure的原理是:假如View还有子View,则measure子View,直到所有的子View完成measure操作之后,再measure自己。ViewGroup中提供的measureChild或measureChildWithMargins就是实现这个功能的。
在具体介绍测量原理之前还是先了解些基础知识,即measure函数的参数由类measureSpec的makeMeasureSpec函数方法生成的一个32位整数,该整数的高两位表示模式(Mode),低30位则是具体的尺寸大小(specSize)。
MeasureSpec有三种模式分别是UNSPECIFIED, EXACTLY和AT_MOST,各表示的意义如下
如果是AT_MOST,specSize代表的是最大可获得的尺寸;
如果是EXACTLY,specSize代表的是精确的尺寸;
如果是UNSPECIFIED,对于控件尺寸来说,没有任何参考意义。
那么对于一个View的上述Mode和specSize值默认是怎么获取的呢,他们是根据View的LayoutParams参数来获取的:
参数为fill_parent/match_parent时,Mode为EXACTLY,specSize为剩余的所有空间;
参数为具体的数值,比如像素值(px或dp),Mode为EXACTLY,specSize为传入的值;
参数为wrap_content,Mode为AT_MOST,specSize运行时决定。
具体测量原理
上面提供的Mode和specSize只是程序员对View的一个期望尺寸,最终一个View对象能从父视图得到多大的允许尺寸则由子视图期望尺寸和父视图能力尺寸(可提供的尺寸)两方面决定。关于期望尺寸的设定,可以通过在布局资源文件中定义的android:layout_width和android:layout_height来设定,也可以通过代码在addView函数调用时传入的LayoutParams参数来设定。父View的能力尺寸归根到最后就是DecorView尺寸,这个尺寸是全屏,由手机的分辨率决定。期望尺寸、能力尺寸和最终允许尺寸的关系,我们可以通过阅读measureChild或measureChildWithMargins都会调用的getChildMeasureSpec函数的源码来获得,下面简单列表说明下三者的关系
父视图能力尺寸 |
子视图期望尺寸 |
子视图最终允许尺寸 |
EXACTLY + Size1 |
EXACTLY + Size2 |
EXACTLY + Size2 |
EXACTLY + Size1 |
fill_parent/match_parent |
EXACTLY+Size1 |
EXACTLY + Size1 |
wrap_content |
AT_MOST+Size1 |
AT_MOST+Size1 |
EXACTLY + Size2 |
EXACTLY+Size2 |
AT_MOST+Size1 |
fill_parent/match_parent |
AT_MOST+Size1 |
AT_MOST+Size1 |
wrap_content |
AT_MOST+Size1 |
UNSPECIFIED+Size1 |
EXACTLY + Size2 |
EXACTLY + Size2 |
UNSPECIFIED+Size1 |
fill_parent/match_parent |
UNSPECIFIED+0 |
UNSPECIFIED+Size1 |
wrap_content |
UNSPECIFIED+0 |
上述表格展现的是子视图最终允许得到的尺寸,显然1、4、7三项没有对Size1和Size2进行比较,所以允许尺寸是可以大于父视图的能力尺寸的,这个时候最终的视图尺寸该是多少呢?AT_MOST和UNSPECIFIED的View又该如何决策最终的尺寸呢?
通过Demo演示的得到的结果,假如Size2比Size1的尺寸大,假如不使用滚动效果的话,子视图超出部分将被裁剪掉,该父视图中如果在该子视图后面还有其他视图,那么也将被裁剪掉,但是通过调用其getVisibility还是显示该控件是可见的,所以裁剪后控件依然是有的,只是用户没办法观察到;在使用滚动效果的情况下,就能将原本被裁剪掉的控件通过滚动显示出来。
对于第二个问题,根据源码View的OnMeasure函数调用的getDefaultSize函数获知,默认情况下,控件都有一个最小尺寸,该值可以通过设置android:minHeight和android:minWidth来设置(无设置时缺省为0);在设置了背景的情况下,背景drawable的最小尺寸与前面设置的最小尺寸比较,两者取大者,作为控件的最小尺寸。在UNSPECIFIED情况下就选用这个最小尺寸,其它情况则根据允许尺寸来。不过这个是默认规则,通过demo发现,TextView在AT_MOST+Size情况下,并不是以Size作为控件的最终尺寸,结果发现在TextView的源码中,重载了onMeasure函数,有价值的代码如下:
……
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
……
if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(widthSize, width);
}
……
if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(desired, heightSize);
}
……
至于其中的width和desired值,感兴趣的同学可以具体关注下。虽然FrameWork提供了视图默认的尺寸计算规则,但是最终的视图布局大小可以重载onMeasure函数来修改计算规则,当然也可以不计算直接通过setMeasuredDimension来设置(需要注意的是,如果通过setMeasuredDimension的同时还要调用父类的onMeasure函数,那么在调用父类函数之前调用的setMeasuredDimension会无效果)。
整个view视图的Measure过程就是一个量体裁衣,按需分配的过程。看一下以下的递归过程:

从上图可以看出,measure过程始于ViewRoot的host.measure(),调的就是view类的measure()函数,该函数然后回调onMeasure。如果host对象是一个ViewGroup实例,一般会重载onMeasure,如果没有的话,则会执行view类中默认的onMeasure。合理的情况是编程人员重载onMeasure并逐一对里面的子view进行measure。我们可以看一下view的measure方法:
-
-
-
-
-
- public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
- if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT ||
- widthMeasureSpec != mOldWidthMeasureSpec ||
- heightMeasureSpec != mOldHeightMeasureSpec) {
-
-
- mPrivateFlags &= ~MEASURED_DIMENSION_SET;
-
- onMeasure(widthMeasureSpec, heightMeasureSpec);
-
- if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) {
- throw new IllegalStateException("onMeasure() did not set the"
- + " measured dimension by calling"
- + " setMeasuredDimension()");
- }
- mPrivateFlags |= LAYOUT_REQUIRED;
- }
- mOldWidthMeasureSpec = widthMeasureSpec;
- mOldHeightMeasureSpec = heightMeasureSpec;
- }
这里强烈建议去看看viewGroup实例FrameLayout和LinearLayout的onMeasure方法,一定会有所感悟的,尤其是LinerLayout的。这样对于viewGroup的专用标签pading和weight也会有新的体会。
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="vertical"
- android:layout_width="fill_parent"
- android:layout_height="100dip"
- >
- <TextView
- android:layout_width="fill_parent"
- android:layout_height="20dip"
- android:layout_weight="2"
- android:text="@string/hello"
- />
- <ListView
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:background="#ff00ff00"
- ></ListView>
- </LinearLayout>
-
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="vertical"
- android:layout_width="fill_parent"
- android:layout_height="100dip"
- >
- <TextView
- android:layout_width="fill_parent"
- android:layout_height="60dip"
- android:layout_weight="2"
- android:text="@string/hello"
- />
- <ListView
- android:layout_width="fill_parent"
- android:layout_height="0dip"
- android:layout_weight="2"
- android:background="#ff00ff00"
- ></ListView>
- </LinearLayout>
请问以上两布局有无不同,能否自行画出?
执行完measure过程,也就是说各个view的大小尺寸已经登记在案,现在它们要确定的是自己应该置身于何处,也就是摆放在哪里。好吧,这个就是layout的职责所在,让父视图按照子视图的大小及布局参数,将子视图放置在合适的位置上。
同样需要看下以下流程图:

- public void layout(int l, int t, int r, int b) {
- int oldL = mLeft;
- int oldT = mTop;
- int oldB = mBottom;
- int oldR = mRight;
-
- boolean changed = setFrame(l, t, r, b);
- if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
- if (ViewDebug.TRACE_HIERARCHY) {
- ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
- }
-
- onLayout(changed, l, t, r, b);
- mPrivateFlags &= ~LAYOUT_REQUIRED;
-
- if (mOnLayoutChangeListeners != null) {
- ArrayList<OnLayoutChangeListener> listenersCopy =
- (ArrayList<OnLayoutChangeListener>) mOnLayoutChangeListeners.clone();
- int numListeners = listenersCopy.size();
- for (int i = 0; i < numListeners; ++i) {
- listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
- }
- }
- }
-
- mPrivateFlags &= ~FORCE_LAYOUT;
- }
view中的该layout函数流程大概如下:
1,调用setFrame()将位置参数保存,这些参数会保存到view内部变量(mLeft,mTop,mRight,mButtom)。如果有数值的变化那么会调用invalidate()重绘那些区域。
2,回调onLayout(),View中定义的onLayout()函数默认什么都不做,View系统提供onLayout函数的目的是为了使系统包含有子视图的父视图能够在onLayout()函数对子视图进行位置分配,正因为这样,如果是viewGroup类型,就必须重载onLayout(),由此ViewGroup的onLayout为abstract也就很好解释了。
3,清楚mPrivateFlags中的LAYOUT_REQUIRED标记,因为layout的操作已经完成了。
为了对layout过程有更深的体会,有必要针对特定的一种viewGroup进行分析,我们还是把魔爪伸向linearLayout,看看它的onMeasure,onMeasure中会根据mOrientation变量,进行不同的layout,我们看一种就行了:
- void layoutVertical() {
- final int paddingLeft = mPaddingLeft;
- int childTop;
- int childLeft;
-
- final int width = mRight - mLeft;
- int childRight = width - mPaddingRight;
-
- int childSpace = width - paddingLeft - mPaddingRight;
-
- final int count = getVirtualChildCount();
-
- final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
- final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
- switch (majorGravity) {
- case Gravity.BOTTOM:
-
- childTop = mPaddingTop + mBottom - mTop - mTotalLength;
- break;
-
- case Gravity.CENTER_VERTICAL:
- childTop = mPaddingTop + (mBottom - mTop - mTotalLength) / 2;
- break;
- case Gravity.TOP:
- default:
- childTop = mPaddingTop;
- break;
- }
-
- for (int i = 0; i < count; i++) {
- final View child = getVirtualChildAt(i);
- if (child == null) {
- childTop += measureNullChild(i);
- ......
- childTop += lp.topMargin;
- setChildFrame(child, childLeft, childTop + getLocationOffset(child),
- childWidth, childHeight);
- childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
- i += getChildrenSkipCount(child, i);
- }
- }
- }
这是见证奇迹的时刻,draw过程就是要把view对象绘制到屏幕上,如果它是一个viewGroup,则需要递归绘制它所有的子视图。视图中有哪些元素是需要绘制的呢?
1,view背景。所有view都会有一个背景,可以是一个颜色值,也可以是一张背景图片
2,视图本身内容。比如TextView的文字
3,渐变边框。就是一个shader对象,让视图看起来更具有层次感
4,滚动条。
按照惯例,还是先看一下绘制的总体流程吧:

眼尖的同学应该发现了,上面这张图比前面的measure和layout多了一步:draw()。在performTraversals()函数中调用的是viewRoot的draw()函数,在该函数中进行一系列的前端处理后,再调用host.draw()。
一般情况下,View对象不应该重载draw()函数,因此,host.draw()就是view.draw()。该函数内部过程也就是View系统绘制过程的核心过程,该函数中会依次绘制前面所说哦四种元素,其中绘制视图本身的具体实现就是回调onDraw()函数,应用程序一般也会重载onDraw()函数以绘制所设计的View的真正界面内容。
Google源码的注释太nice了,我加任何说辞都显得多余,就不画蛇添足了:
- public void draw(Canvas canvas) {
- if (ViewDebug.TRACE_HIERARCHY) {
- ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
- }
-
- final int privateFlags = mPrivateFlags;
- final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&
- (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
- mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- int saveCount;
-
- if (!dirtyOpaque) {
- final Drawable background = mBGDrawable;
- if (background != null) {
- final int scrollX = mScrollX;
- final int scrollY = 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);
- }
- }
- }
-
-
- final int viewFlags = mViewFlags;
- boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
- boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
- if (!verticalEdges && !horizontalEdges) {
-
- if (!dirtyOpaque) onDraw(canvas);
-
-
- dispatchDraw(canvas);
-
-
- onDrawScrollBars(canvas);
-
-
- return;
- }
-
-
-
-
-
-
-
-
- boolean drawTop = false;
- boolean drawBottom = false;
- boolean drawLeft = false;
- boolean drawRight = false;
-
- float topFadeStrength = 0.0f;
- float bottomFadeStrength = 0.0f;
- float leftFadeStrength = 0.0f;
- float rightFadeStrength = 0.0f;
-
-
- int paddingLeft = mPaddingLeft;
-
- final boolean offsetRequired = isPaddingOffsetRequired();
- if (offsetRequired) {
- paddingLeft += getLeftPaddingOffset();
- }
-
- int left = mScrollX + paddingLeft;
- int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
- int top = mScrollY + getFadeTop(offsetRequired);
- int bottom = top + getFadeHeight(offsetRequired);
-
- if (offsetRequired) {
- right += getRightPaddingOffset();
- bottom += getBottomPaddingOffset();
- }
-
- final ScrollabilityCache scrollabilityCache = mScrollCache;
- final float fadeHeight = scrollabilityCache.fadingEdgeLength;
- int length = (int) fadeHeight;
-
-
-
- if (verticalEdges && (top + length > bottom - length)) {
- length = (bottom - top) / 2;
- }
-
-
- if (horizontalEdges && (left + length > right - length)) {
- length = (right - left) / 2;
- }
-
- if (verticalEdges) {
- topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
- drawTop = topFadeStrength * fadeHeight > 1.0f;
- bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
- drawBottom = bottomFadeStrength * fadeHeight > 1.0f;
- }
-
- saveCount = canvas.getSaveCount();
-
- int solidColor = getSolidColor();
- if (solidColor == 0) {
- final int flags = 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);
- }
- } else {
- scrollabilityCache.setFadeColor(solidColor);
- }
-
-
- if (!dirtyOpaque) onDraw(canvas);
-
-
- dispatchDraw(canvas);
-
-
- final Paint p = scrollabilityCache.paint;
- final Matrix matrix = scrollabilityCache.matrix;
- final Shader fade = scrollabilityCache.shader;
-
- if (drawTop) {
- matrix.setScale(1, fadeHeight * topFadeStrength);
- matrix.postTranslate(left, top);
- fade.setLocalMatrix(matrix);
- canvas.drawRect(left, top, right, top + length, p);
- }
- 。。。。。
- canvas.restoreToCount(saveCount);
-
- onDrawScrollBars(canvas);
- }
绘制完界面内容后,如果该视图还包含子视图,则调用dispatchDraw()函数,实际上起作用的还是viewGroup的dispatchDraw()函数。需要说明的是应用程序不应该再重载ViewGroup中该方法,因为它已经有了默认而且标准的view系统流程。dispatchDraw()内部for循环调用drawChild()分别绘制每一个子视图,而drawChild()内部又会调用draw()函数完成子视图的内部绘制工作。
- protected void dispatchDraw(Canvas canvas) {
- final int count = mChildrenCount;
- final View[] children = mChildren;
- int flags = mGroupFlags;
-
- if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
- final boolean cache = (mGroupFlags & FLAG_ANIMATION_CACHE) == FLAG_ANIMATION_CACHE;
- final boolean buildCache = !isHardwareAccelerated();
- for (int i = 0; i < count; i++) {
- final View child = children[i];
- if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
- final LayoutParams params = child.getLayoutParams();
- attachLayoutAnimationParameters(child, params, i, count);
- bindLayoutAnimation(child);
- }
-
- }
-
- int saveCount = 0;
- final boolean clipToPadding = (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);
-
- }
-
-
- mPrivateFlags &= ~DRAW_ANIMATION;
- mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;
-
- boolean more = false;
- final long drawingTime = getDrawingTime();
-
- if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) {
- for (int i = 0; i < count; i++) {
- final View child = children[i];
- if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
- more |= drawChild(canvas, child, drawingTime);
- }
- }
- } else {
- for (int i = 0; i < count; i++) {
- final View child = children[getChildDrawingOrder(count, i)];
- if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
- more |= drawChild(canvas, child, drawingTime);
- }
- }
- }
-
-
- if (mDisappearingChildren != null) {
- final ArrayList<View> disappearingChildren = mDisappearingChildren;
- final int disappearingCount = disappearingChildren.size() - 1;
-
- for (int i = disappearingCount; i >= 0; i--) {
- final View child = disappearingChildren.get(i);
- more |= drawChild(canvas, child, drawingTime);
- }
- }
-
- if (clipToPadding) {
- canvas.restoreToCount(saveCount);
- }
-
-
- flags = mGroupFlags;
-
- if ((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) {
- invalidate(true);
- }
- }