在你們都瞭解過Android View的測量、佈局、繪製機制後,咱們來細化地分析一下關於View的重繪invalidate與更新requestLayoutandroid
public class CustomEmptyView extends View {
public CustomEmptyView(Context context) {
super(context);
}
public CustomEmptyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CustomEmptyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.i("CustomEmptyView", "onMeasure");
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
Log.i("CustomEmptyView", "onLayout");
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.i("CustomEmptyView", "onDraw");
}
}
複製代碼
從View的繪製機制可知,View從測量、佈局、繪製的步驟中會對應執行該View#onMeasure()、View#onLayout()、View#onDraw()。那麼咱們今天討論的View#invalidate()和View#requestLayout()呢?咱們打印一下數據。canvas
View#invalidate()的執行步驟是:緩存
2019-03-26 17:32:34.739 8075-8075/com.example.myapplication I/CustomEmptyView: onDraw
複製代碼
View#requestLayout()的執行步驟是:bash
2019-03-26 17:33:13.497 8075-8075/com.example.myapplication I/CustomEmptyView: onMeasure
2019-03-26 17:33:13.501 8075-8075/com.example.myapplication I/CustomEmptyView: onLayout
2019-03-26 17:33:13.503 8075-8075/com.example.myapplication I/CustomEmptyView: onDraw
複製代碼
從打印數據來推測就是View#invalidate()只會執行View#onDraw();而View#requestLayout()則會從新走View的繪製流程。接下來咱們從源碼的角度來分析一下。 下面的源碼分析基於android-28app
咱們分析一下View#requestLayout()。咱們定位到對應的源碼ide
/**
* Call this when something has changed which has invalidated the
* layout of this view. This will schedule a layout pass of the view
* tree. This should not be called while the view hierarchy is currently in a layout
* pass ({@link #isInLayout()}. If layout is happening, the request may be honored at the
* end of the current layout pass (and then layout will run again) or after the current
* frame is drawn and the next layout occurs.
*
* 當某內容發生更改,致使該視圖的佈局重繪時調用此函數。這將安排視圖樹的佈局傳遞。
* 當視圖層次結構當前處於佈局Layout事件時,不會執行該函數.
* 若是正在進行佈局,能夠在當前佈局傳遞結束時(而後佈局將再次運行)或在繪製當前幀並執行下一個佈局以後執行請求。
* <p>Subclasses which override this method should call the superclass method to
* handle possible request-during-layout errors correctly.</p>
*/
@CallSuper
public void requestLayout() {
if (mMeasureCache != null) mMeasureCache.clear();
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
// Only trigger request-during-layout logic if this is the view requesting it,
// not the views in its parent hierarchy
ViewRootImpl viewRoot = getViewRootImpl();
// 該方法不在佈局事件期間中執行
if (viewRoot != null && viewRoot.isInLayout()) {
if (!viewRoot.requestLayoutDuringLayout(this)) {
return;
}
}
mAttachInfo.mViewRequestingLayout = this;
}
// PFLAG_FORCE_LAYOUT會在執行View的measure()和layout()方法時判斷,這個是之前的文章看到的。
// 可是在當前源碼的View.class和ViewRootImpl.class,全局搜索PFLAG_FORCE_LAYOUT,並無直接的判斷,致使View#requestLayout()不執行測量和佈局方法
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
// isLayoutRequested()對應是mLayoutRequested字段,該字段在默認爲false
if (mParent != null && !mParent.isLayoutRequested()) {
// 執行父容器的requestLayout()方法
mParent.requestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}
複製代碼
當咱們點擊ViewGroup#requestLayout(),發現它是一個空實現,咱們可知ViewParent是interface類,咱們經過以前的View的分析,能夠去ViewRootImpl類看看ViewGroup#requestLayout()的實現方法。函數
@Override
public void requestLayout() {
// mHandlingLayoutInLayoutRequest這個參數,經過全局變量定位,在performLayout()開始時爲true,結束時爲false,與以前說的,不在佈局期間執行相對應
if (!mHandlingLayoutInLayoutRequest) {
// 檢查是否UI線程
checkThread();
// 這裏將mLayoutRequested設爲true
mLayoutRequested = true;
scheduleTraversals();
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
複製代碼
看到scheduleTraversals(),我相信你們都以爲快觸摸到真相,可是發現點擊該方法裏的實現,並不能找到咱們想要的,此時咱們想一下以前的打印數據,View#requestLayout()會從新執行View的繪製步驟,View的繪製步驟最核心是ViewRootImpl#performTraverals,按照這個思路咱們繼續尋找。oop
從上面的源碼中,咱們看到mChoreographer這個對象,咱們曾經在分析滑動流程度的時候,說起過Choreographer編舞者這個對象,咱們最後從mChoreographer這個對象中的mTraversalRunnable參數找到線索。源碼分析
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
複製代碼
最終咱們發現mTraversalRunnable這個是一個Runnable對象,在scheduleTraversals()中傳入mTraversalRunnable,就會執行doTraversal(),在doTraversal()中咱們也如願地找到咱們想要的核心方法ViewRootImpl#performTraverals().當調用ViewRootImpl#performTraverals就從新開始該控件的測量、佈局、繪製步驟。符合了咱們一開始的打印數據。佈局
接着咱們看一下View#invalidate的源碼。
/**
* Invalidate the whole view. If the view is visible,
* 重繪整個View,若是View是可視的。
* {@link #onDraw(android.graphics.Canvas)} will be called at some point in
* the future.
* <p>
* This must be called from a UI thread. To call from a non-UI thread, call
* {@link #postInvalidate()}.
* 這個方法必須被使用在UI線程,用在非UI線程的方法爲postInvalidate()
*/
public void invalidate() {
invalidate(true);
}
/**
* This is where the invalidate() work actually happens. A full invalidate()
* causes the drawing cache to be invalidated, but this function can be
* called with invalidateCache set to false to skip that invalidation step
* for cases that do not need it (for example, a component that remains at
* the same dimensions with the same content).
* 這就是invalidate()方法工做發生的地方,一個完整的invalidate()方法會引發繪製
* 緩存失效,可是這個函數能設置參數invalidateCache爲false來跳太重繪步驟,因爲
* 該方法不被須要,例如一個組件保持相同的尺寸和相同的內容
*
* @param invalidateCache Whether the drawing cache for this view should be
* invalidated as well. This is usually true for a full
* invalidate, but may be set to false if the View's contents or * dimensions have not changed. * 這繪製緩存是否應被重繪.一個完整的重繪一般爲true,可是可能設置爲false,若是View的內容和尺寸沒有被改變。 * * @hide */ public void invalidate(boolean invalidateCache) { invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true); } void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache, boolean fullInvalidate) { // 暫不見賦值,從下面的方法中,得出他是一個被捨棄的方法,能夠跳過 if (mGhostView != null) { mGhostView.invalidate(true); return; } // 跳太重繪。從方法描述可知,該方法判斷該View不被重繪,當它處於不可見和沒有處於動畫中 if (skipInvalidate()) { return; } // 這裏大量的參數對比,應該就是上面所說的判斷座標位置有沒發生變化,若是發生了變化就標識爲須要重繪 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; } mPrivateFlags |= PFLAG_DIRTY; // 若是須要所有重繪,invalidate()未傳參調用時默認爲true if (invalidateCache) { // 記住這個PFLAG_INVALIDATED標誌位 mPrivateFlags |= PFLAG_INVALIDATED; mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID; } // Propagate the damage rectangle to the parent view. // 從下面參數mParent可知,應該是將須要重繪這個事件通知給父容器 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); // 這個就是重點,咱們去看一下父容器的invalidateChild()方法 p.invalidateChild(this, damage); } // Damage the entire projection receiver, if necessary. if (mBackground != null && mBackground.isProjected()) { final View receiver = getProjectionReceiver(); if (receiver != null) { receiver.damageInParent(); } } } } 複製代碼
從上面的分析可知,通過invalidate()的重載方法,最終會調用invalidateInternal(),在這個方法裏頭,要判斷是否須要重繪,若是須要重繪,就對該View進行標識,而後將該View的Rect信息傳遞給父容器的invalidateChild().
與以前的View#requestLayout()類似,最終一樣是執行ViewRootImpl#invalidateChild(),而後我繼續分析ViewRootImpl#invalidateChild()的實現。
@Override
public void invalidateChild(View child, Rect dirty) {
invalidateChildInParent(null, dirty);
}
// 因爲沒有註釋,咱們從方法名去分析源碼,重載至這個最終的方法,意思爲在父容器中重繪子控件
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
// 檢查線程,裏面判斷爲,該方法須要執行在UI線程,驗證了以前View#invalidate()的描述
checkThread();
if (DEBUG_DRAW) Log.v(mTag, "Invalidate child: " + dirty);
// 從上一步可知,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) {
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();
}
if (!mWillDrawSoon && (intersected || mIsAnimating)) {
// 上面的設置和調整,最終調用scheduleTraversals()
scheduleTraversals();
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
複製代碼
看到這裏,咱們立刻聯想起以前分析的View#requestLayout(),一樣是scheduleTraversals(),可是從打印數據可知,View#invalidate()是不會執行測量與佈局的,可是目前來看它們最終調用的方法但是一致的。
咱們能夠留意一下以前的ViewRootImpl#requestLayout()方法中,主動將一個全局變量mLayoutRequested設置爲true;那麼大膽猜想這個對象確定會影響到performMeasure()與performLayout(),咱們翻一下ViewRootImpl#performTraversals()
private void performTraversals() {
···
boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
···
boolean windowShouldResize = layoutRequested && windowSizeMayChange
&& ((mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight())
|| (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT &&
frame.width() < desiredWindowWidth && frame.width() != mWidth)
|| (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT &&
frame.height() < desiredWindowHeight && frame.height() != mHeight));
···
if (mFirst || windowShouldResize || insetsChanged || viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
···
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
···
}
···
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
boolean triggerGlobalLayoutListener = didLayout
|| mAttachInfo.mRecomputeGlobalAttributes;
if (didLayout) {
performLayout(lp, mWidth, mHeight);
}
···
}
複製代碼
對於複雜的ViewRootImpl#performTraversals(),咱們抽取一些關鍵的代碼,的確能夠驗證mLayoutRequested對象是會影響測量和佈局對應的方法的,所以能夠驗證咱們一開始的打印數據,View#invalidate()是不會執行測量和佈局的。
從View#invalidate()的註釋描述可知,View#invalidate()須要執行在UI線程中,若是在非UI線程須要使用View#postInvalidate(),咱們簡單地分析一下源碼。
public void postInvalidate() {
postInvalidateDelayed(0);
}
public void postInvalidate(int left, int top, int right, int bottom) {
postInvalidateDelayed(0, left, top, right, bottom);
}
public void postInvalidateDelayed(long delayMilliseconds, int left, int top,
int right, int bottom) {
// We try only with the AttachInfo because there's no point in invalidating // if we are not attached to our window final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { final AttachInfo.InvalidateInfo info = AttachInfo.InvalidateInfo.obtain(); info.target = this; info.left = left; info.top = top; info.right = right; info.bottom = bottom; attachInfo.mViewRootImpl.dispatchInvalidateRectDelayed(info, delayMilliseconds); } } 複製代碼
ViewRootImpl#dispatchInvalidateRectDelayed()
public void dispatchInvalidateRectDelayed(AttachInfo.InvalidateInfo info, long delayMilliseconds) {
final Message msg = mHandler.obtainMessage(MSG_INVALIDATE_RECT, info);
mHandler.sendMessageDelayed(msg, delayMilliseconds);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_INVALIDATE_RECT:
final View.AttachInfo.InvalidateInfo info = (View.AttachInfo.InvalidateInfo) msg.obj;
info.target.invalidate(info.left, info.top, info.right, info.bottom);
info.recycle();
break;
···
}
}
複製代碼
從源碼可知,View#postInvalidate()會發送一個MSG_INVALIDATE_RECT的Handler消息,在接收消息後,一樣是執行View#invalidate()方法。
在翻閱資料的時候,不少地方都會描述到View#requestLayout不執行performDraw(),可是本身的打印結果是會顯示執行performDraw()的。咱們帶着問題翻一下源碼。直接定位到ViewRootImpl#performTraversals的performDraw()
private void performTraversals() {
···
// dispatchOnPreDraw()返回註釋是:若是當前繪製應該被取消和從新調度,則爲True,不然爲false。
// final boolean isViewVisible = viewVisibility == View.VISIBLE; 只要是顯示的View,cancelDraw爲true
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
// cancelDraw經過dispatchOnPreDraw()的註釋解析和isViewVisible,得出cancelDraw應該爲false
// newSurface默認爲false,在測量判斷邏輯中,在判斷是否新的Surface會設置爲true,這裏應該是false
// 由於會執行performDraw()
if (!cancelDraw && !newSurface) {
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).startChangingAnimations();
}
mPendingTransitions.clear();
}
performDraw();
}
}
複製代碼
ViewRootImpl#performDraw()
private void performDraw() {
// Display.STATE_OFF表示顯示狀態:顯示關閉。
// mReportNextDraw對象默認false,可在ViewRootImpl#reportNextDraw()中設置爲true,可是第一個判斷已經爲false,不影響判斷
if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) {
return;
} else if (mView == null) {// 必然不爲null
return;
}
final boolean fullRedrawNeeded = mFullRedrawNeeded || mReportNextDraw;
mFullRedrawNeeded = false;
mIsDrawing = true;
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");
boolean usingAsyncReport = false;
if (mReportNextDraw && mAttachInfo.mThreadedRenderer != null
&& mAttachInfo.mThreadedRenderer.isEnabled()) {
usingAsyncReport = true;
mAttachInfo.mThreadedRenderer.setFrameCompleteCallback((long frameNr) -> {
// TODO: Use the frame number
pendingDrawFinished();
});
}
try {
// 最重點是這句,draw()方法的執行,能影響這段代碼的執行,只有上面的兩個return邏輯。
boolean canUseAsync = draw(fullRedrawNeeded);
if (usingAsyncReport && !canUseAsync) {
mAttachInfo.mThreadedRenderer.setFrameCompleteCallback(null);
usingAsyncReport = false;
}
} finally {
mIsDrawing = false;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
···
}
複製代碼
從上面的判斷能夠推斷到draw()是會被執行,與網上得出結論不一致,暫時沒得出什麼緣由,若有分析錯誤和不足的地方,但願指定一下。
View繪製步驟是performMeasure()、performLayout()、performDraw(),咱們通過對invalidate和requestLayout的源碼分析,能夠得出。invalidate()方法只會執行performDraw()方法;而requestLayout()方法會執行performMeasure()、performLayout()、performDraw()。在對應的應用場景,若是隻是View的顯示內容發生變化且不改變View的大小和位置,則使用invalidate(),若是大小、位置、內容都發生改變則調用requestLayout()。