1. 程式人生 > >[例證]淺談getWidth()和getMeasureWidth()區別

[例證]淺談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();
            }
        } 
        ...
}