自定義view重寫onMeasure方法自定義設(shè)置寬高,不同布局表現(xiàn)不一致問題分析

新來同事在學(xué)習(xí)自定義view的時候惯悠,參照書上的例子自定義了一個view:

MyView.java

private int getMySize(int defaultSize, int measureSpec){
        int mySize = defaultSize;
        int mode = MeasureSpec.getMode(measureSpec);
        int size = MeasureSpec.getSize(measureSpec);
        switch (mode){
            case MeasureSpec.UNSPECIFIED:{//如果沒有指定大小邻邮,就設(shè)置為默認(rèn)大小
                mySize = defaultSize;
                break;
            }
            case MeasureSpec.AT_MOST:
            case MeasureSpec.EXACTLY: {//如果測量模式是最大取值為size
                //我們將大小取最大值,你也可以取其他值
                mySize = size;
                break;
            }//如果是固定大小克婶,那就不要去改變它
        }
        return mySize;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = getMySize(100,widthMeasureSpec);
        int height = getMySize(100,heightMeasureSpec);
        if (width<height){
            height = width;
        }else {
            width = height;
        }
        setMeasuredDimension(width,height);
        Log.d("TAG", "onMeasure: "+width+":"+height);
    }

他在重寫的onMeasure中重新設(shè)置了view寬高筒严,但是他神奇的發(fā)現(xiàn)Linerlayout中放入自定view表現(xiàn)達(dá)到預(yù)期丹泉,是一個正方形。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.my.textanimator.view.MyView
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:background="@android:color/holo_green_light"
        app:layout_constraintBottom_toBottomOf="parent"/>

</LinearLayout>

但是一旦將父布局修改為RelativeLayout時鸭蛙,寬高分別交換表現(xiàn)不一致摹恨,width為match_parent 時畫出來是矩形,height為match_parent時又是能達(dá)到預(yù)期的正方形娶视。這一下給我問懵了晒哄,我也沒認(rèn)真看過布局的源碼一時半會兒還真覺得很神奇,講不出來為什么肪获。但是覺得挺有意思就決定看看源碼分析分析寝凌。

1.LinearLayout

子view如何布局是父布局的onLayout方法決定的,方法如下:

 @Override
    //對子view進(jìn)行布局
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }

    //垂直布局
    void layoutVertical(int left, int top, int right, int bottom) {
       ...

        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {
                //拿到子view測量到的寬高
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();

                //子組件的Params
                final LinearLayout.LayoutParams lp =
                        (LinearLayout.LayoutParams) child.getLayoutParams();

                int gravity = lp.gravity;
                if (gravity < 0) {
                    gravity = minorGravity;
                }
                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                //默認(rèn)情況下走left
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                                + lp.leftMargin - lp.rightMargin;
                        break;

                    case Gravity.RIGHT:
                        childLeft = childRight - childWidth - lp.rightMargin;
                        break;

                    case Gravity.LEFT:
                    default:
                        childLeft = paddingLeft + lp.leftMargin;
                        break;
                }

                if (hasDividerBeforeChildAt(i)) {
                    childTop += mDividerHeight;
                }

                childTop += lp.topMargin;
                //layout時 right=left+childWidth,bottom=top+childHeight  即右邊和底部都是根        
               據(jù)子組件自己測量的值來的
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

                i += getChildrenSkipCount(child, i);
            }
        }
    }

 private void setChildFrame(View child, int left, int top, int width, int height) {
        child.layout(left, top, left + width, top + height);
    }

由上源碼分析可知孝赫,具體layout 是根據(jù)子組件的測量結(jié)果來布局的较木,所以接下看看測量方法。

既然是長寬表現(xiàn)問題寒锚,那么就直接去看看view的onMeasure方法:

  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }

根據(jù)方向不同走不同方法劫映,我們就看默認(rèn)的垂直布局的吧!

 void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {

            //省略部分不重要代碼
            ....

            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            totalWeight += lp.weight;

            final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;//false
            if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
                ...
            } else {
                //useExcessSpace=false
                if (useExcessSpace) {
                    // The heightMode is either UNSPECIFIED or AT_MOST, and
                    // this child is only laid out using excess space. Measure
                    // using WRAP_CONTENT so that we can find out the view's
                    // optimal height. We'll restore the original height of 0
                    // after measurement.
                    lp.height = LayoutParams.WRAP_CONTENT;
                }

                // Determine how big this child would like to be. If this or
                // previous children have given a weight, then we allow it to
                // use all available space (and we will shrink things later
                // if needed).
                final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
                //測量子view 調(diào)用measureChildWithMargins  最終調(diào)用我們自定義viewonMeasure方法
                measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                        heightMeasureSpec, usedHeight);

                final int childHeight = child.getMeasuredHeight();

                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                       lp.bottomMargin + getNextLocationOffset(child));

                if (useLargestChild) {
                    largestChildHeight = Math.max(childHeight, largestChildHeight);
                }
            }

}

   protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        //lp.width=MATCH_PARENT 根據(jù)getChildMeasureSpec方法得到傳輸給自定義view的寬為父布局 
       的寬度
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        //lp.height=100dp  同理通過getChildMeasureSpec方法得到傳遞給子view的高為子view的高
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        //傳給子view width=parentWidth(1080)height=100
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

由上知道傳給myview的參數(shù)刹前,myView中重寫onMeasure方法 導(dǎo)致寬高相同并且取最小的泳赋,所以最終子view測量出來的width=height=100。

至此了解到LinearLayout表現(xiàn)達(dá)到預(yù)期的原因了:LinearLayout最終布局時是使用的子View自己測量出來的寬高喇喉,子view又重寫了測量方法祖今,導(dǎo)致寬高相等,所以顯示出來是預(yù)期的正方形拣技。

2.RelativeLayout中的表現(xiàn)

然而在RelativeLayout中設(shè)置:layout_width=match_parent height=100dp 顯示結(jié)果卻是一個高度為100dp的矩形千诬,未能達(dá)到預(yù)期的正方形。

參照LinearLayout的分析膏斤,我們直接看RelativeLayout的onMeasure方法:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        ...
       //父布局RelativeLayout寬高都MATCH_PARENT所以 為EXACTLY
        if (widthMode != MeasureSpec.UNSPECIFIED) {
            //父布局寬1920
            myWidth = widthSize;
        }

        if (heightMode != MeasureSpec.UNSPECIFIED) {
            //父布局高1080
            myHeight = heightSize;
        }

        if (widthMode == MeasureSpec.EXACTLY) {
            width = myWidth;
        }

        if (heightMode == MeasureSpec.EXACTLY) {
            height = myHeight;
        }

      ...

        for (int i = 0; i < count; i++) {
            View child = views[i];
            if (child.getVisibility() != GONE) {
                LayoutParams params = (LayoutParams) child.getLayoutParams();
                int[] rules = params.getRules(layoutDirection);

                applyHorizontalSizeRules(params, myWidth, rules);
                //測量子view的方法
                measureChildHorizontal(child, params, myWidth, myHeight);
                //設(shè)置子view right的方法
                if (positionChildHorizontal(child, params, myWidth, isWrapContentWidth)) {
                    offsetHorizontalAxis = true;
                }
            }
        }

      ...

        for (int i = 0; i < count; i++) {
            final View child = views[i];
            if (child.getVisibility() != GONE) {
                final LayoutParams params = (LayoutParams) child.getLayoutParams();

                applyVerticalSizeRules(params, myHeight, child.getBaseline());
                //測量子view的方法
                measureChild(child, params, myWidth, myHeight);
                 //設(shè)置子viewbottom的方法
                if (positionChildVertical(child, params, myHeight, isWrapContentHeight)) {
                    offsetVerticalAxis = true;
                }

            ...

        setMeasuredDimension(width, height);
    }

源碼內(nèi)容很多徐绑,我們忽略掉不重要的部分,主要是獲取父布局的寬高莫辨,測量子view傲茄,給子view設(shè)置左右位置方便繪制,再次測量子view的寬高沮榜。

第一次測量子view寬高的方法measureChildHorizontal

private void measureChildHorizontal(
            View child, LayoutParams params, int myWidth, int myHeight) {
        //子view寬的測量要求
        final int childWidthMeasureSpec = getChildMeasureSpec(params.mLeft, params.mRight,
                params.width, params.leftMargin, params.rightMargin, mPaddingLeft, mPaddingRight,
                myWidth);

        final int childHeightMeasureSpec;
        //myHeight=1080
        if (myHeight < 0 && !mAllowBrokenMeasureSpecs) {
         ...
        } else {
            final int maxHeight;
            if (mMeasureVerticalWithPaddingMargin) {
                maxHeight = Math.max(0, myHeight - mPaddingTop - mPaddingBottom
                        - params.topMargin - params.bottomMargin);
            } else {
                maxHeight = Math.max(0, myHeight);
            }

            final int heightMode;
            if (params.height == LayoutParams.MATCH_PARENT) {
                heightMode = MeasureSpec.EXACTLY;
            } else {
                heightMode = MeasureSpec.AT_MOST;
            }
            //得到最終的高度為1080
            childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight, heightMode);
        }

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

private int getChildMeasureSpec(int childStart, int childEnd,
            int childSize, int startMargin, int endMargin, int startPadding,
            int endPadding, int mySize) {
        //childSize=MATCH_PARENT=-1  mySize=1920
        int childSpecMode = 0;
        int childSpecSize = 0;

        final boolean isUnspecified = mySize < 0;//>0
        if (isUnspecified && !mAllowBrokenMeasureSpecs) {
            ...
        }
        int tempStart = childStart;
        int tempEnd = childEnd;

         //true
        if (tempStart == VALUE_NOT_SET) {
            //              0                 0
            tempStart = startPadding + startMargin;
        }
        //true
        if (tempEnd == VALUE_NOT_SET) {
            //        1920      0               0
            tempEnd = mySize - endPadding - endMargin;
        }

        // Figure out maximum size available to this view
        final int maxAvailable = tempEnd - tempStart;//maxAvailable=1920

        if (childStart != VALUE_NOT_SET && childEnd != VALUE_NOT_SET) {
         ...
        } else {
                  //childSize=-1
            if (childSize >= 0) {
               ...
            } else if (childSize == LayoutParams.MATCH_PARENT) {//true
                // Child wanted to be as big as possible. Give all available
                // space.
                childSpecMode = isUnspecified ? MeasureSpec.UNSPECIFIED : 
                MeasureSpec.EXACTLY;
                //childSpecSize=1920
                childSpecSize = Math.max(0, maxAvailable);
            } else if (childSize == LayoutParams.WRAP_CONTENT) {
                // Child wants to wrap content. Use AT_MOST to communicate
                // available space if we know our max size.
                if (maxAvailable >= 0) {
                    // We have a maximum size in this dimension.
                    childSpecMode = MeasureSpec.AT_MOST;
                    childSpecSize = maxAvailable;
                } else {
                    // We can grow in this dimension. Child can be as big as it
                    // wants.
                    childSpecMode = MeasureSpec.UNSPECIFIED;
                    childSpecSize = 0;
                }
            }
        }

由上分析可知傳遞給子view的參數(shù)為(1920,1080)盘榨,根據(jù)子view重寫方法可以得到測量結(jié)果:(1080,1080)。

接下來是確定子view的左右位置positionChildHorizontal()

private boolean positionChildHorizontal(View child, LayoutParams params, int myWidth,
            boolean wrapContent) {

        final int layoutDirection = getLayoutDirection();
        int[] rules = params.getRules(layoutDirection);
        //false
        if (params.mLeft == VALUE_NOT_SET && params.mRight != VALUE_NOT_SET) {
            ...
        } else if (params.mLeft != VALUE_NOT_SET && params.mRight == VALUE_NOT_SET) {
           ...
        } else if (params.mLeft == VALUE_NOT_SET && params.mRight == VALUE_NOT_SET) {
            // Both left and right vary
             //false
            if (rules[CENTER_IN_PARENT] != 0 || rules[CENTER_HORIZONTAL] != 0) {
                if (!wrapContent) {
                    centerHorizontal(child, params, myWidth);
                } else {
                    positionAtEdge(child, params, myWidth);
                }
                return true;
            } else {
                // This is the default case. For RTL we start from the right and for LTR we start
                // from the left. This will give LEFT/TOP for LTR and RIGHT/TOP for RTL.
                positionAtEdge(child, params, myWidth);
            }
        }
        return rules[ALIGN_PARENT_END] != 0;
    }

  private void positionAtEdge(View child, LayoutParams params, int myWidth) {
        if (isLayoutRtl()) {
            params.mRight = myWidth - mPaddingRight - params.rightMargin;
            params.mLeft = params.mRight - child.getMeasuredWidth();
        } else {//true
            //0               0                     0
            params.mLeft = mPaddingLeft + params.leftMargin;
            //                  0                   1080
            params.mRight = params.mLeft + child.getMeasuredWidth();
        }
    }

由上可以得到子view的左右位置分別為0,1080蟆融。

再次測量子view并且設(shè)置子view的top和bottom:

 //新的測量子view的方法
 private void measureChild(View child, LayoutParams params, int myWidth, int myHeight) {
        //(0,1080,MATCH_PARENT,0,0,0,0,1920)
        int childWidthMeasureSpec = getChildMeasureSpec(params.mLeft,
                params.mRight, params.width,
                params.leftMargin, params.rightMargin,
                mPaddingLeft, mPaddingRight,
                myWidth);
        //(0,0,100dp,0,0,0,0,1080)
        int childHeightMeasureSpec = getChildMeasureSpec(params.mTop,
                params.mBottom, params.height,
                params.topMargin, params.bottomMargin,
                mPaddingTop, mPaddingBottom,
                myHeight);
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

根據(jù)getChildMeasureSpec方法得到寬繼續(xù)為1920草巡,但是高度由于params.height=100dp,所以childsize>0

//childSize=100 
if (childSize >= 0) {
                // Child wanted an exact size. Give as much as possible.
                childSpecMode = MeasureSpec.EXACTLY;
                   //maxAvilable=1080
                if (maxAvailable >= 0) {
                    // We have a maximum size in this dimension.
                    //childSpecSize=100
                    childSpecSize = Math.min(maxAvailable, childSize);
                } else {
                    // We can grow in this dimension.
                    childSpecSize = childSize;
                }
            }

最終可知傳遞給子view的參數(shù)為(1920,100),最終計算出來的子view的寬高為100,100

設(shè)置top和bottom

    private boolean positionChildVertical(View child, LayoutParams params, int myHeight,
            boolean wrapContent) {

        int[] rules = params.getRules();

        if (params.mTop == VALUE_NOT_SET && params.mBottom != VALUE_NOT_SET) {
            // Bottom is fixed, but top varies
            params.mTop = params.mBottom - child.getMeasuredHeight();
        } else if (params.mTop != VALUE_NOT_SET && params.mBottom == VALUE_NOT_SET) {
            // Top is fixed, but bottom varies
            params.mBottom = params.mTop + child.getMeasuredHeight();
        } else if (params.mTop == VALUE_NOT_SET && params.mBottom == VALUE_NOT_SET) {
            // Both top and bottom vary
            if (rules[CENTER_IN_PARENT] != 0 || rules[CENTER_VERTICAL] != 0) {
                if (!wrapContent) {
                    centerVertical(child, params, myHeight);
                } else {
                    params.mTop = mPaddingTop + params.topMargin;
                    params.mBottom = params.mTop + child.getMeasuredHeight();
                }
                return true;
            } else {
                //                  0              0
                params.mTop = mPaddingTop + params.topMargin;
                //                     0                   100
                params.mBottom = params.mTop + child.getMeasuredHeight();
            }
        }
        return rules[ALIGN_PARENT_BOTTOM] != 0;
    }

由上可知子view的top為0型酥,bottom為100山憨。

至此我們就明白RelativeLayout中為何表現(xiàn)為矩形了:

由于測量方法不同查乒,第一次測量出來寬高為父布局的寬高的最小值,所以導(dǎo)致 right為父布局的寬萍歉,第二次測量高度為子view高度侣颂,所以取最小值 測量處理的結(jié)果為子view的高100档桃,最終導(dǎo)致繪制出來一個矩形枪孩。

同理當(dāng)子view的寬高交換以后,第一次測量出來的width為子view的寬度100藻肄,第二次雖然子view的高為matchParent蔑舞,但是子view的寬為100,總體測量去小值嘹屯,導(dǎo)致最終繪制出來的圖形為100*100的正方形攻询。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市州弟,隨后出現(xiàn)的幾起案子钧栖,更是在濱河造成了極大的恐慌,老刑警劉巖婆翔,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拯杠,死亡現(xiàn)場離奇詭異,居然都是意外死亡啃奴,警方通過查閱死者的電腦和手機(jī)潭陪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來最蕾,“玉大人依溯,你說我怎么就攤上這事∥猎颍” “怎么了黎炉?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長醋拧。 經(jīng)常有香客問我慷嗜,道長,這世上最難降的妖魔是什么趁仙? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任洪添,我火速辦了婚禮,結(jié)果婚禮上雀费,老公的妹妹穿的比我還像新娘干奢。我一直安慰自己,他們只是感情好盏袄,可當(dāng)我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布忿峻。 她就那樣靜靜地躺著薄啥,像睡著了一般。 火紅的嫁衣襯著肌膚如雪逛尚。 梳的紋絲不亂的頭發(fā)上垄惧,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天,我揣著相機(jī)與錄音绰寞,去河邊找鬼到逊。 笑死,一個胖子當(dāng)著我的面吹牛滤钱,可吹牛的內(nèi)容都是我干的觉壶。 我是一名探鬼主播,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼件缸,長吁一口氣:“原來是場噩夢啊……” “哼铜靶!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起他炊,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤争剿,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后痊末,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蚕苇,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年舌胶,在試婚紗的時候發(fā)現(xiàn)自己被綠了捆蜀。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡幔嫂,死狀恐怖辆它,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情履恩,我是刑警寧澤锰茉,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站切心,受9級特大地震影響飒筑,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜绽昏,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一协屡、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧全谤,春花似錦肤晓、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽漫萄。三九已至,卻和暖如春盈匾,著一層夾襖步出監(jiān)牢的瞬間腾务,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工削饵, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留岩瘦,地道東北人。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓葵孤,卻偏偏與公主長得像担钮,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子尤仍,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容