大家都知道Android View繪制過程包含Measure、Layout、Draw三個主要的過程,這個過程看似簡單,但是在應(yīng)用的時候,很多同學(xué)還是不能很好的運(yùn)用。我希望這篇文章可以把其中的一部分——Measure——講的更加清晰一點(diǎn)。
Measure過程是對View大小的測量過程,相比其他兩個過程,Measure的邏輯更加復(fù)雜。Measure過程是RootView調(diào)用performTraversals()方法時執(zhí)行的。我們只關(guān)心“看的到”的部分。Measure的過程由View樹上的View在onMeasure方法中調(diào)用子View的measure方法完成的。有點(diǎn)繞,不過,對于自定義View或者自定義ViewGroup來說,我們需要關(guān)心下面的內(nèi)容:
- 自定義View:覆寫onMeasure方法,計(jì)算合適的大小,并將結(jié)果通過
setMeasuredDimension()
方法保存結(jié)果。 - 自定義ViewGroup:除了完成上面所說的工作外,還需要調(diào)用子View的measure方法,確保每個子View都正確的測量。
自定義View
先貼一個算是通用的自定義View onMeasure方法實(shí)現(xiàn):
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(measure(widthMeasureSpec, true), measure(heightMeasureSpec, false));
}
protected int measure(int measureSpec, boolean WOH) {
int size = MeasureSpec.getSize(measureSpec);
int mode = MeasureSpec.getMode(measureSpec);
int measured;
if (mode == MeasureSpec.EXACTLY) {
measured = size;
} else {
int measureMinimum = WOH ? getMinimumMeasureWidth() : getMinimumMeasureHeight();
// 根據(jù)內(nèi)容計(jì)算最小值
// measureMinimum = Math.max(measureMinimum, MIN_CONTENT_SIZE);
if (WOH) {
measureMinimum = Math.max(measureMinimum, measureMinimum + getPaddingLeft() + getPaddingRight());
} else {
measureMinimum = Math.max(measureMinimum, measureMinimum + getPaddingTop() + getPaddingBottom());
}
measured = measureMinimum;
if (mode == MeasureSpec.AT_MOST) {
measured = Math.min(measured, size);
}
}
return measured;
}
上面的代碼對于繪制類的自定義View(主要作用在于展示更豐富的圖形樣式,而不在于布局)比較實(shí)用,以上代碼計(jì)算大小的步驟:
-
先取View的期望的最小寬/高,這個最小值由View的內(nèi)容和設(shè)置決定。
什么是期望的最小寬/高?
View的大小的應(yīng)該至少滿足內(nèi)容的顯示需求,比如要顯示一個10個漢字的View,那么這個View的期望最小寬/高就是“當(dāng)前文字樣式下10個漢字的寬/高 + padding”。
-
根據(jù)MeasureSpec的模式,確定最終的寬/高。具體邏輯是:
- MeasureSpec.EXACTLY:以MeasureSpec的size為準(zhǔn)。
- MeasureSpec.AT_MOST:取期望和MeasureSpec的size的最小值。
- MeasureSpec.UNSPECIFIED:取期望值。
MeasureSpec的三種模式,下面還會專門說明。所以暫時先不要糾結(jié)上面邏輯的理由。
調(diào)用
setMeasuredDimension()
方法保存結(jié)果。
總結(jié)
自定義View的Measure過程通用處理方法:首先要確定View需要的(顯示內(nèi)容)最小/合適大小,然后根據(jù)MeasureSpec的三種模式確定最終的measured尺寸。
MeasureSpec
之所以沒有開始就講這個類,是因?yàn)樵谥v之前我希望大家先對自定義View的Measure過程有個印象。
定義:MeasureSpec封裝了父View對子View的布局需求。所以這個類表示了一種需求,需求,需求。
MeasureSpec由mode和size兩個部分組成,這兩個部分通過位計(jì)算儲存到一個int類型中,(怎么個結(jié)構(gòu)這里就不細(xì)說了,看源碼吧),通過getMode()和getSize()獲取。這兩個方法加上構(gòu)造方法基本就是MeasureSpec的全部API了。
size很好理解,下面把三種mode翻譯成普通話:(以下“我”代表父View,“你”代表子View,“size”表示MeasureSpec的size)
- MeasureSpec.EXACTLY:我需要你的大小和size一樣。
- MeasureSpec.AT_MOST:你可以是(根據(jù)內(nèi)容確定的或是)任意大小,但是不能超過size。
- MeasureSpec.UNSPECIFIED:你可以是(根據(jù)內(nèi)容確定的或是)任意大小。
以上“我”代表父View,“你”代表子View,“size”表示MeasureSpec的size。
對于子View來說,在onMeasure方法中拿到MeasureSpec之后,就要根據(jù)自己的期望和MeasureSpec的需求確定最終大小。而且一般情況下,對于繪制類的自定義View,通過第一節(jié)的方法都可以完成Measure過程。
對于父View來說,首先它也是“爺爺View”的子View,所以也是要在onMeasure方法中處理,拿到MeasureSpec之后,不僅要通過自己的期望和“爺爺View”的需求確定大小,還要負(fù)責(zé)子View的measure過程,它需要(在自己的onMeasure方法中)
- 通過調(diào)用子View的View.measure(int, int)方法向子View傳遞自己的合適的需求。
- 通過調(diào)用子View的
View.measure(int, int)
方法向子View傳遞自己的合適的需求。 - 通過調(diào)用子View的
View.measure(int, int)
方法向子View傳遞自己的合適的需求。 - 通過調(diào)用子View的
View.measure(int, int)
方法向子View傳遞自己的合適的需求。
具體父View應(yīng)該怎么做,請繼續(xù)往下看。
總結(jié)
MeasureSpec表是父View對子View的measure需求。對于自定義View來說,需要在onMeasure中考慮MeasureSpec的值,從而確定最終measured尺寸;對于自定義ViewGroup而言,還需要通過調(diào)用子View的View.measure(int, int)方法向子View傳遞自己的合適的需求。
LayoutParams
在進(jìn)入自定義ViewGroup的Measure過程之前,還需要考慮一個因素。LayoutParams,直譯過來就是“布局參數(shù)”。上面講了MeasureSpec是父View傳遞給子View的需求,而LayoutParams,是子View的布局參數(shù)。作用是什么呢?向父View傳遞需求。兩個需求是有差別的,一般對于子View來說,只需要關(guān)心MeasureSpec,而LayoutParams是ViewGroup需要考慮的因素。這也是為什么在這里討論LayoutParams的原因。
LayoutParams(這里指ViewGroup.LayoutParams)相比MeasureSpec更加簡單,封裝了兩個值,width和height。這兩個值是開發(fā)者對子View大小的約束,對ViewGroup來說,這兩個值表示“子View希望ViewGroup如何Measure自己”。覺得暈沒關(guān)系,繼續(xù)往下看。
width和height的取值類型一致,共有三種:
- MATCH_PARENT (-1): 表示子View希望自己和父控件的width/height一致。一般情況下,會導(dǎo)致onMeasure方法中得到mode為EXACTLY,size為父View寬/高的MeasureSpec。
- WRAP_CONTENT (-2): 表示子View希望自己的width和height由自己的內(nèi)容決定。一般情況下,會導(dǎo)致onMeasure方法中得到mode為AT_MOST,size為父View寬/高的MeasureSpec。
- 任意非負(fù)整數(shù): 表示子View希望自己的width和height是確切的這個值。一般情況下,會導(dǎo)致onMeasure方法中得到mode為EXACTLY,size為該值的MeasureSpec。
對于ViewGroup來說,LayoutParams的取值表達(dá)了子View對自己width和height的期望。
Q: Android View的size不是View的onMeasure確定的嗎?為什么要向父View傳遞期望?
A: 第一節(jié)有提到,View的onMeasure方法要根據(jù)onMeasure的參數(shù)(兩個MeasureSpec)最終確定。而在ViewGroup知道View的類型之前,是不知道如何向子View傳遞MeasureSpec的(ViewGroup也是很講道理的,MeasureSpec表達(dá)了ViewGroup對子View的measure期望,但也不能隨便傳啊。)。LayoutParams就是ViewGroup確定向子View傳遞怎樣的MeasureSpec的確定因素之一。
ViewGroup根據(jù)LayoutParams的width/height和自己的設(shè)計(jì)(每個特定的ViewGroup類型,比如LinearLayout、FrameLayout)來確定向子類傳遞的MeasureSpec。
注意:這里說的是【LayoutParams的width/height】而不是【LayoutParams】,因?yàn)樘囟ǖ腣iewGroup是可以自己定義屬于自己的LayoutParams的,比如LinearLayout.LayoutParams定義了gravity,RelativeLayout定義了toLeftOf、above等特定的布局參數(shù),這些參數(shù)在ViewGroup的onMeasure方法內(nèi)也都會考慮到,但總的來說還是width/height在起作用,尤其是對于大部分自定義ViewGroup來說。舉個例子:
RelatIveLayout里面的子View,即便將LayoutParams.width設(shè)置為WRAP_CONTENT,但是如果同時將這個子View的alignParentLeft、alignParentRight設(shè)置為true的話,子View在onMeasure里面拿到的widthMeasureSpec的mode依然是EXACTLY。
因?yàn)镽elatIveLayout根據(jù)以上兩個alignParentLeft/Right屬性判斷,這個子View是希望MATCH_PARENT的。
總結(jié)
LayoutParams表示子View對自己布局(包含measure和layout)的期望,ViewGroup.LayoutParams僅包含width和height兩個值。ViewGroup在確定自己對某個子View的MeasureSpec時,一般需要考慮這個子View的LayoutParams參數(shù)。
自定義ViewGroup
如果自定義ViewGroup是繼承自Framework內(nèi)的幾個Layout類,那么Measure過程大部分情況下不需要關(guān)心。因?yàn)椋?/p>
- 如果自定義ViewGroup的目的是為了自定義自View的布局規(guī)則,那么請直接繼承ViewGroup類。
- 如果自定義ViewGroup的目的是為了包裝業(yè)務(wù),那么不需要涉及布局規(guī)則的定義,就不需要關(guān)心Measure和Layout過程了。
- 如果兩者都有,那么參見第一條。
這里我們討論直接繼承自ViewGroup的情況。根據(jù)剛才的結(jié)論,自定義ViewGroup類,自然是要干涉子View的布局邏輯。比如:按比例布局、按某種圖形布局、自動折行等等。
自定義ViewGroup就是上面討論的“父View”,所以它需要:
-
通過調(diào)用子View的
View.measure(int, int)
方法向子View傳遞自己的合適的需求。
這句話是第6次出現(xiàn)了,這很重要。
你至少應(yīng)該從中得到以下信息:(以下VG表示“自定義ViewGroup”)
- 自定義ViewGroup要在onMeasure方法中調(diào)用所有需要布局的子View(有些View,比如不需要顯示,可以不測量)的measure方法。
- 自定義ViewGroup要向子View傳遞Measure需求。
- 自定義ViewGroup向子View傳遞的MeasureSpec是代表自己對子View的Measure需求,可以并且一般也都和onMeasure方法的參數(shù)(“爺爺View的需求”)的MeasureSpec不同。
- 自定義ViewGroup在確定MeasureSpec時,要考慮到子View的LayoutParams參數(shù),從而確定合適的MeasureSpec。
所以自定義ViewGroup的Measure過程的關(guān)鍵就是向子View傳遞合適的需求,就是對每個子View構(gòu)建合適的MeasureSpec。
以FrameLayout為例
還是很抽象對嗎?讓我們來看一下Framework內(nèi)置的Layout是怎么做的。這里討論FrameLayout,因?yàn)镕rameLayout的measure過程相對簡單,不至于跑題。當(dāng)然,這個過程也是可以覆蓋剛才我們討論的整個過程和原理的(事實(shí)上,上面討論的原理是普適性的)。如果你有興趣可以再繼續(xù)研究下其他Layout的measure過程,我認(rèn)同 Read the ** source code. 是最有效最基礎(chǔ)的學(xué)習(xí)方法。
源碼就不貼了,太占地方,下面要和大家一起分析的是版本號為23的SDK中的源碼,可以打開AndroidStudio對照著看。
onMeasure步驟
對每個View進(jìn)行measure,調(diào)用ViewGroup.measureChildWithMargin方法。這里傳入的MeasureSpec是使用ViewGroup的默認(rèn)實(shí)現(xiàn)計(jì)算(注1)得到的。同時記錄所有子View的width/height的最大值。
取子View的最大width/height,考慮自己的minHeight/minWidth、Foreground和Padding,得到新的最大值,作為自己的暫時measuredWidth/measuredHeight。
結(jié)合onMeasure的參數(shù)MeasureSpec(父ViewGroup的measure需求),得到最終measuredWidth、measuredHeight,調(diào)用setMeasuredDimension方法。(measuredState見注2)
判斷:如果onMeasure參數(shù)中有非EXACTLY mode的MeasureSpec(某個方向或者某兩個方向尺寸不確定),并且子View的LayoutParams中,有MATCH_PARENT的值。如果不滿足,結(jié)束onMeasure;否則繼續(xù)。
-
對LayoutParams中有MATCH_PARENT的值的View重新measure。對設(shè)置了MATCH_PARENT值的這個方向,使用經(jīng)第1、2、3步驟處理后最終確定的自己(FrameLayout)的width/height作為size,EXACTLY作為mode的MeasureSpec。
因?yàn)榈?步measure子View的時候,沒有考慮到1、2、3步驟之后最終確定的自己的大小,所以對于設(shè)置了MATCH_PARENT的View,無法給出確切的值,所以要再次調(diào)用子View的measure方法,傳入正確的值。
注1:ViewGroup.getChildMeasureSpec方法。根據(jù)從父ViewGroup獲取到的MeasureSpec和子View的LayoutParams,得到合適的MeasureSpec。
注2:關(guān)于measuredState,目前應(yīng)用很狹窄,暫時可以忽略。
它的場景只有一種,涉及到的值常量也只有一個:MEASURED_STATE_TOO_SMALL。表示measure過程中最終確定的size小于measure過程中計(jì)算得到的需要的(內(nèi)容)size。
在上面的步驟中,上述第3步中會調(diào)用FrameLayout的View.resolveSizeAndState方法,如果暫時的measuredWidth/measuredHeight小于父ViewGroup提供的MeasureSpec的size并且MeasureSpec的mode為AT_MOST的話,將在最終得到的measuredSize的高8位保存MEASURED_STATE_TOO_SMALL(0x01000000)。
舉個例子:
RelativeLayout > FrameLayout > View三層布局,F(xiàn)rameLayout的LayoutParams為WRAP_CONTENT,里面View的寬高設(shè)為超過RelativeLayout的值,F(xiàn)rameLayout的measureState就包含MEASURED_STATE_TOO_SMALL。
總結(jié)
自定義ViewGroup的measure過程除了要確定自身的measuredSize;同時要向子View傳遞合適的MeasureSpec,保證子View正確measure,在確定MeasureSpec時,通常要考慮到每個子View的LayoutParams。
實(shí)例分析
理論是要結(jié)合實(shí)踐的,下面通過兩個實(shí)例,來分別分析下自定義View和自定義ViewGroup的measure。
自定義View——FixRatioImageView
FixRatioImageView繼承自Image,作用是根據(jù)image source的比例確定View的大小,要求有一邊為EXCATLY(MATCH_PARENT或固定數(shù)值)。開發(fā)中會遇到需要固定比例顯示的圖片資源,有些時候是需要有固定的布局需求的。ImageView其實(shí)已經(jīng)設(shè)計(jì)了屬性adjustViewBounds
但是在第三方系統(tǒng)上的兼容性并不好,所以我們通過干涉ImageView的onMeasure方法,實(shí)現(xiàn)這個需求。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mRatio == 0) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width = widthSize, height = heightSize;
if (widthMode != MeasureSpec.EXACTLY && heightMode != MeasureSpec.EXACTLY) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
} else if (widthMode == MeasureSpec.EXACTLY) {
height = (int) (width / mRatio + 0.5f);
} else if (heightMode == MeasureSpec.EXACTLY) {
width = (int) (height * mRatio + 0.5f);
}
setMeasuredDimension(width, height);
}
mRatio表示固定的寬高比:width/height
上面的代碼,先判斷是否有寬或者高為EXACTLY,如果都不是,那么使用ImageView的measure邏輯,否則根據(jù)mRatio的值,計(jì)算另一邊的值。有一邊為EXCATLY這個前提不能適用所有情況,但是大部分需求都能滿足了。
使用方法如下:
<com.kyleduo.androidcustomview.view.FixRatioImageView
android:id="@+id/fix_ratio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
android:src="@drawable/fixratio"
app:fr_rate="10.56338"/>
自定義ViewGroup——內(nèi)容左對齊的KeyValueItem
不知道大家有沒有做過這種列表:
每個列表項(xiàng)分為標(biāo)題和內(nèi)容,標(biāo)題和內(nèi)容都是左對齊,并且所有內(nèi)容都要左對齊。但你遇到這種列表你會想到怎么做呢?
如果你能想到通過自定義ViewGroup實(shí)現(xiàn),那一定是極好的。很明顯,對于這類需要有特殊布局要求的開發(fā)需求,自定義ViewGroup應(yīng)該是首先想到的方法。
思路是這樣的,自定義KeyValueItem,使用靜態(tài)變量儲存左側(cè)Title的最大寬度,然后measure右側(cè)Message的時候,減去左側(cè)寬度,保證正確測量。onLayout里面,右側(cè)Message layout的左邊緣在Title最大寬度右側(cè)。
是不是很清晰?嗯,還有個問題需要考慮,因?yàn)檫@個ViewGroup肯定是要復(fù)用的,而最大寬度通過靜態(tài)變量保存,那么多個頁面進(jìn)行復(fù)用的時候,就會出現(xiàn)寬度被污染的問題。分析需求,這種列表是在同一個ViewGroup下布局的,也就是說他們有一個公共的父View,那么我們就可以用父View作為Key,保存這個最大寬度,當(dāng)Key變化時對最大寬度進(jìn)行清空。直接引用父View當(dāng)然不行,我們?nèi)「竀iew對象的hashCode()作為key。
如果你向我一樣這個列表每個Activity中只出現(xiàn)一次,那么直接用Context的hashCode也可以。
這里貼出onMeasure和onLayout的源碼:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
throw new IllegalArgumentException("width must be exactly");
}
if (getParent() != null) {
int parentHash = getParent().hashCode();
if (parentHash != sMaxKey) {
sMaxKey = parentHash;
sMaxTitleWidth = 0;
}
}
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int maxTitleWidth = widthSize / 2 - space * 2;
int maxChildHeight = 0;
for (int i = 0; i < getChildCount(); i++) {
int childWidth = widthSize;
View child = getChildAt(i);
if (child == mTitleTv) {
childWidth = maxTitleWidth;
} else if (child == mContentTv) {
childWidth = widthSize - sMaxTitleWidth - space * 3; // |-[title]-space-[content]-space|
}
child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.AT_MOST), heightMeasureSpec);
if (child == mTitleTv) {
int width = child.getMeasuredWidth();
if (width > sMaxTitleWidth) {
sMaxTitleWidth = width;
}
}
int height = child.getMeasuredHeight();
if (height > maxChildHeight) {
maxChildHeight = height;
}
}
setMeasuredDimension(widthSize, Math.max(maxChildHeight + space * 2, mMinHeight));
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int centerY = getMeasuredHeight() / 2;
int left = getPaddingLeft() + space;
int right = getMeasuredWidth() - space;
if (mTitleTv != null) {
mTitleTv.layout(left, centerY - mTitleTv.getMeasuredHeight() / 2, left + mTitleTv.getMeasuredWidth(), centerY + mTitleTv.getMeasuredHeight() / 2);
left += sMaxTitleWidth + space;
}
if (mContentTv != null) {
mContentTv.layout(left, centerY - mContentTv.getMeasuredHeight() / 2, right, centerY + mContentTv.getMeasuredHeight() / 2);
}
}
Q: 代碼里并沒有使用for循環(huán)之類的語句遍歷子View?
A: KeyValueListItem并不是一個通用的Layout控件,里面只有兩個子View并且是確定的兩個子View,而且他們的Layout結(jié)構(gòu)也是確定的,所以可以直接針對這兩個對象進(jìn)行Layout。
如果是自定義類似標(biāo)簽云這種包含平等子View的ViewGroup,那么遍歷是必然的。
最終的效果是這樣的:
如果查看LayoutBounds,可以看到也是非常干凈。
總結(jié)
Measure過程是對View尺寸的測量過程,View通過onMeasure方法確定自己的尺寸,ViewGroup在確定自己尺寸的同時,要正確調(diào)用子View的measure()方法,讓子View正確測量。自定義View和ViewGroup的時候,也是通過onMeasure方法完成measure過程。
這篇文章分別討論了自定義View、自定義ViewGroup的measure方法,也解釋了MeasureSpec和LayoutParams的含義以及他們是如何在View的measure過程中提起作用的;然后分析了FrameLayout的onMeasure方法實(shí)現(xiàn);最后通過兩個實(shí)例分析在場景用應(yīng)用了measure過程的技術(shù)要點(diǎn)。
關(guān)于Android View的measure,就講到這里吧,希望對大家有幫助。