[例證]淺談getWidth()和getMeasureWidth()區別
一、一個簡單的例子:
重寫自定義View的onDraw()程式碼:
oval.left=getMeasuredWidth()/2-radius;
oval.top=getMeasuredHeight()/2 -radius;
oval.right=getMeasuredWidth()/2 +radius;
oval.bottom=getMeasuredHeight()/2 +radius;
canvas.drawArc(oval,0,360,true,mPaint);
得到效果圖如下:
這麼做肯定沒問題。
有這麼個疑問:
二、getwidth,getheight和getMeasuredWidth和getMeasuredHeight的分別是什麼?區別是什麼?
定義總結:
getwidth():viewGroup的遍歷每個子view,子view的layout()方法測量的結果。測量方式:getwidth=子佈局右側-子佈局左側;
getMeasuredWidth():viewGroup的遍歷每個子view,子view的最近一次呼叫measure()方法測量後得到的,就是View的寬度。
原始碼分析區別:
回到這個getWidth()方法和getMeasureWidth()的區別這個問題上。
網上說法很多,我決定自己一點點從原始碼裡面扣。然後舉例說明。
三、View#getMeasuredWidth():
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
得到的是最近一次呼叫measure()方法測量後得到的是View的寬度。
跟一下原始碼知道:
平時我們自定義View(繼承view)會重寫onMeasure方法:(什麼情況下寫onMeasure?後續會有解答)
View#onMeasure原始碼如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
上面View#onMeasure方法會呼叫
View#setMeasuredDimension原始碼如下:
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
View#setMeasuredDimension方法會呼叫
View#setMeasuredDimensionRaw原始碼如下:
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
看到這個方法程式碼第一行第二行有個賦值
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
這就是我們的getMeasuredWidth()與getMeasuredHeight()方法的值。
例如:
所以當我們重寫onMeasure方法時,如果對setMeasuredDimension()這個方法引數直接自定義,如setMeasuredDimension(200,300),那麼getMeasuredWidth()的值必然就是200,getMeasuredHeight()的值必然就是300。
使用場景:
當然,我們一般情況下,不會使用直接這種方式寫死引數,一般還是對onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法的引數進行處理,再傳入setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
四、什麼情況下我們需要對onMeasure方法重寫呢?
總結: 子View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同決定”這個結論 為什麼使用?因為我們
自定義view對於屬性為wrap_content這種情況,如果不做處理其實是與match_parent是一樣效果的。
原因是什麼呢?
參考如下:任玉剛的書中表格:(原始碼也可分析,暫不做此處發散)
當父容器的specmode為EXACTLY和AT_MOST時子view不管是wrap_content還是match_parent,它的預設大小都是父容器大小parentSize。
不信?舉個栗子:
直接先上一個自定義view為match_parent時的效果圖:
再上一個自定義view為wrap_content時的圖:
好吧,沒有比較就沒有傷害。上一個自定義view為40dp的寬高的圖。
這回效果很明顯了吧。除非是精確值,否則大小都等於父佈局的大小。
那麼這我當然不能接受。我wrap_content需要有所變化,需要一個預設值大小200*200。
於是有了
<pre name="code" class="java">@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int speSize = MeasureSpec.getSize(heightMeasureSpec);
int speMode = MeasureSpec.getMode(heightMeasureSpec);
Log.d("MyView", "---speSize = " + speSize + "");
Log.d("MyView", "---speMode = " + speMode + "");
if(speMode == MeasureSpec.AT_MOST){
Log.d("MyView", "---AT_MOST---");
}
if(speMode == MeasureSpec.EXACTLY){
Log.d("MyView", "---EXACTLY---");
}
if(speMode == MeasureSpec.UNSPECIFIED){
Log.d("MyView", "---UNSPECIFIED---");
}
if(speMode==MeasureSpec.AT_MOST){
setMeasuredDimension(100, 100);
}else{
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
}
}
再看效果圖:
所以由此類推,我們之所以重寫onMeasure也就是為了wrap_content時能自動按照需求改變。回到原本的話題的第二塊:getWidth()方法:
五、View#getWidth()
原始碼如下
View#getWidth()
@ViewDebug.ExportedProperty(category = "layout")
public final int getWidth() {
return mRight - mLeft;
}
這裡的mRight和mLeft到底是什麼呢?其實它是layout過程傳過來的四個引數中的兩個:
View#layout()
<pre name="code" class="java">public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
有兩種測量setOpticalFrame、setFrame,最終都會在其中呼叫了setFrame方法,它的原始碼如下:
View#setFrame()
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
//....
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
//......
}
return changed;
}
這樣就有了mLeft和mRight兩個值了。
當然,光知道來處還的會用。我們平時自定義view繼承自view時是不會對onlayout方法重寫的。只有當重寫佈局viewGroup時才會對onlayout重寫。
六、什麼時候會遇到getWidth()和getMeasureWidth()不一致?
當繼承佈局viewGroup時,重寫onlayout方法。對子view的 childView.layout(0,0,200,200);
我們平時重寫onlayout()方法主要是為了對子佈局自定義,比如瀑布流,比如放不下換行顯示子view這種操作。
舉個栗子:
當繼承佈局viewGroup時,重寫onlayout方法,
重寫後的onlayout原始碼如下:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int mViewGroupWidth = getMeasuredWidth(); //當前ViewGroup的總寬度
int mPainterPosX = l; //當前繪圖游標橫座標位置
int mPainterPosY = t; //當前繪圖游標縱座標位置
int childCount = getChildCount();
for ( int i = 0; i < childCount; i++ ) {
View childView = getChildAt(i);
int width = childView.getMeasuredWidth();
int height = childView.getMeasuredHeight();
//如果剩餘的空間不夠,則移到下一行開始位置
if( mPainterPosX + width > mViewGroupWidth ) {
mPainterPosX = l;
mPainterPosY += height;
}
//執行ChildView的繪製
// childView.layout(mPainterPosX,mPainterPosY,mPainterPosX+width, mPainterPosY+height);
childView.layout(0,0,100, 200);
//記錄當前已經繪製到的橫座標位置
mPainterPosX += width;
}
}
例如:
這裡對子view佈局使用固定值childView.layout(0,0,100, 200);
看下佈局檔案:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<com.example.user.mytestview.MyViewGroup
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.user.mytestview.MyView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.example.user.mytestview.MyViewGroup>
</RelativeLayout>
在子view種打個log:發現
現在子 view的getWidth和getMeasuredWidth不一樣了。
七、什麼時候用getWidth?什麼時候用getMeasureWidth()?
由view繪製流程我們知道:順序是:onMeasure()--》onLayout()--》onDraw();(見原始碼ViewRootImpl#performTraversals() 方法,下一篇打算講這個內容)
所以再onMeasure之後可以getMeasuredWidth,在Onlayout()之後 可以用getWitdth().
此處簡單附上精簡原始碼:
ViewRootImpl#performTraversals()
原始碼如下:
private void performTraversals() {
...
if (!mStopped) {
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); // 1
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
if (didLayout) {
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
...
}
if (!cancelDraw && !newSurface) {
if (!skipDraw || mReportNextDraw) {
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).startChangingAnimations();
}
mPendingTransitions.clear();
}
performDraw();
}
}
...
}