美文网首页Android自定义View
自定义view重写onMeasure方法自定义设置宽高,不同布局

自定义view重写onMeasure方法自定义设置宽高,不同布局

作者: 落叶随风_509f | 来源:发表于2021-07-27 18:28 被阅读0次

    新来同事在学习自定义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:{//如果没有指定大小,就设置为默认大小
                    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中重新设置了view宽高,但是他神奇的发现Linerlayout中放入自定view表现达到预期,是一个正方形。

    <?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时,宽高分别交换表现不一致,width为match_parent 时画出来是矩形,height为match_parent时又是能达到预期的正方形。这一下给我问懵了,我也没认真看过布局的源码一时半会儿还真觉得很神奇,讲不出来为什么。但是觉得挺有意思就决定看看源码分析分析。

    1.LinearLayout

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

     @Override
        //对子view进行布局
        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);
                    //默认情况下走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  即右边和底部都是根        
                   据子组件自己测量的值来的
                    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 是根据子组件的测量结果来布局的,所以接下看看测量方法。

    既然是长宽表现问题,那么就直接去看看view的onMeasure方法:

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

    根据方向不同走不同方法,我们就看默认的垂直布局的吧!

     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 调用measureChildWithMargins  最终调用我们自定义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 根据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的参数,myView中重写onMeasure方法 导致宽高相同并且取最小的,所以最终子view测量出来的width=height=100。

    至此了解到LinearLayout表现达到预期的原因了:LinearLayout最终布局时是使用的子View自己测量出来的宽高,子view又重写了测量方法,导致宽高相等,所以显示出来是预期的正方形。

    2.RelativeLayout中的表现

    然而在RelativeLayout中设置:layout_width=match_parent height=100dp 显示结果却是一个高度为100dp的矩形,未能达到预期的正方形。

    参照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);
                    //设置子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);
                     //设置子viewbottom的方法
                    if (positionChildVertical(child, params, myHeight, isWrapContentHeight)) {
                        offsetVerticalAxis = true;
                    }
    
                ...
    
            setMeasuredDimension(width, height);
        }
    
    

    源码内容很多,我们忽略掉不重要的部分,主要是获取父布局的宽高,测量子view,给子view设置左右位置方便绘制,再次测量子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的参数为(1920,1080),根据子view重写方法可以得到测量结果:(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并且设置子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);
        }
    
    

    根据getChildMeasureSpec方法得到宽继续为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的参数为(1920,100),最终计算出来的子view的宽高为100,100

    设置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中为何表现为矩形了:

    由于测量方法不同,第一次测量出来宽高为父布局的宽高的最小值,所以导致 right为父布局的宽,第二次测量高度为子view高度,所以取最小值 测量处理的结果为子view的高100,最终导致绘制出来一个矩形。

    同理当子view的宽高交换以后,第一次测量出来的width为子view的宽度100,第二次虽然子view的高为matchParent,但是子view的宽为100,总体测量去小值,导致最终绘制出来的图形为100*100的正方形。

    相关文章

      网友评论

        本文标题:自定义view重写onMeasure方法自定义设置宽高,不同布局

        本文链接:https://www.haomeiwen.com/subject/bdiomltx.html