自定義View——invalidate傳遞與繪制流程分析
上一篇文章 自定義View——View的彈性滑動 中,我們對View的滑動進行了實戰以及簡單分析。但在文章的最后,仍然遺留了兩個問題,第一個是invalidate與postInvalidate有什么區別呢?第二個是invalidate是如何調用computeScroll()方法的呢?這兩個問題將在這一篇文章中進行分析。
一、invalidate與postInvalidate
invalidate與postInvadlidate都是用于請求View重繪的API,invalidate在主線程中進行調用,而postInvadlidate則在子線程中進行調用。
我們來分析下postInvadlidate的源碼 :
public void postInvalidate() {
postInvalidateDelayed(0);
}
postInvalidate()蔣會調用postInvalidateDelayed(0)方法,繼續跟進。
public void postInvalidateDelayed(long delayMilliseconds) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
}
}
postInvalidateDelayed方法,通過attachInfo獲取到當前的ViewRootImpl對象,調用它的dispatchInvalidateDelayed方法
public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
mHandler.sendMessageDelayed(msg, delayMilliseconds);
}
從上面的源碼已經可以看出,postInvalidate的子線程這一個特性了。再繼續跟下去看看。
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_INVALIDATE:
((View) msg.obj).invalidate();
break;
...
}
}
代碼跟到這里,也就明白了,postInvalidate通過sendMessageDelayed的方法,加入到了looper中,之后在handleMessage中再調用對應View的invalidate()方法,請求View重繪。
二、invalidate流程分析
現在我們來看看invalidate是如何讓View進行重繪的呢?
(PS:我這里使用的API版本為23,具體的代碼可能和其他的版本有稍許不同)
1、invalidate的請求傳遞
我們的旅程從View的invalidate傳遞過程開始
現在來看看View#invalidate()方法。
public void invalidate() {
invalidate(true);
}
void invalidate(boolean invalidateCache) {
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}
invalidate調用View#invalidateInternal方法傳入當前View的位置參數。
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
// 如果View重繪,則它也將重繪
if (mGhostView != null) {
mGhostView.invalidate(true);
return;
}
// View是否可見,是否在動畫運行中
if (skipInvalidate()) {
return;
}
// 根據View的標記來判斷View是否需要進行重繪
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
|| (fullInvalidate && isOpaque() != mLastIsOpaque)) {
if (fullInvalidate) {
mLastIsOpaque = isOpaque();
mPrivateFlags &= ~PFLAG_DRAWN;
}
// 設置標志,表明View正在被重繪
mPrivateFlags |= PFLAG_DIRTY;
//清除緩存,設置標志,表明重繪由當前View發起
if (invalidateCache) {
mPrivateFlags |= PFLAG_INVALIDATED;
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
}
// 把需要重繪的View區域傳遞給父View
final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
// 設置重繪區域
damage.set(l, t, r, b);
// 關鍵代碼,調用父View的方法,向上傳遞重繪事件
p.invalidateChild(this, damage);
}
...
}
}
上述代碼中,會判斷當前View的狀態,是否需要進行重繪,之后設置一系列標記位。通過父View的invalidateChild(this, damage)方法,將需要重繪的區域傳遞給父View。
接著來看下ViewGroup#invalidateChild方法,這里僅截取了其中的主要代碼
public final void invalidateChild(View child, final Rect dirty) {
ViewParent parent = this;
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
...
// 保存子View的left、top
final int[] location = attachInfo.mInvalidateChildLocation;
location[CHILD_LEFT_INDEX] = child.mLeft;
location[CHILD_TOP_INDEX] = child.mTop;
if (!childMatrix.isIdentity() ||
(mGroupFlags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) {
RectF boundingRect = attachInfo.mTmpTransformRect;
boundingRect.set(dirty);
Matrix transformMatrix;
if ((mGroupFlags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) {
Transformation t = attachInfo.mTmpTransformation;
boolean transformed = getChildStaticTransformation(child, t);
if (transformed) {
transformMatrix = attachInfo.mTmpMatrix;
transformMatrix.set(t.getMatrix());
if (!childMatrix.isIdentity()) {
transformMatrix.preConcat(childMatrix);
}
} else {
transformMatrix = childMatrix;
}
} else {
transformMatrix = childMatrix;
}
transformMatrix.mapRect(boundingRect);
// 設置需要重繪的區域
dirty.set((int) (boundingRect.left - 0.5f),
(int) (boundingRect.top - 0.5f),
(int) (boundingRect.right + 0.5f),
(int) (boundingRect.bottom + 0.5f));
}
// 這里的do...while方法,讓view可以不斷的去調用父類的
// invalidateChildInParent方法,來傳遞重繪請求
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;
}
}
// 這里是關鍵代碼,他會調用父類的
parent = parent.invalidateChildInParent(location, dirty);
if (view != null) {
// Account for transform on current parent
Matrix m = view.getMatrix();
if (!m.isIdentity()) {
RectF boundingRect = attachInfo.mTmpTransformRect;
boundingRect.set(dirty);
m.mapRect(boundingRect);
dirty.set((int) (boundingRect.left - 0.5f),
(int) (boundingRect.top - 0.5f),
(int) (boundingRect.right + 0.5f),
(int) (boundingRect.bottom + 0.5f));
}
}
} while (parent != null);
}
}
上述代碼中,設置了需要重繪的區域dirty。之后再do…while方法中,反復的調用 parent = parent.invalidateChildInParent(location, dirty) 方法,來調用父類的invalidateChildInParent對View的重繪請求進行傳遞。這里的parent有可能是ViewGroup,也有可能是ViewRoot,我們先來看看ViewGroup#invalidateChildInParent方法
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) {
// 子View中的布局位置轉換為父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;
location[CHILD_LEFT_INDEX] = left;
location[CHILD_TOP_INDEX] = top;
if (mLayerType != LAYER_TYPE_NONE) {
mPrivateFlags |= PFLAG_INVALIDATED;
}
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);
}
if (mLayerType != LAYER_TYPE_NONE) {
mPrivateFlags |= PFLAG_INVALIDATED;
}
return mParent;
}
}
return null;
}
在上述代碼中,將會使用offset,把子View需要重繪的坐標區域轉換為父View中的坐標區域。之后使用union對子View與父View的區域進行集合運算,獲得需要繪制的區域。
接下來我們再來看看ViewRoot#invalidateChildInParent方法,ViewRoot并不是View,ViewRoot的實現類為ViewRootImpl,我們來看下它的invalidateChildInParent方法。
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
// 檢查線程是否為創建View的線程,即創建View的線程中是否含有此ViewRootImpl
checkThread();
if (DEBUG_DRAW) Log.v(TAG, "Invalidate child: " + dirty);
// 檢查重繪區域
if (dirty == null) {
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);
}
}
invalidateRectOnScreen(dirty);
return null;
}
private void invalidateRectOnScreen(Rect dirty) {
...
if (!mWillDrawSoon && (intersected || mIsAnimating)) {
//關鍵代碼,ViewTree列表
scheduleTraversals();
}
}
上述代碼中,進入之后會線程以及重繪區域的檢查,之后調用invalidateRectOnScreen方法,然后調用scheduleTraversals()方法。
來繼續看看ViewRootImpl#scheduleTraversals()。
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
// handler消息傳遞繪制請求
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
// 關鍵代碼,執行ViewTree遍歷
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
上述代碼中,將會之后handler,之后會調用mTraversalRunnable類,從而調用doTraversal方法,最后調用performTraversals()執行ViewTree的遍歷。
現在繼續查看ViewRootImpl#performTraversals()方法。
private void performTraversals() {
...
if (!cancelDraw && !newSurface) {
if (!skipDraw || mReportNextDraw) {
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).startChangingAnimations();
}
mPendingTransitions.clear();
}
// 關鍵代碼
performDraw();
}
}
...
}
private void performDraw() {
...
final boolean fullRedrawNeeded = mFullRedrawNeeded;
mFullRedrawNeeded = false;
mIsDrawing = true;
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");
try {
// 關鍵代碼
draw(fullRedrawNeeded);
} finally {
mIsDrawing = false;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
...
}
在其中進行View的是否可見,是否為surfasce,是否正在繪制,是否存在于刪除列表中等判斷,之后調用performDraw()開始執行繪制。在performDraw()又調用了ViewRootImpl的draw方法,并傳遞了fullRedrawNeeded參數,此參數源自mFullRedrawNeeded成員變量,用于表示是否需要重新繪制全部的View。現在繼續看看ViewRootImpl#draw源碼。
private void draw(boolean fullRedrawNeeded) {
Surface surface = mSurface;
...
// 獲取mDirty,該值表示需要重繪的區域
final Rect dirty = mDirty;
if (mSurfaceHolder != null) {
// The app owns the surface, we won't draw.
dirty.setEmpty();
if (animating) {
if (mScroller != null) {
mScroller.abortAnimation();
}
disposeResizeBuffer();
}
return;
}
// 如果為ture,則設置dirty區域為全屏
if (fullRedrawNeeded) {
mAttachInfo.mIgnoreDirtyState = true;
dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
}
...
// 重繪區域、動畫判斷
// 硬件渲染判斷
// 關鍵代碼
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
...
}
在draw方法中,根據傳如fullRedrawNeeded參數,設置需要重繪的dirty區域,最后調用drawSoftware方法,把參數傳遞進去,現在繼續看ViewRootImpl#drawSoftware源碼。
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
...
try {
if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
}
dirty.setEmpty();
mIsAnimating = false;
mView.mPrivateFlags |= View.PFLAG_DRAWN;
try {
canvas.translate(-xoff, -yoff);
if (mTranslator != null) {
mTranslator.translateCanvas(canvas);
}
canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
attachInfo.mSetIgnoreDirtyState = false;
// 關鍵代碼,mView為DecorView,開啟View繪制
mView.draw(canvas);
drawAccessibilityFocusedDrawableIfNeeded(canvas);
} finally {
if (!attachInfo.mSetIgnoreDirtyState) {
// Only clear the flag if it was not set during the mView.draw() call
attachInfo.mIgnoreDirtyState = false;
}
}
}
...
}
上述代碼中,首先對canvas進行一些屬性設置,包括色塊、平移等。之后調用mView.draw(canvas)方法,開始對View進行繪制。mView就是window中的頂級視圖DecorView(這個坑會在之后的文章中說明,這里當做一個頂級的ViewGroup即可)。
2、繪制流程
DecorView繼承自FrameLayout,而ViewGroup的draw方法繼承自View,so,所以我們直接看View#draw即可。
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// we're done...
return;
}
...
}
draw方法中,官方對其的步驟進行了清晰的注釋,我們來看下流程,在執行流程之前會檢查繪制區域是否透明:
* 1、繪制View背景,如果透明則不繪制
* 2、如果需要,則保存畫布的圖層
* 3、繪制View內容,如果透明則不繪制
* 4、繪制子View————這個很重要
* 5、如果需要,則繪制View的褪色邊緣和恢復圖層
* 6、繪制裝飾滾動條
這里最重要的步驟是第四步,繪制子View,現在我們來看下這個ViewGroup#dispatchDraw(canvas)方法,注意這里的View是一個DecorView,所以要在ViewGroup中去查看這個方法,View中的這個方法是一個空方法。
protected void dispatchDraw(Canvas canvas) {
...
for (int i = 0; i < childrenCount; i++) {
while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
final View transientChild = mTransientViews.get(transientIndex);
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
more |= drawChild(canvas, transientChild, drawingTime);
}
transientIndex++;
if (transientIndex >= transientCount) {
transientIndex = -1;
}
}
int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
while (transientIndex >= 0) {
// there may be additional transient views after the normal views
final View transientChild = mTransientViews.get(transientIndex);
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
more |= drawChild(canvas, transientChild, drawingTime);
}
transientIndex++;
if (transientIndex >= transientCount) {
break;
}
}
...
}
上述代碼對所有的子View進行遍歷,并調用ViewGroup#drawChild方法。
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
drawChild又調用了子View的draw方法,這樣繪制就傳遞了下去,當然這個draw方法和之前這一小節一開始介紹的View#draw方法并不一樣,我們來看看
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
...
if (!drawingWithRenderNode) {
computeScroll();
sx = mScrollX;
sy = mScrollY;
}
...
if (!drawingWithDrawingCache) {
if (drawingWithRenderNode) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
((DisplayListCanvas) canvas).drawRenderNode(renderNode);
} else {
// 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 if (cache != null) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
if (layerType == LAYER_TYPE_NONE) {
// no layer paint, use temporary paint to draw bitmap
Paint cachePaint = parent.mCachePaint;
if (cachePaint == null) {
cachePaint = new Paint();
cachePaint.setDither(false);
parent.mCachePaint = cachePaint;
}
cachePaint.setAlpha((int) (alpha * 255));
canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
} else {
// use layer paint to draw the bitmap, merging the two alphas, but also restore
int layerPaintAlpha = mLayerPaint.getAlpha();
mLayerPaint.setAlpha((int) (alpha * layerPaintAlpha));
canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint);
mLayerPaint.setAlpha(layerPaintAlpha);
}
}
...
}
上述代碼會先判斷之前是否進行過了繪制,如果沒有則進入快速繪制通道,對沒有背景的View進行繪制。判斷是否需要跳過自身的draw繪制方法,如果跳過則進入dispatchDraw,不跳過則進入當前View的draw方法,即這一小節開頭的draw方法,就此形成了循環。同時我們在這里看到了 computeScroll() 方法,也就印證了上一篇文章對于彈性滑動過程的描述。
流程圖如下:
三、小結
本文對上一篇遺留的問題postInvalidate與invalidate的區別進行了回答與分析,對invalidate的傳遞流程,以及View的繪制流程進行了源碼分析,解答了invalidate是如何調用computeScroll()的問題。如果在閱讀過程中,有任何疑問與問題,歡迎與我聯系。
來自:http://www.idtkm.com/customview/customview9/