經過上一章的摸索,我們已經瞭解了Android中View的繪製流程分別是measure、layout和draw,那麼對Android有一些瞭解的話,一定知道View中有這樣幾個方法invalidate、postInvalidate以及requestLayout,我們知道這些方法調用後會觸發View的重繪(不一定正確的說法),那麼它們的用法是什麼,有什麼區別以及使用時候有哪些注意事項,這就是我們這一篇文章力求弄明白的東西,好了,廢話不多說,開始今天的分析。
invalidate
我們從View源碼中追本溯源,可以看到在View中方法的調用情況是這樣的:
/**
* 使當前View失效,若View是可見的那麼onDraw方法會在未來的某個時間點被調用
* 該方法必須在UI線程被調用,若需要在非UI線程調用,調用postInvalidate
*/
public void invalidate() {
invalidate(true);
}
/**
* 這個方法是invalidate整整產生作用的地方,一個invalidate請求會將繪製緩存設爲失效
* 不過這個方式可以設置參數爲false來禁止繪製緩存失效
*/
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 ((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)) {
// Propagate the damage rectangle to the parent 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);
//調用父控件的invalidateChild方法
p.invalidateChild(this, damage);
}
...
}
}
從這裏可以看出invalidate方法的作用是令當前View的繪製緩存失效,並且會調用onDraw方法,不過我們並沒有在上述代碼中看到onDraw(注意invalidate方法只能在UI線程中調用,工作線程調用該方法會報錯),我們知道該方法最終調用了父控件的invalidateChild方法,那麼我們來查看下ViewGroup中的invalidateChild:
/**
* 不要主動調用該方法
*/
public final void invalidateChild(View child, final Rect dirty) {
ViewParent parent = this;
...
do {
...
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);
} while (parent != null);
}
}
該方法執行了一段循環代碼,這裏層層調用父控件的invalidateChildInParent方法,知道返回值爲空,我們知道繪製的起點是在ViewRootImpl中,那麼打開ViewRootImpl,查看其invalidateChildInParent代碼:
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
invalidateRectOnScreen(dirty);
return null;
}
private void invalidateRectOnScreen(Rect dirty) {
if (!mWillDrawSoon && (intersected || mIsAnimating)) {
scheduleTraversals();
}
}
這裏在返回null之後上述循環終止,我們注意到在循環終止前ViewRootImpl調用了scheduleTraversals方法,這個方法跟我們之前講過的performTraversals方法很像,我們來看看裏面有什麼:
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//這行代碼在Choreographer類內部使用Handler機制最終調用了doTraversal方法
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
//調用此方法判斷是否需要重新測量放置以及繪製
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
可以看到在繞了一圈之後,ViewRootImpl最終會調用我們上篇講到的performTraversals方法,重複判斷流程。至此,View的invalidate方法執行流程完畢。
postInvalidate
我們在前面的註釋裏可以看出,invalidate方法是不可以在非UI線程執行的,而postInvalidate則可以,結合我們之前學習的Handler知識,我們猜想可能是使用了handler機制,最終仍然是調用了invalidate,那我們來源碼中驗證一下我們的猜想:
public void postInvalidate() {
postInvalidateDelayed(0);
}
public void postInvalidateDelayed(long delayMilliseconds) {
// 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) {
attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
}
}
public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
mHandler.sendMessageDelayed(msg, delayMilliseconds);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_INVALIDATE:
((View) msg.obj).invalidate();
break;
}
}
可以看到,跟我們猜想的一樣,經過一系列列調用,最後使用Handler機制,調用了invalidate方法,也就是說這兩個方法區別基本只在線程之間。
requestLayout
requestLayout也是我麼View中一個很重要的方法,我們在之前講繪製流程的時候也有看到這個方法,那麼我們來探究一下它究竟做了什麼:
public void requestLayout() {
...
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
...
}
這方法與之前invalidateInternal方法類似,也是一層層調用父控件的requestLayout方法,直到ViewRootImpl(ViewGroup沒有複寫該方法,這裏不再關注):
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
又看到了熟悉的scheduleTraversals方法,這裏跟之前invalidate的區別僅僅只是標誌位可能有所改變,後續流程一致。
小結
一般引起invalidate調用的情景如下:
- 調用invalidate()方法,請求重新draw(),但只會繪製調用者本身。
- 調用setSelection()方法 :請求重新draw(),但只會繪製調用者本身。
- 調用setVisibility()方法 : 當View可視狀態在INVISIBLE轉換VISIBLE時會間接調用invalidate方法,繼而繪製該View。當View的可視狀態在INVISIBLE\VISIBLE 轉換爲GONE狀態時會間接調用requestLayout和invalidate方法,同時由於View樹大小發生了變化,所以會請求measure過程以及draw過程,同樣只繪製需要“重新繪製”的視圖。
- 調用setEnabled()方法 : 請求重新draw(),但不會重新繪製任何視圖包括該調用者本身。
- 調用requestFocus方法。請求View樹的draw過程,只繪製“需要重繪”的View。
- invalidate方法只能在UI線程調用,而postInvalidate可以在工作線程調用,不過invalidate效率更高(顯然)
- requestLayout()方法會調用measure過程和layout過程,不會調用draw過程,也不會重新繪製任何View包括該調用者本身
好了,這就是本篇的全部內容了,這裏還是偏理論的東西較多,掌握脈絡即可。下一篇文章我們會來聊一聊Android中的事件分發機制,敬請期待~
如果您覺得文章不錯,可以來我的個人博客查看更多內容~