作為一名 Android 開發,正常情況下對 View 的繪制機制基本還是耳熟能詳的,尤其對于經常需要自定義 View 實作一些特殊效果的同學。
網上也出現了大量的 Blog 講 View 的
onMeasure()
、
onLayout()
、
onDraw()
等,雖然這是一個每個 Android 開發都應該知曉的東西,但這一系列實在是太多了,完全不符合咱們短平快的這個系列初衷。
那麼,今天我們就來簡單談談
measure()
過程中非常重要的
MeasureSpec
。
對于絕大多數人來說,都是知道
MeasureSpec
是一個 32 位的 int 類型。并且取了最前面的兩位代表 Mode,後 30 位代表大小 Size。
相比也非常清楚
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 檔案:
<?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 預覽中卻會驚訝的發現不一樣的情況。
我們所期望的是填充滿螢幕的
TextView
,但實際效果卻和
TextView
設定高度為
wrap_content
如出一轍。
很明顯,這一定是高度測量出現的問題,如果我們的父布局是
LinearLayout
,很明顯沒有任何問題。是以問題一定出在了
NestedScrollView
的
onMeasure()
中。
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
才能實作滾動效果,但很遺憾并不是這樣的。是以在這裡抛出這個問題,希望有知情人士能一起讨論。