自由筆記-AndroidView模塊之View繪製流程分析

View繪製流程:

起始點爲ViewRootImp的performTraversals方法。在該方法中調動這3個方法來觸發以下3個流程

performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

performLayout(lp, desiredWindowWidth, desiredWindowHeight);

performDraw();

 

一、 measure()過程

主要作用:爲整個View樹計算實際的大小,即設置實際的高(對應屬性:mMeasuredHeight)和寬(對應屬性:mMeasureWidth)

每個View的控件的實際寬高都是由父視圖和本身視圖決定的。

ViewRoot根對象地屬性mView(其類型一般爲ViewGroup類型)調用measure()方法去計算View樹的大小,回調View/ViewGroup對象的onMeasure()方法.

如果一個View是ViewGroup,它還有子View,那麼它會調用measureChildWithMargins()去測量子View。在該方法裏會調用

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

 

在View的onMeasure()方法裏面進行實際測量並設定View的寬高,通過調用 setMeasuredDimension(h , l)方法設定

 

簡單來說,在最頂層的ViewRoot調用measure方法,然後會觸發回調onMeasure方法,在onMeasure方法中又會觸發調用子View的measure方法來一層層的往下測量。

 

measure—>onMeasure->measure......

 

二、layout佈局過程

 

該過程和measure過程一樣,也是從ViewRoot發起,逐層下發,通過layout觸發onLayout方法,在onLayout方法裏面觸發子控件的layout方法。

1 、layout方法會設置該View視圖位於父視圖的座標軸,即mLeft,mTop,mLeft,mBottom(調用setFrame()函數去實現)接下來回調onLayout()方法

(如果該View是ViewGroup對象,需要實現該方法,對每個子視圖進行佈局) ;

2、如果該View是個ViewGroup類型,需要遍歷每個子視圖chiildView,調用該子視圖的layout()方法去設置它的座標值。

只有ViewGroup需要重寫Onlayout方法,去實現它裏面的子控件的擺放

 

layout->onLayout->layout......

 

三、draw繪製流程

該過程和measure過程也類似。從ViewRoot的draw方法開始,逐層遞歸往下,通過draw方法觸發onDraw方法,但是對於ViewGroup有點區別,draw方法主要流程如下:

1、繪製該View的背景

2、爲顯示漸變框做一些準備操作(見5,大多數情況下,不需要改漸變框)

3、調用onDraw()方法繪製視圖本身 (每個View都需要重載該方法,ViewGroup不需要實現該方法)

4、調用dispatchDraw ()方法繪製子視圖(如果該View類型不爲ViewGroup,即不包含子視圖,不需要重載該方法)

值得說明的是,ViewGroup類已經爲我們重寫了dispatchDraw ()的功能實現,應用程序一般不需要重寫該方法,但可以重載父類

函數實現具體的功能。

5、dispatchDraw()方法內部會遍歷每個子視圖,調用drawChild()去重新回調每個子視圖的draw()方法(注意,這個

地方“需要重繪”的視圖纔會調用draw()方法)。值得說明的是,ViewGroup類已經爲我們重寫了dispatchDraw()的功能

實現,應用程序一般不需要重寫該方法,但可以重載父類函數實現具體的功能。

6、繪製滾動條,前臺等

 

draw->drawBackground->onDraw->dispatchDraw->onDrawForeground

在ViewGroup中,系統已經爲我們實現好了dispatchDraw方法,它會判斷哪些子控件需要重新繪製,在該方法中調用drawChild然後在調用子控件的draw方法,進入到以這個子控件爲root的繪製流程

我們需要重寫onDraw方法來完成我們控件的繪製。

 

 

 

四、觸發重新繪製的三個函數:invalidate(),requsetLaytout()以及requestFocus(),這三個函數最終會調用到ViewRoot中的schedulTraversale()方法,該函數然後發起一個異步消息,消息處理中調用

performTraverser()方法對整個View進行遍歷。

performTraverser方法中分別調用performMeasure,performLayout,performDraw

 

invalidate:請求重繪View樹,即draw()過程,假如視圖發生大小沒有變化就不會調用layout()過程,並且只繪製那些“需要重繪的”

視圖,即誰(View的話,只繪製該View ;ViewGroup,則繪製整個ViewGroup)請求invalidate()方法,就繪製該視圖。

一般引起invalidate()操作的函數如下:

1、直接調用invalidate()方法,請求重新draw(),但只會繪製調用者本身。

2、setSelection()方法 :請求重新draw(),但只會繪製調用者本身。

3、setVisibility()方法 : 當View可視狀態在INVISIBLE轉換VISIBLE時,會間接調用invalidate()方法,繼而繪製該View。

4、setEnabled()方法 :請求重新draw(),但不會重新繪製任何視圖包括該調用者本身。

 

requestLayout:只是對View樹重新佈局layout過程包括measure()和layout()過程,不會調用draw()過程,但不會重新繪製任何視圖包括該調用者本身。

 

requestFocus:請求View樹的draw()過程,但只繪製“需要重繪”的視圖。

 

View的測量過程:

1、MeasureSpec :測量規格類,其中高2位代表測量模式,低30位代表大小,其中,測量模式有以下三種

private static final int MODE_SHIFT = 30;

private static final int MODE_MASK = 0x3 << MODE_SHIFT;

 

/**

* Measure specification mode: The parent has not imposed any constraint

* on the child. It can be whatever size it wants.

*/

public static final int UNSPECIFIED = 0 << MODE_SHIFT; //父容器不對View有任何限制,要多大給多大,一般用於系統內部。

 

/**

* Measure specification mode: The parent has determined an exact size

* for the child. The child is going to be given those bounds regardless

* of how big it wants to be.

*/

public static final int EXACTLY = 1 << MODE_SHIFT;//精準模式,父容器檢查出子View的精確大小,一般對應於指定dp或者match_parent(當父容器的測量模式爲EXACTLY的時候)

 

/**

* Measure specification mode: The child can be as large as it wants up

* to the specified size.

*/

public static final int AT_MOST = 2 << MODE_SHIFT;//最大空間模式,父容器指定了一個大小SpecSize,子View不能夠超過這個大小,具體是多少看子View的實現,一般對應於wrap_content

 

 

2、MeasureSpec和LayoutParams的關係

首先是根View DecorView的MeasureSpec的創建過程,參看ViewRootImp的measureHierarchy方法

private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,

final Resources res, final int desiredWindowWidth, final int desiredWindowHeight);

這裏面host是DecorView,lp是window的layoutParams,desiredWindowWidth和desiredWindowHeight是屏幕的寬高。

 

接着我們看getRootMeasureSpec這個方法,它在measureHierarchy方法裏面執行:

childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);其中這個lp.width是佈局的寬高,他有以下三個值

private static int getRootMeasureSpec(int windowSize, int rootDimension) {

int measureSpec;

switch (rootDimension) {

 

case ViewGroup.LayoutParams.MATCH_PARENT:

// Window can't resize. Force root view to be windowSize.

measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);

break;

case ViewGroup.LayoutParams.WRAP_CONTENT:

// Window can resize. Set max size for root view.

measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);

break;

default:

// Window wants to be an exact size. Force root view to be that size.

measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);

break;

}

return measureSpec;

}

從代碼可以看出,DecorView的測量規格,根據width的取值,按照以下規則確定:

ViewGroup.LayoutParams.MATCH_PARENT:精準模式,就是屏幕的寬高

ViewGroup.LayoutParams.WRAP_CONTENT:最大模式,只要不超過窗口大小就行

固定大小,寫死dp值的時候,也是精準模式,大小爲LayoutParams裏面指定的大小

 

在繪製入口performTraversals()方法初始階段複製爲WindowManager.LayoutParams lp = mWindowAttributes;

final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();

WindowManager.LayoutParams 繼承自ViewGroup.LayoutParams

public LayoutParams() {

super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);

type = TYPE_APPLICATION;

format = PixelFormat.OPAQUE;

}

所以DecorView默認寬高是填滿窗口,是精準模式。當根View的測量規格確認之後,我們就會進行它的子View的測量規格

 

3、子View的MeasureSpec確定

子View的MeasureSpec是根據父view的MeasureSpec確認的,在我們確認了頂級View的MeasureSpec之後,我們開始確認子View的MeasureSpec

一般來講,佈局都是一個容器,需要測量子View,DecorView其實是一個FrameLayout,打開源碼看它的onMeasure方法,會調用 super.onMeasure(widthMeasureSpec, heightMeasureSpec);

進入到FrameLayout的onMeasure方法裏面,進行子View的測量。這裏強調一下,幀佈局和線性佈局都會調用ViewGroup的measureChildWithMargins方法

去測量子View,和相對佈局複雜一些,因爲它的各個子View會相互依賴。而在ViewGroup裏面也有measureChildren的方法去循環調用測量子View的方法

protected void measureChild(View child, int parentWidthMeasureSpec,

int parentHeightMeasureSpec) {

final LayoutParams lp = child.getLayoutParams();

 

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,

mPaddingLeft + mPaddingRight, lp.width);

final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,

mPaddingTop + mPaddingBottom, lp.height);

 

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

}

這裏我們就以幀佈局來分析

protected void measureChildWithMargins(View child,

int parentWidthMeasureSpec, int widthUsed,

int parentHeightMeasureSpec, int heightUsed) {

final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

 

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,

mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin

+ widthUsed, lp.width);

final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,

mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin

+ heightUsed, lp.height);

 

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

}

從代碼可以看出,會調用getChildMeasureSpec來確認子控件的MeasureSpec。入參中,第一個是父控件的測量規格,第二個是父控件已經使用的距離,

包括父View的大小,內邊距和外邊距,第三個是父控件的寬度。接下來根據源碼來分析子View的測量規格的確認。以寬度爲例

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {

//首先獲取到父控件的測量模式和測量大小

int specMode = MeasureSpec.getMode(spec);

int specSize = MeasureSpec.getSize(spec);

//判斷子View還是否有會只的距離,沒有的話就取0

int size = Math.max(0, specSize - padding);

 

int resultSize = 0;

int resultMode = 0;

//判斷父控件的測量模式

switch (specMode) {

// Parent has imposed an exact size on us 如果父控件是精確模式

case MeasureSpec.EXACTLY:

if (childDimension >= 0) {//如果子控件的寬度大於0,那麼子控件的大小就是設定的大小,模式也爲精準模式

resultSize = childDimension;

resultMode = MeasureSpec.EXACTLY;

} else if (childDimension == LayoutParams.MATCH_PARENT) {

// Child wants to be our size. So be it.如果子控件的寬度是填充滿父控件,那麼子控件的寬度就是父控件剩下的寬度,模式也爲精準模式

resultSize = size;

resultMode = MeasureSpec.EXACTLY;

} else if (childDimension == LayoutParams.WRAP_CONTENT) {

// Child wants to determine its own size. It can't be 如果子控件的寬度是包裹內容大小,那麼子控件的大小也先設定爲父控件的大小,確保子控件不會比父控件大,模式爲最大填充模式

// bigger than us.

resultSize = size;

resultMode = MeasureSpec.AT_MOST;

}

break;

 

// Parent has imposed a maximum size on us //如果父控件的模式是最大填充模式

case MeasureSpec.AT_MOST:

if (childDimension >= 0) {//如果子控件的寬度大於0,那麼子控件的大小就是設定的大小,模式也爲精準模式

// Child wants a specific size... so be it,

resultSize = childDimension;

resultMode = MeasureSpec.EXACTLY;

} else if (childDimension == LayoutParams.MATCH_PARENT) {

// Child wants to be our size, but our size is not fixed.

// Constrain child to not be bigger than us.

//如果子控件的寬度是填充滿父控件,那麼子控件的寬度就是父控件剩下的寬度,模式也爲最大填充模式

resultSize = size;

resultMode = MeasureSpec.AT_MOST;

} else if (childDimension == LayoutParams.WRAP_CONTENT) {

// Child wants to determine its own size. It can't be 如果子控件的寬度是包裹內容大小,那麼子控件的大小也先設定爲父控件的大小,確保子控件不會比父控件大,模式爲最大填充模式

// bigger than us.

resultSize = size;

resultMode = MeasureSpec.AT_MOST;

}

break;

 

// Parent asked to see how big we want to be //如果父控件是未確認大小模式

/**

* Always return a size of 0 for MeasureSpec values with a mode of UNSPECIFIED

*/

//View.sUseZeroUnspecifiedMeasureSpec 說明下這個變量,從註釋中可以看出,如果是UNSPECIFIED,那麼它會永遠返回0,但是在安卓M(6.0)之後

//這個值會返回true,官方註釋是說,在安卓6.0之後,可以有一個hints的size對於UNSPECIFIED的MeasureSpecs,在滾動的容器裏面,左邊的

//子控件會知道父控件的期望size,例如,list items可以是他們父控件的三分之一的大小

static boolean sUseZeroUnspecifiedMeasureSpec = false;

case MeasureSpec.UNSPECIFIED:

if (childDimension >= 0) {

// Child wants a specific size... let him have it //如果子控件的寬度大於0,那麼子控件的大小就是設定的大小,模式也爲精準模式

resultSize = childDimension;

resultMode = MeasureSpec.EXACTLY;

} else if (childDimension == LayoutParams.MATCH_PARENT) {

// Child wants to be our size... find out how big it should

// be

resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;

resultMode = MeasureSpec.UNSPECIFIED;

} else if (childDimension == LayoutParams.WRAP_CONTENT) {

// Child wants to determine its own size.... find out how

// big it should be

resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;

resultMode = MeasureSpec.UNSPECIFIED;

}

//如果子控件不是確認的大小,那麼6.0之前,子View的size會是0,模式是UNSPECIFIED,6.0之後子View的size是父控件的確認size,模式是UNSPECIFIED

break;

}

//noinspection ResourceType

return MeasureSpec.makeMeasureSpec(resultSize, resultMode);

}

4、子控件的測量

當View的測量規格確認之後,就會進入到View的measure方法然後進入onMeasure方法

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),

getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));

}

通過setMeasuredDimension會設定View的最終測量寬高

public static int getDefaultSize(int size, int measureSpec) {

int result = size;

int specMode = MeasureSpec.getMode(measureSpec);

int specSize = MeasureSpec.getSize(measureSpec);

 

switch (specMode) {

case MeasureSpec.UNSPECIFIED://如果是不確認模式,大小爲size,getSuggestedMinimumWidth方法的返回值

result = size;

break;

case MeasureSpec.AT_MOST:

case MeasureSpec.EXACTLY:

result = specSize;//如果是另外兩種模式,就將之前的測量規格的值設定給最終的值。實際情況下,每一個控件都會自己重寫自己的onMeasure方法,根據內容重新設定寬高的值

break;

}

return result;

}

這裏在說下 getSuggestedMinimumWidth

protected int getSuggestedMinimumWidth() {

return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());

}

如果沒有設定背景,那麼寬度就是mMinWidth,對應android:mMinWidth屬性的值,沒有設定就是0;如果有設定背景,那麼獲取背景的最小寬度和mMinWidth比較取最大值

public int getMinimumWidth() {

final int intrinsicWidth = getIntrinsicWidth();

return intrinsicWidth > 0 ? intrinsicWidth : 0;

}

該方法返回背景Drawable的原始寬度,這裏並不是所有Drawable會有原始寬度BitmapDrawable有值,ShapeDrawable是0

 

我們可以看出,如果子控件是warp_content模式,那麼它的測量大小是parenSize和AT_MOST,這樣和mathc_content是一樣的,

實際上,在具體的控件的onMeasure方法,最後的大小取值不一定就是parenSize。這裏我們以TextView爲例,來看看它的onMeasure方法

int widthMode = MeasureSpec.getMode(widthMeasureSpec);

int heightMode = MeasureSpec.getMode(heightMeasureSpec);

int widthSize = MeasureSpec.getSize(widthMeasureSpec);

int heightSize = MeasureSpec.getSize(heightMeasureSpec);

int width;

int height;

首先取出測量模式和寬高,在定義實際的寬高變量

if (widthMode == MeasureSpec.EXACTLY) {

// Parent has told us how big to be. So be it.如果是精準模式,實際寬就是測量的寬

width = widthSize;

}

如果不是精準模式而是最大填充模式,根據text的內容,計算出實際的大小然後和測量的大小比較取最小的那個

if (widthMode == MeasureSpec.AT_MOST) {

width = Math.min(widthSize, width);

}

高度也是一樣的代碼,然後就設定最終測量結果setMeasuredDimension(width, height);

所以,在我們自定義控件的時候,如果要使得warp_content生效,我們必須對onMeasure方法進行重寫,參考TextView,定義好實際寬度高度

對於AT_MOST模式,我們要判斷是使用之前測量規格里面的大小還是使用實際寬度大小。

 

5、獲取控件的寬高

通過源碼我們知道,在onResume方法執行之後,視圖纔開始進行測量,所以在onResume方法獲取不到View的寬高值,所以需要通過以下途徑獲取

@Override

public void onWindowFocusChanged(boolean hasFocus) {

super.onWindowFocusChanged(hasFocus);

}

 

view.post(runnable)

 

ViewTreeObserver observer = view.getViewTreeObserver();

observer.addOnGlobalLayoutListener(

 

//在這裏面要記得移除掉這個監聽器

view.getViewTreeObserver().removeGlobalOnLayoutListener(this)

);

 

View的Layout過程

layout過程比測量過程簡單的多,主要首先會通過setFrame方法來確認View的四個頂點的位置,即mLeft,mRight,mTop,mBottom,確認好四個值之後會開始

執行layout方法。

public final int getWidth() {

return mRight - mLeft;

}

public final int getMeasuredWidth() {

return mMeasuredWidth & MEASURED_SIZE_MASK;

}

正常情況下,在layout過程,會調用setFrame方法來確認四個頂點的位子,而傳入的值,一般都是left,left+mMeasuredWidth。所以在最終的情況下

getWidth會和getMeasuredWidth一樣。

 

View的draw過程

1、繪製背景 background.draw(canvas)

2、繪製自己 onDraw

3、繪製children(dispatchDraw)

4、繪製裝飾 onDrawScrollBars

5、通知WMS繪製完成mWindowSession.finishDrawing(mWindow);

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章