我是靠谱客的博主 落寞中心,最近开发中收集的这篇文章主要介绍Android 自定义控件基础:measure过程,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

measure过程要分为两种情况:如果是原始的View,需要通过measure 方法就完成其测量过程;如果是ViewGroup,除了完成自己的测量过程外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个流程。

ViewRootImpl

View 的整个绘制流程的启动入口可以从 ViewRootImpl 的 performTraversals 方法开始看。performTraversals 方法调用了performMeasure()方法。

// ViewRootImpl 的源码

private void performTraversals() {
    ....
    
    if (!mStopped || mReportNextDraw) {
        boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
        (relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
        
        if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
           || mHeight != host.getMeasuredHeight() || dispatchApplyInsets ||
         updatedConfiguration) {
         
            // mWidth/mHeight:屏幕的宽/高   lp.width/lp.height:DecorView 的宽高
            // 得到DecorView 的MeasureSpec
            int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
            int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);  
            // 测量过程开始
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            .....
        }
    }
    
    ....
}

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        if (mView == null) {
            return;
        }
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            // mView(DecorView) 调用View#measure方法 进行测量宽/高
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
}

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

View measure过程

View 的measure 过程由其measure方法来完成,measure方法是一个final类型的方法,这意味着子类不能重写此方法,在View 的measure 方法中会去调用View的onMeasure方法。

// View 的源码

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ....
    final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
                || heightMeasureSpec != mOldHeightMeasureSpec;
        final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
                && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
        final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
                && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
        final boolean needsLayout = specChanged
                && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
    //如果上一次的测量规格和这次不一样,则条件满足,重新测量视图View的大小        
    if (forceLayout || needsLayout) {
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }
    }
    ....
    
    mOldWidthMeasureSpec = widthMeasureSpec;
    mOldHeightMeasureSpec = heightMeasureSpec;
}

从上面的代码可以看出,首先判断和上次测量的MeasureSpec是否相等,如果不相等就重新测量。measure() 方法调用了onMeasure()方法,并且把相应的宽/高测量规格传进去。说明测量的主要的逻辑在View#onMeasure()方法中。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // setMeasuredDimension方法 设置View宽/高的测量值
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(),
    widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

// 根据View默认大小的宽高和父View传递的测量规格重新计算View的测量宽高
public static int getDefaultSize(int size, int measureSpec) {
        // size代表view的默认尺寸大小
        int result = size;
        // measureSpec代表父容器提供的子View的MeasureSpec
        int specMode = MeasureSpec.getMode(measureSpec);
        // specSize 是View 测量后的大小
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        // result代表返回的view的尺寸大小
        return result;
}

protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth,mBackground.getMinimumWidth());
}

protected int getSuggestedMinimumHeight() {
    return (mBackground == null) ? mMinHeight :
    max(mMinHeight, mBackground.getMinimumHeight());
}

从以上源码来看,如果View没有设置背景,那么View的宽度为mMinWidth,而mMinWidth对应于android:minWidth 这个属性所指定的值,因此View的宽度为android:minWidth属性所指定的值。android:minWidth属性如果不指定,那么mMinWidth则默认为0;如果View指定了背景,则返回android:minWidth和背景的最小宽度这两者中的最大值,getSuggestedMinimumWidth 和 getSuggestedMinimumHeight的返回值就是View在UNSPECIFIED情况下的测量宽/高。

//  View 源码

public int getMinimumWidth() {
        final int intrinsicWidth = getIntrinsicWidth();
        return intrinsicWidth > 0 ? intrinsicWidth : 0;
}

public int getMinimumHeight() {
        final int intrinsicHeight = getIntrinsicHeight();
        return intrinsicHeight > 0 ? intrinsicHeight : 0;
}

从以上的代码来看,返回的是Drawable的原始宽度/高度,前提是这个Drawable 有原始宽/高度,否则就返回0;举个例子,ShapeDrawable 无原始宽/高;而BitmapDrawable有原始宽/高(图片的尺寸)。

Question: 直接继承View 的自定义控件需要重写onMeasure 方法并设置wrap_content 时自身大小,否则在布局中使用 wrap_content就相当于使用 match_parent?

Why: 如果View在布局中使用wrap_content,那么它的specMode是AT_MOST模式,在这种模式下,它的宽/高等于specSize;这种情况下View的specSize是parentSize,而parentSize是父容器中目前可以使用大小,也就是父容器当前剩余的空间大小。View的宽/高就等于父容器当前剩余的空间大小。

Answer :

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec,heightMeasureSpec);
    int widthSpecMode=MeasureSpec.getMode(widthMeasureSpec);
    int widthSpecSize=MeasureSpec.getSize(widthMeasureSpec);
    
    
    int heightSpecMode=MeasureSpec.getMode(heightMeasureSpec);
    int heightSpecSize=MeasureSpec.getSize(heightMeasureSpec);
    
    if(widthSpecMode==MeasureSpec.AT_MOST &&heightSpecMode==MeasureSpec.AT_MOST){
        setMeasuredDimension(mWidth,mHeight);
    }else if(widthSpecMode==MeasureSpec.AT_MOST){
        setMeasuredDimension(mWidth,heightSpecSize);
    }else if(heightSpecMode==MeasureSpec.AT_MOST){
        setMeasuredDimension(widthSpecSize,mHeight);
    }
    
}

在上面的代码中,只需要给View指定一个默认的内部宽/高(mWidth 和mHeight),并在wrap_content时设置此宽/高即可。对于非wrap_content时,用系统的测量值即可。至于这个默认的内部宽/高的大小如何指定,这个没有固定的依据,根据需要灵活指定即可

ViewGroup 的measure 过程

对于ViewGroup来说,除了完成自己的measure 过程外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个过程。ViewGroup是一个抽象类,因此它没有重写View 的onMeasure方法,但提供了measureChildren 的方法。

// ViewGroup 源码

// 遍历子View & 调用measureChild()进行下一步测量
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            // 调用measureChild()进行下一步的测量
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
}

protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        //  获取子视图的布局参数    
        final LayoutParams lp = child.getLayoutParams();

        // 根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);
        // 将计算好的子View的MeasureSpec值传入measure(),进行最后的测量        
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            
}

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        // specSize 父容器的大小
        int specSize = MeasureSpec.getSize(spec);
        // padding 父容器中已占用的空间大小
        // size 子元素可用空间大小
        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

从以上的代码来看,ViewGroup在measure时,会对每一个子元素会调用measureChild方法进行测量。measureChild方法就是取出子元素的LayoutParams,然后再通过getChildMeasureSpec来创建子元素的MeasureSpec,接着将MeasureSpec直接传递给View的measure方法进行测量。

下面具体分析实例:LinearLayout 源码measureHorizontal

// LinearLayout 源码

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

void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) {
     ....
     // 获取垂直方向上的子View个数
     final int count = getVirtualChildCount();
     ....
     for (int i = 0; i < count; ++i) {
        
        final View child = getVirtualChildAt(i);
        
        // 子View不可见,直接跳过该View的measure过程,
        // getChildrenSkipCount()返回值恒为0
        if (child.getVisibility() == GONE) {
                i += getChildrenSkipCount(child, i);
                continue;
           }
        
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        
        // 记录子View是否有weight属性设置,用于后面判断是否需要二次measure
        totalWeight += lp.weight;
        
         final boolean useExcessSpace = lp.width == 0 && lp.weight > 0;
         if (widthMode == MeasureSpec.EXACTLY && useExcessSpace) {
         
             if (isExactly) {
                    mTotalLength += lp.leftMargin + lp.rightMargin;
                } else {
                    final int totalLength = mTotalLength;
                    mTotalLength = Math.max(totalLength, totalLength +
                            lp.leftMargin + lp.rightMargin);
                }
                
              ...  
         }else{
             ...
             
             final int usedWidth = totalWeight == 0 ? mTotalLength : 0;
             // 该方法内部最终会调用measureChildren(),从而遍历所有子View & 测量
             measureChildBeforeLayout(child, i, widthMeasureSpec, usedWidth,
                        heightMeasureSpec, 0);
             
             if (isExactly) {
                    mTotalLength += childWidth + lp.leftMargin + lp.rightMargin
                            + getNextLocationOffset(child);
                } else {
                    final int totalLength = mTotalLength;
                    mTotalLength = Math.max(totalLength, totalLength + childWidth + lp.leftMargin
                            + lp.rightMargin + getNextLocationOffset(child));
                }         
         }
         
     }
     
    if (useLargestChild &&
                (widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED)) {
            mTotalLength = 0;

            for (int i = 0; i < count; ++i) {
                final View child = getVirtualChildAt(i);
                ...

                if (child.getVisibility() == GONE) {
                    i += getChildrenSkipCount(child, i);
                    continue;
                }

                final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
                        child.getLayoutParams();
                if (isExactly) {
                    mTotalLength += largestChildWidth + lp.leftMargin + lp.rightMargin +
                            getNextLocationOffset(child);
                } else {
                    final int totalLength = mTotalLength;
                    mTotalLength = Math.max(totalLength, totalLength + largestChildWidth +
                            lp.leftMargin + lp.rightMargin + getNextLocationOffset(child));
                }
            }
        }
    
    // mTotalLength用于存储LinearLayout在水平方向的长度
    mTotalLength += mPaddingLeft + mPaddingRight;
    
    
    int widthSize = mTotalLength;
    int widthSizeAndState = resolveSizeAndState(widthSize, widthMeasureSpec, 0);
    widthSize = widthSizeAndState & MEASURED_SIZE_MASK;
    
    maxHeight += mPaddingTop + mPaddingBottom;

    // Check against our minimum height
    maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
     
    setMeasuredDimension(widthSizeAndState | (childState&MEASURED_STATE_MASK),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        (childState<<MEASURED_HEIGHT_STATE_SHIFT)));
}

测量流程:
在这里插入图片描述

总结

  • 对于 View 来说,其 MeasureSpec 是由其父容器 ViewGroup 的 MeasureSpec 和 View 自身的 LayoutParams 来共同决定的。
  • 对于DecorView,其MearsureSpec是窗口尺寸和其自己的LayoutParames共同决定。

最后

以上就是落寞中心为你收集整理的Android 自定义控件基础:measure过程的全部内容,希望文章能够帮你解决Android 自定义控件基础:measure过程所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(51)

评论列表共有 0 条评论

立即
投稿
返回
顶部