自定義view onMeasure android測量模式
在自定義view中多半都會去重寫onMeasure方法,進行view的測量,測量出大小後,再在onDraw方法中進行繪製,下面是一段簡易的自定義view的程式碼:
public class MyTextView extends View { //在new一個MyTextView物件的時候會呼叫 public MyTextView(Context context) { this(context,null); } //在xml佈局檔案中使用MyTextView會呼叫 public MyTextView(Context context, AttributeSet attrs) { this(context, attrs,0); } //在xml佈局檔案中使用MyTextView並給MyTextView設定style樣式會呼叫 public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //獲取寬高模式 int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode=MeasureSpec.getMode(heightMeasureSpec); //獲取寬高大小 int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); } }
通過MeasureSpec.getMode()可以獲取到寬高模式,系統提供了下面三個模式的常量值:
//在xml佈局中設定為wrap_content
MeasureSpec.AT_MOST;
//在xml佈局中設定為具體的值比如100dp或者match_parent或者fill_parent
MeasureSpec.EXACTLY;
//在實際開發中很少用到,ScrollView、ListView等原始碼中有使用到
MeasureSpec.UNSPECIFIED;
在專案開發中有時候會用到ScrollView和ListView的巢狀(當然現在很少用到了),就會碰到ListView顯示條目不全的問題,其實就是ScrollView在測量時將ListView的測量模式設定為MeasureSpec.UNSPECIFIED,ListView在測量時的判斷所導致的;ScrollView是一個佈局容器,肯定是繼承自ViewGoup的,在ViewGroup中會發現measureChild()方法,該方法是用來測量佈局容器中子view的方法,在ViewGoup中的measureChild()方法中並沒有指定子view的測量模式,
//這個是ViewGroup中的measureChild方法原始碼 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); //呼叫view中的measure方法去測量子view child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
ViewGroup中沒有做任何動作,但是ScrollView中重寫了ViewGroup中的measureChild()方法,
//這是ScrollView中measureChild的原始碼,
@Override
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
ViewGroup.LayoutParams lp = child.getLayoutParams();
int childWidthMeasureSpec;
int childHeightMeasureSpec;
//在這裡指定了子view的height mode 為MeasureSpec.UNSPECIFIED
childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft
+ mPaddingRight, lp.width);
final int verticalPadding = mPaddingTop + mPaddingBottom;
childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - verticalPadding),
MeasureSpec.UNSPECIFIED);
//進行子view的測量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
在這裡首先要明白的是childMeasure(childeWidthMeasureSpec,childHeightMeasureSpec);中的兩個引數childeWidthMeasureSpec和childHeightMeasureSpec;childeWidthMeasureSpec和childHeightMeasureSpec它是包含兩部分的,前兩位是mode,後30為是值(size);接下來就會呼叫view中的measure方法及onMeasure()方法,並把寬高值和寬高模式傳入;但是ListView的話將View中的onMeasure方法進行了重寫;
//這裡是ListView中onMeasure方法原始碼
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Sets up mListPadding
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//獲取寬高模式
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//獲取寬高大小
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int childWidth = 0;
int childHeight = 0;
int childState = 0;
mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED
|| heightMode == MeasureSpec.UNSPECIFIED)) {
final View child = obtainView(0, mIsScrap);
// Lay out child directly against the parent measure spec so that
// we can obtain exected minimum width and height.
measureScrapChild(child, 0, widthMeasureSpec, heightSize);
childWidth = child.getMeasuredWidth();
childHeight = child.getMeasuredHeight();
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(
((LayoutParams) child.getLayoutParams()).viewType)) {
mRecycler.addScrapView(child, 0);
}
}
//在這裡對寬度模式進行了判斷
if (widthMode == MeasureSpec.UNSPECIFIED) {
widthSize = mListPadding.left + mListPadding.right + childWidth +
getVerticalScrollbarWidth();
} else {
widthSize |= (childState & MEASURED_STATE_MASK);
}
//在這裡對高度模式進行了判斷
if (heightMode == MeasureSpec.UNSPECIFIED) {
//如果高度的模式是MeasureSpec.UNSPECIFIED 計算的heightSize大小就是top+bottom+單個item條目高度(childHeight)+分割線的高度
heightSize = mListPadding.top + mListPadding.bottom + childHeight +
getVerticalFadingEdgeLength() * 2;
}
if (heightMode == MeasureSpec.AT_MOST) {
// TODO: after first layout we should maybe start at the first visible position, not 0
//如果高度模式是MeasureSpec.AT_MOST就會去計算所有item條目的高度,並賦值個heightSize
heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
}
//設定計算好的寬高
setMeasuredDimension(widthSize, heightSize);
mWidthMeasureSpec = widthMeasureSpec;
}
在ScrollView和ListView巢狀的時候,ScrollView給ListView高度模式設定的是MeasureSpec.UNSPECIFIED,同時ListView又對onMeasure方法進行了重寫,所以就出現了ScrollView巢狀ListView條目顯示不全的問題,其實只需將ListView的高度模式設定為MeasureSpec.AT_MOST就可以去測量計算所有item的高度了;
public class MyListView extends ListView {
public MyListView(Context context) {
this(context,null);
}
public MyListView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public MyListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//指定ListView的高度模式為MeasureSpecAT_MOST 並指定大小為Integer最大值右移兩位
heightMeasureSpec=MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE>>2,MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
這樣子問題就解決了,寬高模式設定為MeasureSpec.AT_MOST容易理解,大小設定為Integer的最大值右移兩位;
//這裡是ListView中測量所有item高度的原始碼 maxHeight就是heightSize
final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
int maxHeight, int disallowPartialChildPosition) {
final ListAdapter adapter = mAdapter;
if (adapter == null) {
return mListPadding.top + mListPadding.bottom;
}
// Include the padding of the list
//定義的返回height變數
int returnedHeight = mListPadding.top + mListPadding.bottom;
final int dividerHeight = mDividerHeight;
// The previous height value that was less than maxHeight and contained
// no partial children
int prevHeightWithoutPartialChild = 0;
int i;
View child;
// mItemCount - 1 since endPosition parameter is inclusive
endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition;
final AbsListView.RecycleBin recycleBin = mRecycler;
final boolean recyle = recycleOnMeasure();
final boolean[] isScrap = mIsScrap;
for (i = startPosition; i <= endPosition; ++i) {
child = obtainView(i, isScrap);
measureScrapChild(child, i, widthMeasureSpec, maxHeight);
if (i > 0) {
// Count the divider for all but one child
returnedHeight += dividerHeight;
}
// Recycle the view before we possibly return from the method
if (recyle && recycleBin.shouldRecycleViewType(
((LayoutParams) child.getLayoutParams()).viewType)) {
recycleBin.addScrapView(child, -1);
}
returnedHeight += child.getMeasuredHeight();
//maxHeight值是Integer.MAX_VALUE>>2為,所有returnedHeight的值是永遠小於maxHeight的,這個if條件是永遠不成立的,這樣就可以返回returnedHeight計算出來的值,也就是設定height大小為Integer.MAX_VALUE>>2的原因
if (returnedHeight >= maxHeight) {
// We went over, figure out which height to return. If returnedHeight > maxHeight,
// then the i'th position did not fit completely.
return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
&& (i > disallowPartialChildPosition) // We've past the min pos
&& (prevHeightWithoutPartialChild > 0) // We have a prev height
&& (returnedHeight != maxHeight) // i'th child did not fit completely
? prevHeightWithoutPartialChild
: maxHeight;
}
if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
prevHeightWithoutPartialChild = returnedHeight;
}
}
// At this point, we went through the range of children, and they each
// completely fit, so return the returnedHeight
return returnedHeight;
}
這裡涉及到java中的左移,右移運算子;
<< : 左移運算子,num << 1,相當於num乘以2
>> : 右移運算子,num >> 1,相當於num除以2
>>> : 無符號右移,忽略符號位,空位都以0補齊