Android View的工作流程(二) measure过程

 一.View的measure过程

View的measure过程是由View的measure方法完成的,他是一个被final关键字修饰的方法,我们无法重写该方法,但是measure方法中会调用onMeasure方法来设置计算后的宽高,onMeasure方法是可以被重写的:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

getDefaultSize方法:

    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;
    }

可以看到EXACTLY和AT_MOST两种模式下都是以specSize作为返回值,而这个specSize就是View测量后的大小。如果View采用AT_MOST模式即wrap_content来绘制那么结合上一篇文章中的图例:

 可以知道View最终在父布局中的绘制会以parentSize作为specSize的实际大小,即我们自定义的直接继承自View的View在布局中使用wrap_content的效果和match_parent是一样的,而解决这个问题的方式就需要重写onMeasure方法来对wrap_content做特殊处理

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val widthSpecMode = MeasureSpec.getMode(widthMeasureSpec)
        val widthSpecSize = MeasureSpec.getSize(widthMeasureSpec)
        val heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)
        val 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)
        }
    }

重写的onMeasure方法中我们需要提供一个View在wrap_content情况下使用的宽高mWidth和mHeight,非wrap_content的场景下就直接使用系统提供的测量值widthSpecSize/heightSpecSize即可。具体mWidth和mHeight该怎么取值要根据实际使用场景来定,参考TextView的onMeasure方法部分源码:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int width;
        int height;

        ...

        if (widthMode == MeasureSpec.EXACTLY) {
            // Parent has told us how big to be. So be it.
            width = widthSize;
        } else {//AT_MOST

            ...

            if (boring == null || boring == UNKNOWN_BORING) {
                if (des < 0) {
                    des = (int) Math.ceil(Layout.getDesiredWidthWithLimit(mTransformed, 0,
                            mTransformed.length(), mTextPaint, mTextDir, widthLimit));
                }
                width = des;
            } else {
                width = boring.width;
            }

            final Drawables dr = mDrawables;
            if (dr != null) {
                width = Math.max(width, dr.mDrawableWidthTop);
                width = Math.max(width, dr.mDrawableWidthBottom);
            }
            
            ...
        }

        ...

        if (heightMode == MeasureSpec.EXACTLY) {
            // Parent has told us how big to be. So be it.
            height = heightSize;
            mDesiredHeightAtMeasure = -1;
        } else {//AT_MOST
            int desired = getDesiredHeight();

            height = desired;
            mDesiredHeightAtMeasure = desired;

            if (heightMode == MeasureSpec.AT_MOST) {
                height = Math.min(desired, heightSize);
            }
        }
        
        ...
        
        setMeasuredDimension(width, height);
    }

可以看到TextView对AT_MOST模式下的宽高都进行了重新定义,具体赋值逻辑太过复杂就不细说了。

二.ViewGroup的measure过程

ViewGroup是一个继承自View的抽象类,它并没有实现onMeasure方法,这是因为ViewGroup作为一个父类他不能对不同需求和场景下的子类布局作统一测量,就像LinearLayout和RelativeLayout一样是两种完全不同的布局方式,他们的测量方式需要他们自己去实现。但是ViewGroup也提供了一个measureChildren方法:

    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方法来测量子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方法和上一篇文章提到的measureChildWithMargins方法原理是一样的,只不过没有把子View的外边距加进去。不过这个measureChildren方法在已知的几种布局中只在AbsoluteLayout布局中有使用,而AbsoluteLayout作为最简单粗暴的一种布局也是几乎没有使用场景,所以可以看出对于子View的测量LinearLayout、RelativeLayout等常用布局都是需要自己去实现的。下面以LinearLayout的onMeasure方法为例:

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

measureVertical方法:

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    ...

    for (int i = 0; i < count; ++i) {
        final View child = getVirtualChildAt(i);
        if (child == null) {
            mTotalLength += measureNullChild(i);
            continue;
        }

        ...
        
        if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
                // Optimization: don't bother measuring children who are only
                // laid out using excess space. These views will get measured
                // later if we have space to distribute.
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
                skippedMeasure = true;
            } else {
                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;
                measureChildBeforeLayout(child, i, widthMeasureSpec, 0, heightMeasureSpec, usedHeight);

                final int childHeight = child.getMeasuredHeight();
                if (useExcessSpace) {
                    // Restore the original height and record how much space
                    // we've allocated to excess-only children so that we can
                    // match the behavior of EXACTLY measurement.
                    lp.height = 0;
                    consumedExcessSpace += childHeight;
                }

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

可以看到LinearLayout会按序遍历每一个子View,并调用measureChildBeforeLayout方法来测量子View,测量完成后获取子View的measuredHeight累加到mTotalLength中作为最后LinearLayout的总高度。所有的子View都测量完成后,LinearLayout会用mTotalLength来测量自身的高度:

mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;

// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

// Reconcile our calculated size with the heightMeasureSpec
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);

setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                heightSizeAndState);

这里的resolveSizeAndState方法就是对不同测量模式下的LinearLayout高度分情况处理:

    public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
        final int specMode = MeasureSpec.getMode(measureSpec);
        final int specSize = MeasureSpec.getSize(measureSpec);
        final int result;
        switch (specMode) {
            case MeasureSpec.AT_MOST:
                if (specSize < size) {
                    result = specSize | MEASURED_STATE_TOO_SMALL;
                } else {
                    result = size;
                }
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
            case MeasureSpec.UNSPECIFIED:
            default:
                result = size;
        }
        return result | (childMeasuredState & MEASURED_STATE_MASK);
    }

当LinearLayout采用match_parent时就直接使用测量的specSize,如果采用wrap_content就使用累加得到的总高度值mTotalHeight,当然这个值也要小于等于父布局给的剩余高度,否则仍然以specSize作为LinearLayout的最终高度。

三.获取measure后的宽高

对于获取measure后的宽高View直接给我们提供了getMeasuredWidth/getMeasuredHeight方法,但是应该在什么时候使用这俩方法呢,首先View在很多情况下会出现多次测量的情况,所以在onMeasure方法中获取的measuredWidth/measuredHeight往往并不是最终正确的宽高,而onLayout是在onMeasure完全结束的情况下执行的,所以一般我们会在onLayout方法中去拿到最终的measuredWidth/measuredHeight。

如果在activity的声明周期方法里面去getMeasuredWidth/getMeasuredHeight会得到正确的宽高吗?答案往往是否定的,因为整个measure的过程和页面的生命周期并没有绑定,当我们在onStart、onResume方法里面去获取宽高时可能View还没有measure结束而获取到一个默认值0。Android为我们提供了以下几种方式去拿到正确的measuredWidth/measuredHeight:

1.onWindowFocusChanged

class DemoView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {

    ...

    override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
        super.onWindowFocusChanged(hasWindowFocus)
        if (hasWindowFocus) {
            val width = this.measuredWidth
            val height = this.measuredHeight
        }
    }
    
    ...

}

这个回调在activity中也是可以设置的,但是需要注意的是它和activity的生命周期存在关联,可能会出现频繁回调的情况,当activity频繁的触发onResume和onPause时onWindowFocusChanged也会频繁的触发回调

2.view.post

View通过post方法把一个Runnable任务加到主线程消息队列的末尾,当这个Runnable执行时View早已经初始化好了:

btn1.post {
    val width = btn1.measuredWidth
    val height = btn1.measuredHeight
}

3.ViewTreeObserver

ViewTreeObserver提供了很多和视图树状态有关的接口,很多都是可以用来获取measuredWidth/measuredHeight,以OnGlobalLayoutListener为例:

class DemoView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
    
    ...    

    init {
        viewTreeObserver.addOnGlobalLayoutListener(object:ViewTreeObserver.OnGlobalLayoutListener{
            override fun onGlobalLayout() {
                viewTreeObserver.removeOnGlobalLayoutListener(this)
                val width = this@DemoView.measuredWidth
                val height = this@DemoView.measuredHeight
            }
        })
    }
    
    ...
}

当view树的状态改变或者里面的view可见性发生变化都会触发OnGlobalLayoutListener回调,此时measuredWidth/measuredHeight将是准确的。

4.measure(int widthMeasureSpec, int heightMeasureSpec)

通过手动对View进行measure来指定width和height,但是这里需要根据LayoutParams来分情况处理:

.match_parent

这种情况下无法直接手动measure,因为我们需要知道父布局的剩余空间大小parentSize,而在当前View中我们是无法知道父布局的剩余空间大小情况的

.具体数值dp/px

例如宽高都是100px:

class DemoView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
    
    ...    

    init {
        val widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY)
        val heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY)
        this.measure(widthMeasureSpec, heightMeasureSpec)
    }
    
    ...
}

.wrap_content

class DemoView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
    
    ...    

    init {
        val widthMeasureSpec = MeasureSpec.makeMeasureSpec((1 shl 30) - 1, MeasureSpec.AT_MOST)
        val heightMeasureSpec = MeasureSpec.makeMeasureSpec((1 shl 30) - 1, MeasureSpec.AT_MOST)
        this.measure(widthMeasureSpec, heightMeasureSpec)
    }
    
    ...
}

当使用AT_MOST模式时,我们可以指定specSize为其所能达到的最大值即measureSpec表示具体尺寸的后三十位全为1,所以specSize=1*10的30次方-1,用kotlin的代码表示就是(1 shl 30) - 1,java的代码表示就是(1 << 30) - 1。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我们间的空白格

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值