寫在前面
今天和老爸去醫院了,老爸最近腰疼,幸好沒什么事,各位也要多多愛惜自己的身體,等身體出了問題就來不及了。好了,閑話到此為止,話說《開發藝術探索》里面不少東西我早就看了,但是因為自己當時水平有限,而且有很多也看不大懂,所以看了忘是挺正常的事。不過感覺隨著工作經驗的增長,以前大學里上過《操作系統》、《計算機網絡》、《數據結構》等一些課都被串到了一起,感覺很好,而且愈發能感覺到自己的不足了。這周的文主要算是一篇讀書筆記吧,記一下讀《開發藝術探索》第四章和View相關的筆記,至于ViewGroup我覺得還需要過段時間,在沉淀一下再來和各位一起探索一下。而且說真的,這篇文我也是硬著頭皮寫下來的,因為看源碼看著看著發現我疑問越來越多,很多都是現階段暫時無法解決的,不過問題總是一個一個去解決的,先過一遍View的measure流程再說其他的。
ViewRoot & DecorView
ViewRoot對應于ViewRootImpl類,在ViewRootImpl類中有如下一段注釋:
/**
* The top of a view hierarchy, implementing the needed
* protocol between View and the WindowManager.
*/
View層最頂部,實現了View和WindowManager間所需的協議。這個類很重要,但是我現在對其理解也不是很深(一是因為讀源碼無力,一是沒有系統的閱讀過源碼),所以就拿書上的話來說:它是連接WindowManager和DecorView的紐帶,View的三大流程均是通過ViewRoot來完成的。在ActivityThread中(同樣很有意思的一點是ActivityThread是一個類,并非線程什么的,當然了如果哪一天我讀通了這一塊回來和各位分享的),當Activity對象被創建完畢后,會將DecorView添加到Window中,同時會創建ViewRootImpl對象,并將ViewRootImpl對象和DecorView建立關聯。
接下來直接放結論,盡快進入正題:
View的繪制流程是從ViewRoot的performTraversals開始的,它經過measure、layout和draw三個過程才能最終將一個View繪制出來。
MeasureSpec
在之前Android自定義View你需要知道的一些東西中我已經簡要的介紹過MeasureSpec了,當然在這不敢再麻煩各位去看了,繼續簡介一下,熟悉的可以跳過這段。
首先咱直接看他的注釋,看看注釋是怎么解釋這玩意的:
/**
* A MeasureSpec encapsulates the layout requirements passed from parent to child.
* Each MeasureSpec represents a requirement for either the width or the height.
* A MeasureSpec is comprised of a size and a mode. There are three possible
* modes:
* <dl>
* <dt>UNSPECIFIED</dt>
* <dd>
* The parent has not imposed any constraint on the child. It can be whatever size
* it wants.
* </dd>
*
* <dt>EXACTLY</dt>
* <dd>
* 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.
* </dd>
*
* <dt>AT_MOST</dt>
* <dd>
* The child can be as large as it wants up to the specified size.
* </dd>
* </dl>
*
* MeasureSpecs are implemented as ints to reduce object allocation. This class
* is provided to pack and unpack the <size, mode> tuple into the int.
*/
最上面那段話的大概意思是一個MeasureSpec封裝了一個從父(控件)傳給子(控件)的布局要求。每個MeasureSpec代表的不是寬就是高的要求。一個MeasureSpec包含了一個尺寸和一個(測量)模式,這些模式如下:
UNSPECIFIED
父(容器)對子(控件)沒有任何限制,子控件可以想要多大就有多大。EXACTLY
父容器已經決定了子控件的精確尺寸,子控件將會變成被給出的約束大小,不管他自己想要變成啥樣。AT_MOST
子控件可以和和他想要的規格尺寸一樣大。
最后一段話解釋了一下為什么這么實現,暫時不需要咱關心這些。看完了注釋基本上已經對MeasureSpec有了一個基本的認識了。但是看完這段注釋,應該會產生一個疑惑,在注釋中說MeasureSpec是父傳遞給子的,那么最頂層的DecorView的MeasureSpec是如何來的呢?《開發藝術探索》上說是在ViewRootImpl的measureHierarchy方法中創建的,主要代碼如下:
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth,lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight,lp.width);
performMeasure(childWidthMeasureSpec,childHeightmeasureSpec);
其中desiredWindowWidth和desiredWindowHeight就是屏幕的尺寸,追蹤下getRootMeasureSpec()源碼看看:
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;
}
可以看到該方法中對于match_parent、wrap_content和其余情況的處理:
LayoutParams.MATCH_PARENT:采用精確模式測量,大小是窗口大小
LayoutParams.WRAP_CONTENT:大小不定,最大是窗口大小
其他:結合我們平時寫xml和代碼的經驗,此處其他應該就是我們制定了大小(比如100dp),大小為指定大小。
View的MeasureSpec的獲取
上面的MeasureSpec注釋里說了,是從父傳遞到子的,那么View很明顯是一個子布局,讓我們上ViewGroup里找找child是如何獲取MeasureSpec的:
/**
* Ask all of the children of this view to measure themselves, taking into
* account both the MeasureSpec requirements for this view and its padding.
* We skip children that are in the GONE state The heavy lifting is done in
* getChildMeasureSpec.
*
* @param widthMeasureSpec The width requirements for this view
* @param heightMeasureSpec The height requirements for this view
*/
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
老規矩,先讓我這個渣渣來翻譯一下注釋……
遍歷View去測量他們自身,既要考慮MeasureSpec的要求又要考慮padding。跳過GONE狀態的(不測量這個狀態的View),更繁重的事在getChildMeasureSpec完成。
很明顯,咱得看一下getChildMeasureSpec方法了:
/**
* Does the hard part of measureChildren: figuring out the MeasureSpec to
* pass to a particular child. This method figures out the right MeasureSpec
* for one dimension (height or width) of one child view.
*
* The goal is to combine information from our MeasureSpec with the
* LayoutParams of the child to get the best possible results. For example,
* if the this view knows its size (because its MeasureSpec has a mode of
* EXACTLY), and the child has indicated in its LayoutParams that it wants
* to be the same size as the parent, the parent should ask the child to
* layout given an exact size.
*
* @param spec The requirements for this view
* @param padding The padding of this view for the current dimension and
* margins, if applicable
* @param childDimension How big the child wants to be in the current
* dimension
* @return a MeasureSpec integer for the child
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
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) {
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) {
// 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
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
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;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
咱繼續翻譯一下……
困難的事咱來做:算出MeasureSpec傳遞給
一個child。這個方法為一個子view的一個尺寸(高或寬)算出正確的MeasureSpec。
他的目的是組合MeasureSpec和LayoutParams的信息讓child獲取最好的可能結果。例如:如果這個view知道他的尺寸(因為測量模式是EXACTLY),這個child已經指出了他的LayoutParams,他想和他的父容器有一樣的尺寸(match_parent),這時父容器應該要求child布局成被給的精確尺寸。
從上面的注釋我們可以了解到,子View的MeasureSpec并不是由其自身的LayoutParams決定的,而是由其父容器和其自身的LayoutParams一同決定的。接下來看一下代碼,看看究竟是怎么操作的:
首先從ViewGroup的寬/高的MeasureSpec中獲取到對應的specMode,然后根據不同的mdoe去執行不同的操作
EXACTLY:如果子View指定了精確值的大小,那么測量模式是EXACTLY,尺寸是傳入的childDimension,這兩個值將會被打包成一個MeasureSpec并返回。如果childDimension等于MATCH_PARENT,那么就將上面獲取到的自身尺寸和EXACTLY模式打包成一個MeasureSpec打包并返回。如果childDimension等于WRAP_CONTENT,那么將自身尺寸賦給孩子的尺寸,讓子View的尺寸不要大于父容器就行了,將這個尺寸和AT_MOST模式打包并返回。
之后的AT_MOST和UNSPECIFIED測量模式和EXACTLY的套路差不多,就不一一看過去了,《Android開發藝術探索》上總結了一個表格:
|columns:childLayoutParams/rows:parentSpecMode|EXACTILY|AT_MOST|UNSPECIFIED
| ------------ |:-----------------: | ------: |
|dp/px|EXACTLY/childSize|EXACTLY/childSize|EXACTLY/childSize|
|match_parent|EXACTLY/parentSize|AT_MOST/parentSize|UNSPECIFIED/0|
|wrap_content|AT_MOST/parentSize|AT_MOST/parentSize|UNSPECIFIED/0|
最后要強調一句和《開發藝術探索》上一樣的話,以上的表格并非是經驗總結,而是將代碼具現為一個表格罷了。
View的measure流程
終于填完了幾個比較重要的坑,可以來講講View的measure流程了。View的測量是由measure()方法實現的 ,在measure()方法中會去調用onMeasure()方法,看一下View的onMeasure()方法的實現:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
關于這個方法有一些要注意的地方,在以前的Android自定義View你需要知道的一些東西都有說過,而且這個方法的注釋里都有寫你需要注意的東西,感興趣的可以去看看,這里就不贅述了。以上方法調用了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:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
看得出來,這里只是進行了一些簡單的判斷和賦值操作,如果測量模式是AT_MOST和EXACTLY:那么就返回View測量后的大小,如果是UNSPECIFIED模式那么就返回傳入的size值。接下來看看傳入的這個size值是怎么獲取的:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
從以上代碼我們可以看出,如果view有背景則從最小寬度和background的寬度中返回較大的那個,如果沒有背景則返回最小寬度,這個最小寬度我們可以通過xml或者setMinimumWidth來設置,默認為0。
接下來通過一個問題來回憶一下今天所了解的東西:在View中使用wrap_content為什么效果和match_parent效果一樣?
首先這個問題需要去ViewGroup里看看,因為View的MeasureSpec是從ViewGroup中傳遞而來的,前面我們畫的那張表格就是處理代碼的具現,我們可以直接查表~
查表可知在match_parent和wrap_content這兩種情況下傳遞給View的size都是parentSize(忽略UNSPECIFIED的情況),那么再看看View有沒有對這兩種情況做特殊的處理就行了。而在上面的getDefaultSize方法里可以看到AT_MOST和EXACTLY兩種模式返回值都是相同的,因此在自定義繼承于View的控件時需要我們去重寫onMeasure()方法來處理wrap_content的情況。
讀到這你可能會發現View和ViewGroup這倆貨根本分不開,事實也是如此,但是在這我就不繼續記我的筆記了,因為有些事我也沒想明白,所以留待以后填坑吧(立了個flag)。
后記
還有一個ViewGroup的measure流程這里并沒有繼續了,想留著以后再來填這個坑吧。記得原來讀《Android開發藝術探索》都是:“哦,原來是這樣的” 狀態,現在讀則是會對一些代碼的流程產生疑惑,而這些疑惑以我現在水平還是有些難以解決的。不過學習就是不斷完善和不斷有新的問題的過程,如同我們初中高中所學的物理一樣,很多時候我們學到的只是相對正確的知識,但是在以后不斷學習的過程中,我們可以不斷的糾正自己以前的錯誤和帶有局限的理解。