我们在自定义View时,通常使用invalidate方法来刷新View,本篇将对invalidate的实现进行分析。invalidate有多个重载方法, 但其最终的实现都是类似的,这里我们从invalidate()开始分析。
frameworks/base/core/java/android/view/View.java
/**
* Invalidate the whole view. If the view is visible,
* {@link #onDraw(android.graphics.Canvas)} will be called at some point in
* the future. This must be called from a UI thread. To call from a non-UI thread,
* call {@link #postInvalidate()}.
*/
//使整个view无效,将会导致view的onDraw调用 即重绘view的过程
public void invalidate() {
invalidate(true);//true表示刷新缓存也应该失效
}
//true表示刷新缓存也应该失效,如果置位false表示view的内容或者大小没有发生变化
void invalidate(boolean invalidateCache) {
if (skipInvalidate()) {//是否应该跳出本次过程,跳过绘制的条件需满足:view不可见 且没有当前动画在执行
return;
}
/*
* 可以进行刷新的条件:
* 1: 需要绘制且已经设置过边界
* 2:当前绘制缓存还是有效,但要使其失效
* 3:view将处于invalided状态
* 4: 透明度发生了变化
* */
if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS) ||
(invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID) ||
(mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED || isOpaque() != mLastIsOpaque) {
mLastIsOpaque = isOpaque();
mPrivateFlags &= ~PFLAG_DRAWN;//重置绘制标记
mPrivateFlags |= PFLAG_DIRTY;//标记PFLAG_DIRTY 表示view当前已经失效
if (invalidateCache) {//需要失效缓存
mPrivateFlags |= PFLAG_INVALIDATED;//标记失效
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;//清除缓存有效的标记
}
final AttachInfo ai = mAttachInfo;//取到view的attch信息
final ViewParent p = mParent;//父view
//noinspection PointlessBooleanExpression,ConstantConditions
if (!HardwareRenderer.RENDER_DIRTY_REGIONS) {
if (p != null && ai != null && ai.mHardwareAccelerated) {//开启了硬件加速
// fast-track for GL-enabled applications; just invalidate the whole hierarchy
// with a null dirty rect, which tells the ViewAncestor to redraw everything
p.invalidateChild(this, null);//开启硬件加速了就将view所有的区域标记为脏区域以使viewRoot重绘所有的内容
return;
}
}
//未开启硬件加速
if (p != null && ai != null) {
final Rect r = ai.mTmpInvalRect;
r.set(0, 0, mRight - mLeft, mBottom - mTop);//将Rect设置为view的大小
// Don't call invalidate -- we don't want to internally scroll
// our own bounds
p.invalidateChild(this, r);//调用父view的invalidateChild方法 具体见ViewGroup
}
}
}
invalidate的实现很简单,只需要满足一定的条件,就需要走绘制流程,这里需要注意两个标记一个是PFLAG_INVALIDATED,代表了View当前已经失效了, 另一个PFLAG_DRAWING_CACHE_VALID,表示View的绘制缓存有效。这里根据参数invalidateCache为true对其进行设置,对于硬件加速和非硬件加速的情况分别走 不同的流程,现在大多数其实是走硬件加速的流程,但这里我们还是看软件绘制的流程,关于硬件加速我们在别的篇章中介绍,在这里Mark~一下就好。
这里会设置一个Rect区域,这个区域实际上就是我们View的大小,也是待刷新的dirty区域,因为mRight-mLeft是View的宽度,mBottom-mTop就是View的高度,随后通过调用ViewParent的 invalidateChild方法,这里的ViewParent实际上就是当前view的父View或者view层级数的ViewRoot,所以invalidateChild是会走到ViewGroup或者ViewRootImpl 中去的。我们接着看哈~
public final void invalidateChild(View child, final Rect dirty) {
ViewParent parent = this;
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
final boolean drawAnimation = (child.mPrivateFlags & PFLAG_DRAW_ANIMATION)
== PFLAG_DRAW_ANIMATION;//child view是否在执行动画
Matrix childMatrix = child.getMatrix();
final boolean isOpaque = child.isOpaque() && !drawAnimation &&
child.getAnimation() == null && childMatrix.isIdentity();//发起请求的view是否是不透明的,执行动画的view则不认为它不透明
int opaqueFlag = isOpaque ? PFLAG_DIRTY_OPAQUE : PFLAG_DIRTY;//child view 的 dirty标记
……
//存放dirty region的起点坐标
final int[] location = attachInfo.mInvalidateChildLocation;
location[CHILD_LEFT_INDEX] = child.mLeft;
location[CHILD_TOP_INDEX] = child.mTop;
……
do {
View view = null;
if (parent instanceof View) {
view = (View) parent;
}
if (drawAnimation) {
if (view != null) {
view.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
} else if (parent instanceof ViewRootImpl) {
((ViewRootImpl) parent).mIsAnimating = true;
}
}
// If the parent is dirty opaque or not dirty, mark it dirty with the opaque
// flag coming from the child that initiated the invalidate
if (view != null) {
if ((view.mViewFlags & FADING_EDGE_MASK) != 0 &&
view.getSolidColor() == 0) {
opaqueFlag = PFLAG_DIRTY;
}
if ((view.mPrivateFlags & PFLAG_DIRTY_MASK) != PFLAG_DIRTY) {
view.mPrivateFlags = (view.mPrivateFlags & ~PFLAG_DIRTY_MASK) | opaqueFlag;
}
}
//得到的是父view的父view
parent = parent.invalidateChildInParent(location, dirty);
……
} while (parent != null);
}
}
invalidateChild接收两个参数,发起invalidate的child view即dirty区域,dirty区域的起始坐标点就是child view 在父view中的左上角坐标,随后进行do...while循环,在循环中我们忽略执行动画的情况,然后为当前父view设置dirty标记。 这个dirty标记为父view记录下子view的视图情况,后续在draw时会用到该标记判断是否需要绘制通过onDraw绘制view 随后调用invalidateChildInParent方法并返回当前父View的父View,一直到ViewRootImpl。可见这个循环其实是一个在View 层级数中从发起View的父View不断上溯到ViewRoot执行invalidateChildInParent的过程,那么在这之间,invalidateChildInParent 到底会做些什么事情呢?这里我们传递给invalidateChildInParent的是dirty区域的在当前父view的左上角的坐标和dirty区域。
// /frameworks/base/core/java/android/view/ViewGroup.java
public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
if ((mPrivateFlags & PFLAG_DRAWN) == PFLAG_DRAWN ||
(mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID) {
if ((mGroupFlags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE)) !=
FLAG_OPTIMIZE_INVALIDATE) {
//计算dirty区域在当前父view中的偏移位置
dirty.offset(location[CHILD_LEFT_INDEX] - mScrollX,
location[CHILD_TOP_INDEX] - mScrollY);
if ((mGroupFlags & FLAG_CLIP_CHILDREN) == 0) {
dirty.union(0, 0, mRight - mLeft, mBottom - mTop);
}
final int left = mLeft;
final int top = mTop;
if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
if (!dirty.intersect(0, 0, mRight - left, mBottom - top)) {
dirty.setEmpty();
}
}
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;//同时置ViewParent的PFLAG_DRAWING_CACHE_VALID标记为0
//更新location
location[CHILD_LEFT_INDEX] = left;
location[CHILD_TOP_INDEX] = top;
if (mLayerType != LAYER_TYPE_NONE) {
mPrivateFlags |= PFLAG_INVALIDATED;
mLocalDirtyRect.union(dirty);
}
return mParent;
} else {
mPrivateFlags &= ~PFLAG_DRAWN & ~PFLAG_DRAWING_CACHE_VALID;
location[CHILD_LEFT_INDEX] = mLeft;
location[CHILD_TOP_INDEX] = mTop;
if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
dirty.set(0, 0, mRight - mLeft, mBottom - mTop);
} else {
// in case the dirty rect extends outside the bounds of this container
dirty.union(0, 0, mRight - mLeft, mBottom - mTop);//和当前VeiwGroup所属区域做并集
}
if (mLayerType != LAYER_TYPE_NONE) {
mPrivateFlags |= PFLAG_INVALIDATED;//ViewGroup的标记置失效
mLocalDirtyRect.union(dirty);
}
return mParent;
}
}
return null;
}
在invalidateChildInParent中,首先通过offset计算dirty区域在当前父view中偏移位置,起始的dirty区域就是发起invalidate的子view 它再父view中的左上角位置就是(mLeft,mTop),随后计算dirty区域,首先判断FLAG_CLIP_CHILDREN是否设置,即我们在布局文件中 设置的android:clipChildren,默认是设置为true的,这个属性是用来限制子view是否在父view的绘制区域内的。设置为false即FLAG_CLIP_CHILDREN未设置的情况下 表示可以超出父view的绘制区域。这种情况下的话就设置dirty区域为当前父view的区域。否则FLAG_CLIP_CHILDREN设置就根据当前dirty区域和父view的区域做交集运算后 得到的dirty区域。如果没交集则dirty置空。然后更新location为当前父view在其父view中的左上角位置,为下一次计算脏区域在其父view中的偏移做准备。这样经过一层层的 计算后最终回溯到ViewRootImpl中的invalidateChildInParent中。
// /frameworks/base/core/java/android/view/ViewRootImpl.java
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
checkThread();
if (dirty == null) {//脏区域为null说明要重新绘制整个view树
invalidate();
return null;
} else if (dirty.isEmpty() && !mIsAnimating) {//没有动画在执行且没有脏区域就不用绘制了
return null;
}
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);
}
}
final Rect localDirty = mDirty;
if (!localDirty.isEmpty() && !localDirty.contains(dirty)) {
mAttachInfo.mSetIgnoreDirtyState = true;
mAttachInfo.mIgnoreDirtyState = true;
}
// Add the new dirty rect to the current one
localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);
// Intersect with the bounds of the window to skip
// updates that lie outside of the visible region
final float appScale = mAttachInfo.mApplicationScale;
final boolean intersected = localDirty.intersect(0, 0,
(int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
if (!intersected) {
localDirty.setEmpty();
}
/*ViewRootImpl同样是ViewParent,它和ViewGroup的invalideChildInParent的区别在于
* ViewRootImpl在统计完dirty region后会进行schedualTraversals进行绘制流程*/
if (!mWillDrawSoon && (intersected || mIsAnimating)) {
scheduleTraversals();
}
return null;
}
checkThread会检查更新的线程是否是ui线程,如果dirty区域为null说明需要绘制整个view树,如果dirty区域为空或者未执行动画也不需要 再进行下去了。如果mCurScrollY不为空说明页面有滚动过,需要据此重新计算dirty区域。随后将dirty区域添加到localDrity中即当前view树 中的dirty区域中去。接着讲当前dirty区域和整个页面区域做交集计算,intersected一般为true也就是有交集,最后通过scheduleTraversals 进行重绘的操作。
// /frameworks/base/core/java/android/view/ViewRootImpl.java
private void performDraw() {
boolean fullRedrawNeeded = mFullRedrawNeeded;
mFullRedrawNeeded = false;
……
try {
draw(fullRedrawNeeded, updateTranformHint);
} finally {
mIsDrawing = false;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
……
}
private void draw(boolean fullRedrawNeeded, boolean updateTranformHint) {
Surface surface = mSurface;
if (!surface.isValid()) {//surface无效的话就返回
return;
}
……
final Rect dirty = mDirty;
//如果是绘制整个页面 就设置dirty为整个屏幕的大小
if (fullRedrawNeeded) {
attachInfo.mIgnoreDirtyState = true;
dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
}
……
//重绘区域不为空 或者正在执行动画
if (!dirty.isEmpty() || mIsAnimating) {
//开启了硬件加速
if (attachInfo.mHardwareRenderer != null && attachInfo.mHardwareRenderer.isEnabled()) {
……
}else{
……
//软件绘制 dirty描述绘制区域
if (!drawSoftware(surface, attachInfo, yoff, scalingRequired, dirty)) {
return;
}
}
}
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int yoff,
boolean scalingRequired, Rect dirty) {
Canvas canvas;
try {
int left = dirty.left;
int top = dirty.top;
int right = dirty.right;
int bottom = dirty.bottom;
……
//根据重绘区域获取一个Canvas
canvas = mSurface.lockCanvas(dirty);
}catch(...){
……
}
try{
mView.draw(canvas);//view的绘制
}finally{
……
surface.unlockCanvasAndPost(canvas);
……
}
return true;
}
scheduleTraversals通过performDraw来绘制view,这里实际上是调用draw(boolean fullRedrawNeeded, boolean updateTranformHint)方法, 对应非硬件加速的情况,这个方法内部调用drawSoftware来进行绘制,注意这里的dirty实际上就是当前view树计算后得到的脏区域。当然也包括了我们之前 调用view的invalidate后计算的脏区域。通过这个脏区域通过在mSurface中设置一个裁剪区域并返回一个Canvas,随后的绘制就在此Canvas的裁剪区域中进行绘制。
View的draw绘制过程主要包括以下方面:
- Draw the background
- If necessary, save the canvas' layers to prepare for fading
- Draw view's content
- Draw children
- If necessary, draw the fading edges and restore layers
- Draw decorations (scrollbars for instance)
public void draw(Canvas canvas) {
……
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState); //子view是否不透明且未执行动画
……
background.draw(canvas);
if (!dirtyOpaque) onDraw(canvas);//子视图不透明 会覆盖掉当前视图 所以skip
dispatchDraw(canvas);
onDrawScrollBars(canvas);
……
}
在draw流程中是否回调onDraw是由dirtyOpaque决定的,而dirtyOpaque是根据标记PFLAG_DIRTY_OPAQUE是否设置来决定的, 还记得在上面invalidateChild时会为父view设置这个标记?这个标记表明子view是不透明的且没有在执行动画,那么此时 就没必要对view进行绘制了,因为子view是在父view之上的,会覆盖掉当前view的视图,所有就没有必要绘制了。 需要注意的是,ViewGroup作为一个容器控件,默认情况下是没有任何东西可画的,它是一个透明控件。
draw过程中的dispatchDraw用来绘制子view,我们看下ViewGroup中实现
// /frameworks/base/core/java/android/view/ViewGroup.java
@Override
protected void dispatchDraw(Canvas canvas) {//绘制子视图
final int count = mChildrenCount;
final View[] children = mChildren;
……
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);
}
}
}
……
}
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
drawChild会调用view的另一个重载方法,它有三个参数。
// /frameworks/base/core/java/android/view/View.java
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
……
mPrivateFlags |= PFLAG_DRAWN;
/*quickReject判断当前View的区域是否落在Canvas的裁剪区域之外,如果是返回true,表示跳过当前view的绘制*/
if (!concatMatrix &&
(flags & (ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS |
ViewGroup.FLAG_CLIP_CHILDREN)) == ViewGroup.FLAG_CLIP_CHILDREN &&
canvas.quickReject(mLeft, mTop, mRight, mBottom, Canvas.EdgeType.BW) &&
(mPrivateFlags & PFLAG_DRAW_ANIMATION) == 0) {
/* 1. view没有关联的matrix矩阵
* 2. view的绘制区域落在Canvas裁剪区域之外
* 3. clipchild设置为true
* 4. 没有执行动画
* **/
mPrivateFlags2 |= PFLAG2_VIEW_QUICK_REJECTED;
return more;
}
……
if (!layerRendered) {
if (!hasDisplayList) {
// Fast path for layouts with no backgrounds
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
dispatchDraw(canvas);//绘制子视图
} else {
draw(canvas);//绘制本身及子视图
}
} else {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
((HardwareCanvas) canvas).drawDisplayList(displayList, null, flags);
}
}
……
}
从draw方法中可以看出,子view的并不是每次进行绘制流程时候都需要绘制一遍,尤其是当view通过invalidate触发绘制时, 因此此时Canvas根据dirty区域设置了裁剪区域,而ViewGroup在绘制子view时会判断其区域是否落在这个裁剪区域内,如果 不在就没有必要进行绘制了,直接返回。这是通过canvas的quickReject进行判断的.随后就是字view的绘制,这里面会判断是否 设置了PFLAG_SKIP_DRAW,这个标记用来控制是否需要对View进行绘制,对ViewGroup来说默认是设置了的,所以它直接通过 dispatchDraw来绘制子view,并不会对自身进行绘制,onDraw也不会进行调用。如果想要使它绘制可以通过setWillNotDraw(false) 来清除PFLAG_SKIP_DRAW标记。这样会进入view(ViewGroup)的draw流程,但具体能不能调用onDraw还要做以下判断
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState); //子view是否不透明且未执行动画
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
void setFlags(int flags, int mask) {
……
if ((changed & DRAW_MASK) != 0) {
if ((mViewFlags & WILL_NOT_DRAW) != 0) {
if (mBackground != null) {
mPrivateFlags &= ~PFLAG_SKIP_DRAW;
mPrivateFlags |= PFLAG_ONLY_DRAWS_BACKGROUND;
} else {
mPrivateFlags |= PFLAG_SKIP_DRAW;
}
} else {
mPrivateFlags &= ~PFLAG_SKIP_DRAW;
}
requestLayout();
invalidate(true);//通过invalidate刷新
}
……
}
到这里invalidate的流程就分析完了,需要注意的是,invalidate会触发绘制流程,但是并不会触发onMeasure和onLayout。