做爲一名 Android 開發,正常狀況下對 View 的繪製機制基本仍是耳熟能詳的,尤爲對於常常須要自定義 View 實現一些特殊效果的同窗。php
網上也出現了大量的 Blog 講 View 的 onMeasure()
、onLayout()
、onDraw()
等,雖然這是一個每一個 Android 開發都應該知曉的東西,但這一系列實在是太多了,徹底不符合我們短平快的這個系列初衷。java
那麼,今天咱們就來簡單談談 measure()
過程當中很是重要的 MeasureSpec
。android
對於絕大多數人來講,都是知道 MeasureSpec
是一個 32 位的 int 類型。而且取了最前面的兩位表明 Mode,後 30 位表明大小 Size。ide
相比也很是清楚 MeasureSpec
有 3 種模式,它們分別是 EXACTLY
、AT_MOST
和 UNSPECIFIED
。佈局
- 精確模式(MeasureSpec.EXACTLY):在這種模式下,尺寸的值是多少,那麼這個組件的長或寬就是多少,對應
MATCH_PARENT
和肯定的值。- 最大模式(MeasureSpec.AT_MOST):這個也就是父組件,可以給出的最大的空間,當前組件的長或寬最大隻能爲這麼大,固然也能夠比這個小。對應
WRAP_CONETNT
。- 未指定模式(MeasureSpec.UNSPECIFIED):這個就是說,當前組件,能夠隨便用空間,不受限制。
一般來講,咱們在自定義 View 的時候會常常地接觸到 AT_MOST
和 EXACTLY
,咱們一般會根據兩種模式去定義本身的 View 大小,在 wrap_content
的時候使用本身計算或者設置的一個默認值。而更多的時候咱們都會認爲 UNSPECIFIED
這個模式被應用在系統源碼中。具體就體如今 NestedScrollView
和 ScrollView
中。測試
咱們看這樣一個 XML 文件:this
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/scrollView" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity">
<TextView android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/colorAccent" android:text="Hello World" android:textColor="#fff">
</TextView>
</android.support.v4.widget.NestedScrollView>
複製代碼
在 NestedScrollView
裏面寫了一個充滿屏幕高度的 TextView
,爲了更方便看效果,咱們設置了一個背景顏色。但咱們從 XML 預覽中卻會驚訝的發現不同的狀況。 spa
咱們所指望的是填充滿屏幕的 TextView
,但實際效果卻和 TextView
設置高度爲 wrap_content
一模一樣。日誌
很明顯,這必定是高度測量出現的問題,若是咱們的父佈局是 LinearLayout
,很明顯沒有任何問題。因此問題必定出在了 NestedScrollView
的 onMeasure()
中。code
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (this.mFillViewport) {
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode != 0) {
if (this.getChildCount() > 0) {
View child = this.getChildAt(0);
LayoutParams lp = (LayoutParams)child.getLayoutParams();
int childSize = child.getMeasuredHeight();
int parentSpace = this.getMeasuredHeight() - this.getPaddingTop() - this.getPaddingBottom() - lp.topMargin - lp.bottomMargin;
if (childSize < parentSpace) {
int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, this.getPaddingLeft() + this.getPaddingRight() + lp.leftMargin + lp.rightMargin, lp.width);
int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(parentSpace, 1073741824);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
}
}
複製代碼
因爲咱們並無在外面設置 mFillViewport
這個屬性,因此並不會進入到 if 條件中,咱們來看看 NestedScrollView
的 super FrameLayout
的 onMeasure()
作了什麼。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
mMatchParentChildren.clear();
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
// ignore something...
}
複製代碼
注意其中的關鍵方法 measureChildWithMargins()
,這個方法在 NestedScrollView
中獲得了徹底重寫。
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
MarginLayoutParams lp = (MarginLayoutParams)child.getLayoutParams();
int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, this.getPaddingLeft() + this.getPaddingRight() + lp.leftMargin + lp.rightMargin + widthUsed, lp.width);
int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.topMargin + lp.bottomMargin, 0);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
複製代碼
咱們看到其中有句很是關鍵的代碼:
int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.topMargin + lp.bottomMargin, 0);
複製代碼
NestedScrollView
直接無視了用戶設置的 MODE,直接採用了 UNSPECIFIED
作處理。通過測試發現,當咱們重寫 NestedScrollView
的這句代碼,而且把 MODE 設置爲 EXACTLY
的時候,咱們獲得了咱們想要的效果,我已經查看 Google 的源碼提交日誌,並無找到緣由。
實際上,絕大多數開發以前遇到的嵌套
ListView
或者RecylerView
只展現一行也是因爲這個問題,解決方案就是重寫NestedScrollView
的measureChildWithMargins()
或者重寫ListView
或者RecylerView
的onMeasure()
方法讓其展現正確的高度。
我起初猜測是隻有 UNSPECIFIED
才能實現滾動效果,但很遺憾並非這樣的。因此在這裏拋出這個問題,但願有知情人士能一塊兒討論。