android view绘制过程
應(yīng)用程序窗口內(nèi)部所包含的視圖對象的實際類型為DecorView。DecorView類繼承了View類,是作為容器(ViewGroup)來使用的,它的實現(xiàn)如圖1所示:
每一個應(yīng)用程序窗口的視圖對象都有一個關(guān)聯(lián)的ViewRoot對象,這些關(guān)聯(lián)關(guān)系是由窗口管理器WindowManagerImpl來維護的,如圖:
?簡單來說,ViewRoot相當于是MVC模型中的Controller,它有以下職責:
?? ? ? ?1. 負責為應(yīng)用程序窗口視圖創(chuàng)建Surface。
?? ? ? ?2. 配合WindowManagerService來管理系統(tǒng)的應(yīng)用程序窗口。
?? ? ? ?3. 負責管理、布局和渲染應(yīng)用程序窗口視圖的UI。
ViewRoot創(chuàng)建過程:
總體過程如下:
繪制過程:
創(chuàng)建
??? 首先,View公有的構(gòu)造函數(shù)的重載形式就有四種:
- View(Context context)??? 通過代碼創(chuàng)建view時使用此構(gòu)造函數(shù),通過context參數(shù),可以獲取到需要的主題,資源等等。
- View(Context context, AttributeSet attrs)??? 當通過xml布局文件創(chuàng)建view時會使用此構(gòu)造函數(shù),調(diào)用了3個參數(shù)的構(gòu)造方法。
- View(Context context, AttributeSet attrs, int defStyleAttr)???? 通過xml布局文件創(chuàng)建view,并采用在屬性中指定的style。這個view的構(gòu)造函數(shù)允許其子類在創(chuàng)建時使用自己的style。調(diào)用了下面四參的構(gòu)造方法。
- View(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)??? 該構(gòu)造函數(shù)可以通過xml布局文件創(chuàng)建view,可以采用theme屬性或者style資源文件指定的style。
??? 參數(shù):
- Context : view運行的上下文信息,從中可以獲取到當前theme,資源文件等信息。
- AttributeSet: xml布局文件中view標簽下指定的屬性集合。
- defStyleAttr: 當前theme中的一條屬性,它包含一條指向theme資源文件中style的引用。默認值為0。
- defStyleRes: 一個style資源文件的標示,表示style的ID,當值為0或者找不到對應(yīng)的theme資源時候采用默認值。
?
??? 綜上所述,單參的構(gòu)造函數(shù)從代碼創(chuàng)建view,其余都調(diào)用四參的構(gòu)造函數(shù)根據(jù)xml布局文件創(chuàng)建view。我們可以在不同的地方指定屬性值,例如:
直接在xml標簽中中指定的attrs值,可以從AttributeSet中獲取。
- 通過在標簽屬性“style”中指定的資源文件。
- 默認的defStyleAttr。
- 默認的defStyleRes。
- 當前theme中的默認值。
??? 構(gòu)造函數(shù)的代碼過長,就不在這里貼了,主要進行的工作是:獲取各項系統(tǒng)定義的屬性,然后根據(jù)屬性值初始化view的各項成員變量和事件。
??? 一般情況下,我們自定義view的時候,根據(jù)實際情況重寫構(gòu)造函數(shù)時,如果只從code創(chuàng)建,則只用實現(xiàn)單參數(shù)的即可。如果需要從xml布局文件中創(chuàng)建,則需要實現(xiàn)單參數(shù)和一個多參數(shù)的就好了,因為多參數(shù)的默認調(diào)用了四參數(shù)的構(gòu)造函數(shù);然后再獲取到自定義的屬性進行處理就OK了。
??? 至此,view的創(chuàng)建以及初始化工作完畢,然后開始繪制view的工作。那么Android系統(tǒng)是如何對view進行繪制的呢?
繪制
??? 在activity獲取到焦點后,會請求Android Framework根據(jù)它的布局文件進行繪制,activity需要提供所繪布局文件的根節(jié)點,然后對布局的樹結(jié)構(gòu)一邊遍歷一邊進行繪制。我們都知道,ViewGroup是View的子類,它可以擁有若干子view,它的很多操作和view相同,不同的是ViewGroup負責繪制其子節(jié)點,而view則負責繪制其自身。整個遍歷過程從上到下,在整個過程中,需要進行大小測量(measure函數(shù))和定位(layout函數(shù)),然后再進行繪制。下面我們來看這些工作是如何進行的:
測定尺寸
? ? 在Android中,所有view被組織成樹狀結(jié)構(gòu),最頂層measure的主要工作就是負責遞歸測量出整個view樹結(jié)構(gòu)的尺寸大小,每個View的控件的實際寬高都是由父視圖和本身視圖決定的。
? ? 在研究源碼之前,我先從整體上概況一下整個遞歸調(diào)用過程。從根view開始,使用measure方法中計算整個view樹的大小,在該方法中調(diào)用子view的onMeasure方法。在onMeasure中主要進行兩個工作:
? ? 根view通常就是一個ViewGroup,需要計算子view尺寸。首先獲取到所有子view,然后調(diào)用measureChildWithMargins方法來計算子view的尺寸。在這個方法中調(diào)用了子view的measure方法。下面我們來看具體源碼。
?
? ? 首先在measure方法中確定view的大小。這個方法被定義為final類型,不可被子類重寫。在View中有一個靜態(tài)內(nèi)部類MeasureSpec封裝了父view要傳遞給子View的布局參數(shù),由size 和 mode共同組成。size即是大小,mode表示模式。(其實就是一個int值高2位表示mode,低30位表示size). mode總共有三種模式:
- UNSPECIFIED:父view并未指定子view的大小,可隨意根據(jù)開發(fā)人員需求指定view大小。
- EXACTLY: 父view嚴格指定了子view的大小
- AT_MOST: 子view的大小不超過該值
??? 方法接收的兩個參數(shù)widthMeasureSpec和heightMeasureSpec表示view的寬高,由上一層父view計算后傳遞過來。view大小的測量工作在標紅的onMeasure方法中進行。我們在自定義view時往往需要重寫該方法,根據(jù)傳入的view大小以及其內(nèi)容來設(shè)定view最終顯示的尺寸。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));}??? 重寫該方法時,我們需要調(diào)用setMeasuredDimension這個方法來存儲已經(jīng)測量好的尺寸(這里默認使用getDefalutSize),只有在調(diào)用過此方法后,才能通過getMeasuredWidth方法和getMeasuredHeight方法獲取到尺寸。同時,我們要保證最后得到的尺寸不小于view的最小尺寸。我們需要注意的是,setMeasuredDimension方法必須在OnMeasure方法中調(diào)用,否則會拋出異常。
? ? OK,measure方法至此完畢。然而,我們可以發(fā)現(xiàn)真正測量view大小的工作并不在此方法中進行,這里僅僅是一個測量框架,根據(jù)各種不同的情況進行判斷,完成一些必要的步驟。這些步驟是必須的也是無法被開發(fā)者更改的,需要根據(jù)情況自定義的工作放在了onMeasure中由開發(fā)者完成。這樣既保證了繪制流程的執(zhí)行,又靈活的滿足了各種需求,是典型的模板方法模式。
??? 由于一個父view下可能有多個子view,所以measure方法不僅僅執(zhí)行一次,而是在父view(viewGroup)中獲取到所有子view,然后遍歷調(diào)用子view的measure方法。
?
定位
??? 當view的大小已經(jīng)設(shè)定完畢,則需要確定view在其父view中的位置,也就是把子view放在合理的位置上。因為只有ViewGroup才包含子view,所以一般我們說起父view,肯定是在說ViewGroup。完成布局工作主要分為兩部分,也是遞歸實現(xiàn)的:
? ? 下面來看具體源碼,父view調(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; // 判斷是否布局是否發(fā)生過改變,是否需要重繪。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); // 確定view在布局中的位置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;}??? 該方法接收四個參數(shù)是子view相對于父view而言的上下左右位置。然而我們發(fā)現(xiàn)其中調(diào)用到的onLayout方法默認的實現(xiàn)是空的。這是因為確定view在布局的位置這個操作應(yīng)該由Layout根據(jù)自身特點來完成。任何布局的定義都要重寫其onLayout方法,并在其中設(shè)定子view的位置。
繪制
??? 在進行完測定尺寸和定位之后,終于可以開始繪制了。這里的工作仍是通過遞歸來完成的。view調(diào)用draw方法來進行繪制,里面調(diào)用onDraw來繪制自身,如果還有子view則需要調(diào)用dispatchDraw來繪制子view。
? ? 繪制需要調(diào)用draw方法,總共分為六個步驟:
??? 我們選擇常規(guī)的繪制過程,不介紹2,5步驟。
??? 第一步,調(diào)用drawBackground繪制背景圖案:
private void drawBackground(Canvas canvas) {final Drawable background = mBackground; // 獲取到當前view的背景,是一個drawable對象 if (background == null) {return;}if (mBackgroundSizeChanged) {// 判斷背景大小是否變化,是則設(shè)置背景邊界background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);mBackgroundSizeChanged = false;mPrivateFlags3 |= PFLAG3_OUTLINE_INVALID;}// Attempt to use a display list if requested.if (canvas.isHardwareAccelerated() && mAttachInfo != null&& mAttachInfo.mHardwareRenderer != null) {mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);final RenderNode displayList = mBackgroundRenderNode;if (displayList != null && displayList.isValid()) {setBackgroundDisplayListProperties(displayList);((HardwareCanvas) canvas).drawRenderNode(displayList);return;}} // 調(diào)用drawable對象的繪制方法完成繪制final int scrollX = mScrollX;final int scrollY = mScrollY;if ((scrollX | scrollY) == 0) {background.draw(canvas);} else {canvas.translate(scrollX, scrollY);background.draw(canvas);canvas.translate(-scrollX, -scrollY);}}??? 第三步,調(diào)用onDraw方法繪制view的內(nèi)容,由于不同的view內(nèi)容不同,所以需要子類進行重寫。
??? 第四步,繪制子view,這里仍然需要當前l(fā)ayout的dispatchDraw方法來完成對各子view的繪制。
??? 第六步,繪制滾動條。
??? 通常情況下,我們自定義view,復(fù)寫onDraw方法來繪制我們定義的view的內(nèi)容即可。
?
總結(jié)
??? 通過研究view類的源碼,我們可以發(fā)現(xiàn),在整個view的繪制流程中我們需要完成測定尺寸,布局定位,繪制這三個步驟。Android在設(shè)計過程中,將固定不變的流程設(shè)計為不可更改的模板方法,然而需要根據(jù)不同情況而定的內(nèi)容則交給開發(fā)者來完成重寫,在模板方法中調(diào)用即可。這樣設(shè)計即保證了整個流程的完整,又給開發(fā)工作帶來了靈活。同時,在類中又根據(jù)不同情況定義了不同的flag,來滿足不同情況的繪制需求,以后有機會再具體研究這些flag的具體意義。
總結(jié)
以上是生活随笔為你收集整理的android view绘制过程的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 说说Java中原子性,可见性与指令重排序
- 下一篇: 语音识别之特征参数提取(一)