Android視圖加載流程(5)之View的詳細繪制流程Layout
上一篇文章我們對View的測量(Measure)進行講解了。接著我們開始聊布局(Layout),以下是我們熟悉的performTraversals方法。
private void performTraversals() {
......
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
......
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
......
//本文重點
canvas = mSurface.lockCanvas(dirty);
mView.draw(canvas);
......
}
我們看出ViewRootImpl創(chuàng)建一個canvas,然后mView(DecorView)調(diào)用draw方法并傳入canvas,此方法執(zhí)行具體的繪制工作。與Measure和Layout類似的需要遞歸繪制。
理解圖 本文重點Draw
源碼解讀
Step1 View
由于ViewGroup沒有重寫View的draw方法,我們看下View的draw方法。
public void draw(Canvas canvas) {
......
/*
* 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
......
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
......
// Step 2, save the canvas' layers
......
if (drawTop) {
canvas.saveLayer(left, top, right, top + length, null, flags);
}
......
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
......
if (drawTop) {
matrix.setScale(1, fadeHeight * topFadeStrength);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, right, top + length, p);
}
......
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
......
}
整個的繪制流程分成6步,通過注釋可以知道第2步和第5步(skip step 2 & 5 if possible (common case))可以忽略跳過,我們對剩余4步就行分析。
Step2 View
第一步:對View的背景進行繪制
private void drawBackground(Canvas canvas) {
//獲取xml中通過android:background屬性或者代碼中setBackgroundColor()、setBackgroundResource()等方法進行賦值的背景Drawable
final Drawable background = mBackground;
......
//根據(jù)layout過程確定的View位置來設(shè)置背景的繪制區(qū)域
if (mBackgroundSizeChanged) {
background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
rebuildOutline();
}
......
//調(diào)用Drawable的draw()方法來完成背景的繪制工作
background.draw(canvas);
......
}
draw方法通過調(diào)運drawBackground(canvas);方法實現(xiàn)了背景繪制。
Step3 View
第三步:對View的內(nèi)容進行繪制
protected void onDraw(Canvas canvas) {
}
ViewGroup沒有重寫該方法,view的方法也緊緊是一個空方法而已。大家都知道不同的View是顯示不同的內(nèi)容的,所以這塊必須是子類去實現(xiàn)具體邏輯。
Step4.1 View
第四步:對當(dāng)前View的所有子View進行繪制
protected void dispatchDraw(Canvas canvas) {
}
View的dispatchDraw()方法是一個空方法。這個我們也比較好理解,本身View自身就沒有所謂的子視圖,而擁有子視圖的就是ViewGroup!所以我們可以看下ViewGroup的dispatchDraw
Step4.2 ViewGroup
@Override
protected void dispatchDraw(Canvas canvas) {
......
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
......
for (int i = 0; i < childrenCount; i++) {
......
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
......
// Draw any disappearing views that have animations
if (mDisappearingChildren != null) {
......
for (int i = disappearingCount; i >= 0; i--) {
......
more |= drawChild(canvas, child, drawingTime);
}
}
......
}
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
可見,Viewgroup重寫了dispatchDraw()方法,該方法內(nèi)部會遍歷每個子View,然后調(diào)用drawChild()方法。而drawChild方法內(nèi)部是直接由子視圖調(diào)用draw()方法。
Step5 View
第六步:對View的滾動條進行繪制
protected final void onDrawScrollBars(Canvas canvas) {
//繪制ScrollBars分析不是我們這篇的重點,所以暫時不做分析
......
}
可以看見其實任何一個View都是有(水平垂直)滾動條的,只是一般情況下沒讓它顯示而已。
總結(jié) Summary
通過以上幾個步驟的分析,繪制(draw)的流程基本與measure和layout類似。通過循環(huán)調(diào)用draw來繪制各個子視圖。
- 如果對象為view就不用遍歷子視圖,如果對象為viewGroup就要遍歷子視圖
- View默認不會繪制任何內(nèi)容,子類必須重寫onDraw方法
- View的繪制是借助onDraw方法傳入的Canvas類來進行的
- 默認情況下子View的ViewGroup.drawChild繪制順序和子View被添加的順序一致,但是你也可以重載ViewGroup.getChildDrawingOrder()方法提供不同順序。
額外 extra
我們經(jīng)常在自定義View的時候會遇到兩種方法invalidate和postinvalidate。我們來看看兩個方法與視圖的繪制有什么樣的聯(lián)系呢?
invalidate方法源碼分析
由于ViewGroup并沒有重寫該方法,所以我們直接看View的invalidate
View
//public,只能在UI Thread中使用,別的Thread用postInvalidate方法,View是可見的才有效,回調(diào)onDraw方法,針對局部View
public void invalidate(Rect dirty) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
//實質(zhì)還是調(diào)運invalidateInternal方法
invalidateInternal(dirty.left - scrollX, dirty.top - scrollY,
dirty.right - scrollX, dirty.bottom - scrollY, true, false);
}
//public,只能在UI Thread中使用,別的Thread用postInvalidate方法,View是可見的才有效,回調(diào)onDraw方法,針對局部View
public void invalidate(int l, int t, int r, int b) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
//實質(zhì)還是調(diào)運invalidateInternal方法
invalidateInternal(l - scrollX, t - scrollY, r - scrollX, b - scrollY, true, false);
}
//public,只能在UI Thread中使用,別的Thread用postInvalidate方法,View是可見的才有效,回調(diào)onDraw方法,針對整個View
public void invalidate() {
//invalidate的實質(zhì)還是調(diào)運invalidateInternal方法
invalidate(true);
}
//default的權(quán)限,只能在UI Thread中使用,別的Thread用postInvalidate方法,View是可見的才有效,回調(diào)onDraw方法,針對整個View
void invalidate(boolean invalidateCache) {
//實質(zhì)還是調(diào)運invalidateInternal方法
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}
//這是所有invalidate的終極調(diào)運方法?。。。。?!
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
......
// 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;
//設(shè)置刷新區(qū)域
damage.set(l, t, r, b);
//傳遞調(diào)運Parent ViewGroup的invalidateChild方法
p.invalidateChild(this, damage);
}
......
}
View的invalidate方法最終調(diào)動invalidateInternal方法。而invalidateInternal方法是將要刷新的區(qū)域傳遞給父視圖,并調(diào)用父視圖的invalidateChild。
ViewGroup
public final void invalidateChild(View child, final Rect dirty) {
ViewParent parent = this;
final AttachInfo attachInfo = mAttachInfo;
......
do {
......
//循環(huán)層層上級調(diào)運,直到ViewRootImpl會返回null
parent = parent.invalidateChildInParent(location, dirty);
......
} while (parent != null);
}
這個過程不斷的向上尋找父親視圖,當(dāng)父視圖為空時才停止。所以我們可以聯(lián)想到根視圖的ViewRootImpl
ViewRootImpl
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
......
//View調(diào)運invalidate最終層層上傳到ViewRootImpl后最終觸發(fā)了該方法
scheduleTraversals();
......
return null;
}
返回為空。剛好符合上面的循環(huán)!接著我們看scheduleTraversals()這個方法是不是感覺很熟悉呢?
這就是Android視圖加載流程(3)之ViewRootImpl的UI刷新機制的Step4.2。ViewRootImpl正準備調(diào)用繪制View視圖的代碼。
ViewRootImpl
void scheduleTraversals() {
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
//實現(xiàn)了Runnable接口
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void doTraversal() {
performTraversals();
}
private void performTraversals() {
//測量
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
//布局
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
//繪制
mView.draw(canvas);
}
一組看下來是不是覺得很清晰呢。View調(diào)用invalidate方法,其實是層層往上遞,直到傳遞到ViewRootImpl后出發(fā)sceheduleTraversals方法,然后整個View樹開始進行重繪制任務(wù)。
理解圖:
postInvalidate方法源碼分析
上面也說道invalidate方法只能在UI線程中執(zhí)行,其他需要postInvalidate方法
View
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;
//核心,實質(zhì)就是調(diào)運了ViewRootImpl.dispatchInvalidateDelayed方法
if (attachInfo != null) {
attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
}
}
此方法必須是在視圖已經(jīng)綁定到Window才能使用,即attachInfo是否為空。隨后調(diào)用ViewRootImpl的dispatchinvalidateDelayed
ViewRootImple
public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
mHandler.sendMessageDelayed(msg, delayMilliseconds);
}
Handler亂入!此時ViewrootImpl類的Handler發(fā)送了一條MSG_INVALIDATE消息。哪里接收這個消息呢?
ViewRootImple
final class ViewRootHandler extends Handler {
public void handleMessage(Message msg) {
......
switch (msg.what) {
case MSG_INVALIDATE:
((View) msg.obj).invalidate();
break;
......
}
......
}
}
實質(zhì)上還是在UI線程中調(diào)用了View的invalidate()方法。
postInvalidate是在子線程中發(fā)消息,UI線程接收消息并刷新UI。
理解圖:
常見的引起invalidate方法操作的原因主要有:
- 直接調(diào)用invalidate方法.請求重新draw,但只會繪制調(diào)用者本身。
- 觸發(fā)setSelection方法。請求重新draw,但只會繪制調(diào)用者本身。
- 觸發(fā)setVisibility方法。 當(dāng)View可視狀態(tài)在INVISIBLE轉(zhuǎn)換VISIBLE時會間接調(diào)用invalidate方法,繼而繪制該View。當(dāng)View的可視狀態(tài)在INVISIBLE\VISIBLE 轉(zhuǎn)換為GONE狀態(tài)時會間接調(diào)用requestLayout和invalidate方法,同時由于View樹大小發(fā)生了變化,所以會請求measure過程以及draw過程,同樣只繪制需要“重新繪制”的視圖。
- 觸發(fā)setEnabled方法。請求重新draw,但不會重新繪制任何View包括該調(diào)用者本身。
- 觸發(fā)requestFocus方法。請求View樹的draw過程,只繪制“需要重繪”的View。
requestLayout方法源碼分析
和invalidate類似,層層往上傳遞。
View
public void requestLayout() {
......
if (mParent != null && !mParent.isLayoutRequested()) {
//由此向ViewParent請求布局
//從這個View開始向上一直requestLayout,最終到達ViewRootImpl的requestLayout
mParent.requestLayout();
}
......
}
獲取父類對象,調(diào)用requestlayout(),最后到達ViewRootImpl
ViewRootImpl
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
//View調(diào)運requestLayout最終層層上傳到ViewRootImpl后最終觸發(fā)了該方法
scheduleTraversals();
}
}
與invalidate是不是很像呢?requestLayout()會分別調(diào)用measure和layout過程,在layout過程的時候視圖如果有位置變化,如長寬變化,此時會調(diào)用draw的過程重新繪制,如果視圖沒有位置變化,則不會調(diào)用draw的過程
至此一整塊的視圖加載流程結(jié)束了!
PS:本文
整理自以下文章,若有發(fā)現(xiàn)問題請致郵 caoyanglee92@gmail.com