自定义View (二) :View的工作流程(Measure)

本文详细探讨了Android自定义View的measure过程,包括View和ViewGroup的测量机制。重点讲解了View的onMeasure方法、measure方法的默认行为,以及在UNSPECIFIED模式下的尺寸计算。同时,提到了ViewGroup如何遍历并测量其子元素,以LinearLayout为例展示了具体的实现。此外,还讨论了在实际应用中如何处理wrap_content和match_parent的测量问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

View的工作流程主要指measure,layout,draw三大流程:

View的measure过程:View的measure过程由其measure方法来完成,measure方法是一个final类型的方法因此子类不能重写此方法,在View中measure方法会调用View的onMeasure方法,因此只需看onMeasure的实现即可.

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
setMeasuredDimension方法会设置View宽高的测量值即getDefaultSize方法或取到的值:

/**
     * Utility to return a default size. Uses the supplied size if the
     * MeasureSpec imposed no constraints. Will get larger if allowed
     * by the MeasureSpec.
     *
     * @param size Default size for this view
     * @param measureSpec Constraints imposed by the parent
     * @return The size this view should be.
     */
    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }
由于UNSPECIFIED一般用于系统测量,我们只需要注意AT_MOST,EXACTLY两种情况.即getDefaultSize返回的大小是measureSpec中specSize,这个specSize就是View的测量后的大小,因为View的最终大小是在layout阶段确定的所以只能说是测量后的大小,但是几乎所有情况下View的测量大小和最终大小是相等的.

在UNSPECIFIED情况下View的大小为getDefaultSize的第一个参数即getSuggestedMinimumWidth和getSuggestedMinimumHight两个方法的返回值

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这个属性所指定的值,如果没有指定则默认值为0;如果View有背景,则View的宽度为max(mMinWidth,mBackground.getMinimumWidth()).

Drawable的getMinimumWidth方法

public int getMinimumHeight() {
        final int intrinsicHeight = getIntrinsicHeight();
        return intrinsicHeight > 0 ? intrinsicHeight : 0;
    }
getMinimumWidth返回的是Drawable的原始高度,如果没有原始高度则返回0,ShapeDrawable无原始宽高,BitmapDrawable有原始宽高.

即getSuggestedMinimumWidth方法可总结为:如果View没有背景返回android:minWidth属性值,值可为0;如果View有背景则返回android:minWidth和背景的最小宽度这两者中最大的值,在UNSPECIFIED情况则下返回View测量宽高.

getDefaultSize方法可以看出View的大小是由specSIze决定的,如果直接继承VIew的自定义控件要重写onMeasure方法wrap_content的大小,否则布局中wrap_content就相当于match_parent,因为如果View布局中使用wrap_content,他的specMode为At_MOST模式,对应的specSize为parentSize纪委父容器中目前可以使用的大小也是父容器中剩余空间的大小,此时View的宽高等于父容器当前剩余空间的大小,这种情况和布局中使用match_parent完全一致,解决方法:

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(widthSpecSize,mHeight);
        }else if(heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(mWidth,heightSpecSize);
        }
    }

mWidth和mHeight根据需要灵活指定.


ViewGroup的Measure过程:

ViewGroup除了完成自己的measure过程还要遍历调用所有子元素的measure方法,各个子元素再递归执行这个过程,和View不同ViewGroup是一个抽象类,因此没有重写View的onMeasure方法,但是提供了一个measureChildren的方法

/**
     * Ask all of the children of this view to measure themselves, taking into
     * account both the MeasureSpec requirements for this view and its padding.
     * We skip children that are in the GONE state The heavy lifting is done in
     * getChildMeasureSpec.
     *
     * @param widthMeasureSpec The width requirements for this view
     * @param heightMeasureSpec The height requirements for this view
     */
    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(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }
measureChild方法:

/**
     * Ask one of the children of this view to measure itself, taking into
     * account both the MeasureSpec requirements for this view and its padding.
     * The heavy lifting is done in getChildMeasureSpec.
     *
     * @param child The child to measure
     * @param parentWidthMeasureSpec The width requirements for this view
     * @param parentHeightMeasureSpec The height requirements for this view
     */
    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);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
measureChild逻辑即为取出子元素的LayoutParams,再通过getChildMeasureSpec来创建子元素的MeasureSpec,接着将MeasureSpec传递给View的measure方法来进行测量.

ViewGroup没有定义其具体的测量过程是由于ViewGroup是一个抽象类,onMeasure方法需要其子类去具体实现,以LinearLayout的onMeasure方法分析ViewGroup的measure过程:

LinearLayout的onMeasure方法:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }
measureVertical方法大概逻辑为遍历子元素对每个子元素执行measureChildBeforeLayout方法,这个方法内部会调用子元素的measure方法,这样子元素开始进入measure过程,并且系统会通过mTotalLength这个变量存储LinearLayout在竖直方向的初步高度.梅测量一个子元素mTotalLength都会增加,包括子元素的高度和子元素在竖直方向上的margin等值,测量完子元素后,LineraLayout会测量自己的大小.对于竖直方向的LinearLayout,它的水平方向遵循View的测量过程,在竖直方向如果是match_parent或者具体数值,它的测量过程会和View的测量一致,即高度为specSize;如果布局中采用的是wrap_content,那么它的高度为所有子元素说占用高度的总和,但仍不能超过它的父容器的剩余空间,它的高度最终还要考虑其竖直方向上的padding参考源码

/**
     * Utility to reconcile a desired size and state, with constraints imposed
     * by a MeasureSpec.  Will take the desired size, unless a different size
     * is imposed by the constraints.  The returned value is a compound integer,
     * with the resolved size in the {@link #MEASURED_SIZE_MASK} bits and
     * optionally the bit {@link #MEASURED_STATE_TOO_SMALL} set if the resulting
     * size is smaller than the size the view wants to be.
     *
     * @param size How big the view wants to be
     * @param measureSpec Constraints imposed by the parent
     * @return Size information bit mask as defined by
     * {@link #MEASURED_SIZE_MASK} and {@link #MEASURED_STATE_TOO_SMALL}.
     */
    public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize =  MeasureSpec.getSize(measureSpec);
        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
            if (specSize < size) {
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result | (childMeasuredState&MEASURED_STATE_MASK);
    }
一般情况下在measure完成后即可通过getMeasureWidth/Height获取到正确的View的测量宽高,但是在特殊情况下系统可能通过多次的measure才能最终确定宽高,这种情况下onMeasure方法中拿到的测量宽高可能不准确,因此比较好的是在onLayout方法中获取View的最终宽高.

View的宽高获取方法

1,onWindowFocusChanged方法含义为View已经初始化完毕,需注意onWindowFocusChanged会被多次调用,当Activity的窗口得到或失去焦点都会被调用一次,如果频繁的进行onResume和onPause,onWindowFocusChanged会被频繁调用示例代码如下:

public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if(hasFocus){
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    }
2,view.post(runnable)

通过post可以将一个runnable投递到消息队列的尾部,然后等待looper调用此runnable,此时View已经初始化完成,示例代码如下:

protected void onStart() {
        super.onStart();
        view.post(new Runnable() {
            @Override
            public void run() {
                int width = view.getMeasuredWidth();
                int height = view.getMeasuredHeight();
            }
        });
    }
3.ViewTreeObserver

使用ViewTreeObserver的众多回调接口完成此功能,例如OnGlobalLayoutListener,当View树的状态发生改变或者View树内部的View可见性发生改变时,onGlobalLayout方法会被回调,伴随着View树的状态改变onGlobalLayout会被多次调用,示例代码如下:

protected void onStart() {
        super.onStart();
        ViewTreeObserver observer = view.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                int width = view.getMeasuredWidth();
                int height = view.getMeasuredHeight();
            }
        });
    }

4,view.measure(int widthMeasureSpec , int heightMeasureSpec).

match_parent无法measure出具体宽高,构造MeasureSpec需要知道parentSize而此时无法知道父容器的剩余空间即parentSize

具体数值(dp/px)例如宽高都为100px获取方法如下:

int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
                int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
                view.measure(widthMeasureSpec,heightMeasureSpec);
wrap_content
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
                int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
                view.measure(widthMeasureSpec,heightMeasureSpec);
View的尺寸使用30位二进制表示,也就是最大是30个1(2^30-1),即(1<<30)-1,在最大化的模式下使用View理论上能支持最大值构造MeasureSpec.








评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值