Android View的繪制流程
源碼版本為 Android 10(Api 29)夹姥,不同Android版本可能有一些差別
View 的繪制從哪里開始
在《Activity常見問題》的 Activity 在 onResume 之后才顯示的原因是什么袖瞻? 部分中我們知道了View是在 onResume()
回調(diào)之后才顯示出來的,顯示過程主要是通過 WindowManagerImpl#addView()
-> WindowManagerGlobal#addView()
-> ViewRootImpl#setView()
這個過程煞躬,我們再次看一下 ViewRootImpl#setView()
的代碼(核心代碼):
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
// 調(diào)用 requestLayout() 方法肛鹏,進行布局(包括measue逸邦、layout、draw)
requestLayout();
mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
// 通過調(diào)用 Session 的 addToDisplay() 方法
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
mTempInsets);
setFrame(mTmpFrame);
}
}
}
有這樣一行 requestLayout()
龄坪,表示請求布局昭雌,我們界面的繪制也是從這一行代碼開始的,接下來健田,我們就來看一下跟蹤一下這段代碼(ViewRootImpl#requestLayout()
):
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
調(diào)用 ViewRootImpl#scheduleTraversals()
方法:
@UnsupportedAppUsage
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
在 scheduleTraversals()
方法中通過 mChoreographer.postCallback()
方法發(fā)送一個要執(zhí)行的實現(xiàn)了 Runnable
的 TraversalRunnable
的對象 mTraversalRunnable
:
mChoreographer.postCallback()
方法內(nèi)部就是通過Handler
機制-
TraversalRunnable
為ViewRootImpl
的內(nèi)部類final class TraversalRunnable implements Runnable { @Override public void run() { doTraversal(); } }
調(diào)用 ViewRootImpl#doTraversal()
方法
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
調(diào)用 ViewRootImpl#performTraversals()
方法烛卧,該方法中關(guān)于繪制的代碼
private void performTraversals(){
……
// 方法測量組件的大小
performMeasure(childWidthMeasureSpec,childHeightMeasureSpec);
……
// 方法用于子組件的定位(放在窗口的什么地方)
performLayout(lp,desiredWindowWidth,desiredWindowHeight);
……
// 繪制組件內(nèi)容
performDraw();
……
}
而在performMeasure()
、performLayout()
和performDraw()
方法的調(diào)用過程可以用下面的圖來表示:
從圖中可以看出系統(tǒng)的View類已經(jīng)寫好了measure()
妓局、layout()
和draw()
方法:
- 在系統(tǒng)View類中
measure()
方法用了final
修飾总放,不能被重寫(我覺得這應(yīng)該是google不想讓開發(fā)者更改measure()
方法里面的邏輯而設(shè)計的,但是開發(fā)者有時又有需求需要自己測量好爬,所以提供了onMeasure()
方法可以重寫)局雄;
代碼 - 在系統(tǒng)View類中的
layout()
方法在調(diào)用onLayout()
方法前調(diào)用了setFrame()
方法,這個方法作用是判斷View的位置是否發(fā)生改變存炮,如果沒有發(fā)生改變炬搭,就不調(diào)用onLayout()
方法,主要是為了提高性能穆桂,在View類中onLayout()
方法是空實現(xiàn)宫盔,這是因為view沒有子類,而當在自定義的控件如果是直接繼承ViewGroup時就必須重寫onLayout()
方法享完;
代碼 - 在系統(tǒng)View類中的
draw()
方法灼芭,開發(fā)者一般不會重寫,因為當我們?nèi)绻貙?code>draw()時般又,就需要按照系統(tǒng)定義好的步驟一步一步的畫彼绷,否則會顯示不出來,相對來說比較麻煩茴迁。而如果我們實現(xiàn)onDraw()
方法寄悯,我們只要關(guān)注我們畫的內(nèi)容即可(畫出來的內(nèi)容就是顯示到界面的內(nèi)容);
代碼 - 當開發(fā)者在自定義控件時一般只需重寫
onMeasure()
笋熬、onLayout()
和onDraw()
方法就可以了热某。
測量 -- measure
源碼追蹤
ViewRootImpl
中的performMeasure()
方法:
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
調(diào)用了mView
的measure()
方法,mView
就是 DecorView
胳螟,是通過 ViewRootImpl#setView()
傳入進來的昔馋,也就是調(diào)用了FrameLayout#measure()
方法,FrameLayout
繼承ViewGroup
糖耸,ViewGroup
沒有重寫也不能重寫measure()
方法盗迟,所以最終調(diào)用的是View
類中的measure()
方法:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
...
}
在View類中的measure()
中系統(tǒng)幫我們做了很多的處理并且不想讓開發(fā)者重寫measure的邏輯项棠,所以使用了final
修飾符進行修飾,并且調(diào)用了onMeasure()
方法引镊,所以在Activity
中View樹的測量過程中骂因,最終是從FrameLayout#onMeasure()
方法開始的,FrameLayout
的onMeasure()
方法如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
......
// 判斷孩子控件的Visibility屬性,如果為gone時,就跳過希坚,因為gone屬性不占用空間
if (count > 1) {// 判斷是否有孩子控件
for (int i = 0; i < count; i++) {
// 通過LayoutParams參數(shù)獲取孩子控件的margin、padding值
...
// 調(diào)用孩子控件的measure()方法測量自身大小
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
在FrameLayout
的onMeasure()
方法中陵且,先是獲取了孩子控件的個數(shù)裁僧,然后獲取每一個孩子控件并判斷visibility
屬性;最終調(diào)用孩子View#measure()
方法測量自身大小慕购。
當開發(fā)者有需要重新測量控件時聊疲,只需要重寫onMeasure()
方法即可,系統(tǒng)在View的measure()
方法中會回調(diào)onMeasure()
方法沪悲,使測量值生效获洲。
下面是繼承自View時重寫的onMeasure()
方法,我就只是簡單的將寬和高都設(shè)置成500:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(500,500);
}
下面是繼承ViewGroup
時重寫的onMeasure()
方法殿如,將所有孩子控件的寬和高的和計算出來作為父控件的寬和高贡珊,最終調(diào)用setMeasuredDimension()
方法設(shè)置值:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMeasure = 0,heightMeasure = 0;
// 直接調(diào)用系統(tǒng)方法測量每一個孩子控件的寬和高
measureChildren(widthMeasureSpec,heightMeasureSpec);
/**
* 系統(tǒng)在調(diào)用measureChildren(widthMeasureSpec,heightMeasureSpec)的過程中,
* 如果孩子控件依然是ViewGroup類型的涉馁,那么又會調(diào)用measureChildren()方法飞崖,否則會調(diào)用
* child.measure(childWidthMeasureSpec, childHeightMeasureSpec)方法測量每一個孩子控件
* 的寬和高,直到所有的孩子控件都測量完成谨胞。
* 這就可以說明measure的過程可以看成是一個遞歸的過程。
*/
// 獲取孩子控件的個數(shù)
int childCount = getChildCount();
// 循環(huán)測量每一個孩子控件的寬和高蒜鸡,得到的和就作為控件的寬和高
for (int i = 0; i < childCount; i++) {
View view = getChildAt(i);
// 獲取每一個孩子的寬和高
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
// 把每一個孩子控件的寬和高加上
widthMeasure += width;
heightMeasure += height;
}
// 調(diào)用setMeasuredDimension()方法保存寬和高胯努,表示measure的結(jié)束
setMeasuredDimension(widthMeasure,heightMeasure);
}
另外在measure的過程中還可能會用到MeasureSpec類(View的內(nèi)部類):
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
// 父控件不沒有對子施加任何約束,子可以是任意大蟹攴馈(也就是未指定)
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
// 父控件決定子的確切大小叶沛,表示width和height屬性設(shè)置成match_parent或具體值
public static final int EXACTLY = 1 << MODE_SHIFT;
// 子最大可以達到的指定大小,當設(shè)置為wrap_content時忘朝,模式為AT_MOST
public static final int AT_MOST = 2 << MODE_SHIFT;
/*
* 通過模式和大小創(chuàng)建一個測量規(guī)范
* @param size the size of the measure specification
* @param mode the mode of the measure specification
* @return the measure specification based on size and mode
*/
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int makeSafeMeasureSpec(int size, int mode) {
if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
return 0;
}
return makeMeasureSpec(size, mode);
}
/*
* 獲取模式
*/
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
/*
* 獲取大小
*/
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
...
}
MeasureSpecs
使用了二進制去減少對象的分配灰署,用最高的兩位數(shù)來表示模式(Mode),剩下的30位表示大小(size)局嘁。
Mode有三種:UNSPECIFIED
(未指定溉箕,沒有約束,可以任意大小)悦昵、EXACTLY
(精確肴茄,表示match_parent或者具體的大小值)、AT_MOST
(最大值但指,表示wrap_content)
measure總結(jié):
View的
measure()
方法被final
修飾寡痰,子類不可以重寫抗楔,但可以通過重寫onMeasure()
方法來測量大小,當然也可以不重寫onMeasure()
方法使用系統(tǒng)默認測量大欣棺埂连躏;如果想要讓自己設(shè)置的值生效,就必須調(diào)用
setMeasuredDimension()
方法設(shè)置寬和高贞滨;-
如果在
Activity
的onCreate()
方法或onResume(
)方法里面直接調(diào)用getWidth()/getHeight()
入热、getMeasureWidth()/getMeasureHeight()
獲取控件的大小得到的結(jié)果很可能是0,因為在onCreate()
或onResume()
的時候系統(tǒng)還沒有調(diào)用measure()
方法(getMeasureWidth()
和getMeasureHeight()
的賦值在View的setMeasuredDimension()
方法中疲迂,所以在調(diào)用完View的setMeasuredDimension()
方法之后getMeasuredWidth()
和getMeasuredHeight()
就已經(jīng)有值了才顿。而getWidth()
和getHeight()
要在onLayout()
方法完成之后才會被賦值),如果一定要在onCreate()
方法或onResume()
方法里面獲取控件的大小尤蒿,可以通過以下方法得到:view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { int width = view.getWidth(); int height = view.getHeight(); view.getViewTreeObserver().removeOnGlobalLayoutListener(this); } });
通過
setMeasuredDimension()
方法設(shè)置的值并不一定就是控件的最終大小郑气,組件真正的大小最終是由setFrame()
方法決定的,該方法一般情況下會參考measure出來的尺寸值腰池;子視圖View的大小是由父容器View和子視圖View布局共同決定的尾组;
如果控件是
ViewGroup
的子類,那就必須測量每一個孩子控件的大小示弓,可以調(diào)用系統(tǒng)的measureChildren()
方法測量跨跨,也可以自己測量囱皿;Android系統(tǒng)對控件的測量過程可以看做是一個遞歸的過程耕渴。
擺放 -- layout
源碼追蹤
ViewRootImpl
中的performLayout()
方法:
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
...
final View host = mView;
...
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
...
for (int i = 0; i < numValidRequests; ++i) {
final View view = validLayoutRequesters.get(i);
Log.w("View", "requestLayout() improperly called by " + view +
" during layout: running second layout pass");
view.requestLayout();
}
}
代碼中的host
是View樹中的根視圖(DecroView
)添诉,也就是最外層容器艾帐,容器的位置安排在左上角(0,0),其大小默認會填滿 mContentParent
容器。該方法作用是確定孩子控件的位置葡公,所以該方法只針對ViewGroup
容器類蒲凶,最終調(diào)用了View
的layout()
方法確定每一個孩子控件的位置:
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
在layout()
中確定位置之前會判斷是否需要重新測量控件的大小,如果需要,就會調(diào)用onMeasure()
方法重新測量控件蹬癌,接下來執(zhí)行 setOpticalFrame()
或 setFrame()
方法確定自身的位置與大小董济,這一步并不會繪制出來封豪,只是將控件位子和大小值保存起來缘琅;接著調(diào)用onLayout()
方法鸽心,在View中onLayout()
方法是空實現(xiàn)糯景。onLayout()
方法的作用是當當前控件是容器控件時,那就必須重寫onLayout()
方法確定每一個孩子控件的位置省骂,而當孩子控件還是ViewGroup
的子類時钞澳,繼續(xù)調(diào)用onLayout()
方法,直到所有的孩子控件都有了確定的位置和大小,這個過程和measure一樣奄侠,也可以看做是一個遞歸的過程卓箫。下面是FrameLayout#onLayout()
方法源碼:
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
final int count = getChildCount();
...// 遍歷所有的孩子控件
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
... // 判斷控件的visible屬性書否為gone,如果為gone就不占用位置
... // 計算childLeft弯洗、childTop旅急、childRight、childBottom牡整,確定位置
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
layout總結(jié):
- View的布局邏輯是由父View藐吮,也就是ViewGroup容器布局來實現(xiàn)的。因此逃贝,我們?nèi)绻远xView一般都無需重寫onLayout()方法谣辞,但是如果自定義一個ViewGroup容器的話,就必須實現(xiàn)onLayout()方法沐扳,因為該方法在ViewGroup類中是抽象的泥从,ViewGroup的所有子類必須實現(xiàn)onLayout()方法(如果我們定義的容器控件是繼承FrameLayout或其他已經(jīng)繼承了ViewGroup類的容器控件時,如果沒有必要可以不用實現(xiàn)onLayout()方法沪摄,因為FrameLayout類中已經(jīng)實現(xiàn)了)躯嫉;
- 如果view控件使用了gone屬性時,在onLayout()方法遍歷中就會跳過當前的View杨拐,因為gone屬性表示不占用位置祈餐;
- 當layout()方法執(zhí)行完成之后,調(diào)用getHeight()/getWidth()方法就能夠得到控件的寬和高的值了哄陶;
- View的layout過程和measure過程類似帆阳,都可以看做是一個遞歸的過程;
- 在Activity中屋吨,layout的過程是從DecorView控件開始的舱痘。
繪制 -- draw
源碼追蹤
ViewRootImpl
中的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);
}
...
}
private void draw(boolean fullRedrawNeeded) {
...
if (!drawSoftware(surface, attachInfo, yoff, scalingRequired, dirty)) {
return;
}
...
}
/**
* @return true if drawing was succesfull, false if an error occurred
*/
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 = mSurface.lockCanvas(dirty);
if (!canvas.isOpaque() || yoff != 0) {
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
}
mView.draw(canvas);
...
} finally {
surface.unlockCanvasAndPost(canvas);
}
...
return true;
}
canvas
對象是從surface
中獲取到的,surface
是中提供了一套雙緩存機制离赫,這樣就提高了繪圖的效率.通過代碼可以看到最后調(diào)用了mView
的draw()
方法,mView
是ecorView
塌碌,也就是FrameLayou
t渊胸,在FrameLayout
和ViewGroup
中都是沒有重寫draw()
方法的,所以最終調(diào)用的是View
中的draw()
方法:
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background 繪制視圖View的背景
* 2. If necessary, save the canvas' layers to prepare for fading 保存畫布canvas的邊框參數(shù)
* 3. Draw view's content 繪制視圖View的內(nèi)容(調(diào)用了onDraw()方法)
* 4. Draw children 繪制當前視圖View的子視圖(調(diào)用dispatchDraw()方法)
* 5. If necessary, draw the fading edges and restore layers 繪制邊框的漸變效果并重置畫布
* 6. Draw decorations (scrollbars for instance) 繪制前景台妆、滾動條等修飾
*/
// Step 1, draw the background, if needed 繪制視圖View的背景
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
...
// Step 2, save the canvas' layers 保存畫布canvas的邊框參數(shù)
saveCount = canvas.getSaveCount();
// Step 3, draw the content 繪制視圖View的內(nèi)容(調(diào)用了onDraw()方法)
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children 繪制當前視圖View的子視圖(調(diào)用dispatchDraw()方法)
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers 繪制邊框的漸變效果并重置畫布
...
canvas.restoreToCount(saveCount);
// Step 6, draw decorations (foreground, scrollbars) 繪制前景翎猛、滾動條等修飾
onDrawForeground(canvas);
}
由以上代碼可以看得到胖翰,系統(tǒng)在View
類中的draw()
方法已經(jīng)將背景、邊框切厘、修飾等都繪制出來了萨咳,而且在第三步和第四步的時候調(diào)用了onDraw()
方法和dispatchDraw()
方法,這樣開發(fā)者在自定義控件的時候就只需要重寫onDraw()
方法或者dispatchDraw()
方法疫稿,也就是只需要關(guān)注最終顯示的內(nèi)容就可以了培他,而不需要去繪制其他的修飾。
在View
里面的onDraw()
方法和dispatchDraw()
方法都是空實現(xiàn)遗座,也就是留給開發(fā)者去實現(xiàn)里面的具體邏輯舀凛。同時,在開發(fā)者實現(xiàn)onDraw()
方法和dispatchDraw()
方法時也可以不用去繪制其他的修飾了途蒋。需要說明一點猛遍,View
中的draw()
方法并不是和measure()
方法一樣被final
修飾,draw()
方法沒有被final
修飾号坡,所以是可以重寫的懊烤,但是當我們重寫draw()
方法時,必須和系統(tǒng)中View
的draw()
方法一樣宽堆,一步一步的實現(xiàn)腌紧,否則就不能將控件繪制出來,所以在自定義控件的時候日麸,一般都是重寫onDraw()
或dispatchDraw()
方法寄啼。
如果自定義控件是繼承至View
時,就重寫onDraw()
方法代箭,在onDraw()
方法中繪制的結(jié)果就是最終顯示的結(jié)果墩划。
以下代碼就是在界面上以坐標(150,150)繪制了一個半徑為35的紅色實心圓:
public class CircleView extends View {
Paint paint;
public CircleView(Context context) {
this(context, null);
}
public CircleView(Context context, AttributeSet attrs) {
super(context, attrs);
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.FILL);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawCircle(150,150,35,paint);
}
}
如果自定義控件是繼承至ViewGroup
時,就重寫dispatchDraw()
方法嗡综,這里直接查看系統(tǒng)FrameLayout
的dispatchDraw()
方法:
@Override
protected void dispatchDraw(Canvas canvas) {
final int childrenCount = mChildrenCount;
...
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);
}
}
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
...
}
在代碼里面調(diào)用了drawChild(canvas, child, drawingTime)
方法用來繪制孩子:
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
而在drawChild()
方法中就是調(diào)用了view
的draw(Canvas canvas, ViewGroup parent, long drawingTime)
方法(三個參數(shù)的方法)來繪制自己乙帮,如果孩子控件還是ViewGroup
的子類,又會重新調(diào)用drawChild()
方法遞歸處理极景,直到所有的孩子控件繪制完成察净,也就表示控件繪制完成了。其實這也和measure盼樟、layout的過程一樣氢卡,可以看做是一個遞歸的過程。
看一下view
中三個參數(shù)的draw(Canvas canvas, ViewGroup parent, long drawingTime)
方法:
/**
* This method is called by ViewGroup.drawChild() to have each child view draw itself.
* 這個方法是在ViewGroup的drawChild()方法中調(diào)用晨缴,用來繪制每一個孩子控件自身译秦。
* This draw() method is an implementation detail and is not intended to be overridden or
* to be called from anywhere else other than ViewGroup.drawChild().
* 這個方法除了在ViewGroup的drawChild()方法中被調(diào)用外,不應(yīng)該在其它任何地方去復(fù)寫或調(diào)用該方法,它屬于ViewGroup筑悴。
* 而這個方法最終也會調(diào)用View的draw(canvas)一個參數(shù)的方法來進行繪制们拙。
*/
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
...
if (!hasDisplayList) {
// 調(diào)用computeScroll()方法,這個方法是用來與Scroller類結(jié)合實現(xiàn)實現(xiàn)動畫效果的
computeScroll();
sx = mScrollX;
sy = mScrollY;
}
...
if (!hasDisplayList) {
// Fast path for layouts with no backgrounds
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
// 繼續(xù)調(diào)用dispatchDraw()方法遞歸處理
dispatchDraw(canvas);
} else {
// 調(diào)用View的draw(canvas)一個參數(shù)的方法
draw(canvas);
}
} else {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
((HardwareCanvas) canvas).drawRenderNode(renderNode, null, flags);
}
...
return more;
}
了解更多關(guān)于Scroller類的相關(guān)內(nèi)容阁吝,可以查看《 Android中的Scroller類》這篇博客砚婆。
draw總結(jié):
-
View
繪制的畫布canvas
是從surface
對象中獲得,而最終也是繪制到surface
中去突勇。surface
提供了一個雙緩存機制装盯,可以提高繪制的效率; - 系統(tǒng)在
View
類中的draw()
方法已經(jīng)將背景与境、邊框验夯、修飾等都繪制出來了,如果在自定義View時直接繼承View
時是重寫draw()
方法摔刁,就必須和系統(tǒng)中View
的draw()
方法一樣挥转,一步一步的實現(xiàn),否則就不能將控件展示出來共屈; - 因為自定義控件一般重寫
onDraw()
方法绑谣,所以每一個控件都會繪制滾動條和其他的修飾; - 自定義
View
如果直接繼承制View
時拗引,需要重寫onDraw()
方法借宵,在onDraw()
中繪制的內(nèi)容就是最終展示到界面的內(nèi)容,自定義View如果是直接繼承ViewGroup
矾削,那就重寫dispatchDraw()
方法壤玫,繪制ViewGroup
的孩子控件; - Android繪制的過程和measure哼凯、layout一樣欲间,可以看做是一個遞歸的過程。