自定义view onMeasure android测量模式

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/wangwo1991/article/details/84480759

在自定义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补齐

猜你喜欢

转载自blog.csdn.net/wangwo1991/article/details/84480759