View的工作原理(二)Measure过程

view的工作流程主要是指measure、layout、draw这三大流程,即测量,布局、绘制。measure决定view测量的宽高,layout决定view的最终宽高和摆放位置,draw将view绘制到屏幕上。

一、 Measure过程

measure 过程要分两种情况:
1、 View。如果是 View 的话,那么只通过 measure 方法就完成其测量过程
2、 ViewGroup。但是如果是 ViewGroup 的话,不仅需要完成自己的测量过程,还需要完成它所有子 View 的测量过程。如果子 View 又是一个 ViewGroup,那么继续递归这个流程。

1、view的measure过程

view的measure源码:

  public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
  ...........
  if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }
    ..........
}

1、这个方法在viewGroup会被调用
2、measure是一个final方法不允许继承,具体测量操作在onMeasure中,接着看onMeasure:

// 参数 宽高的MeasureSpec
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   // 参数,宽高大小 及其 MeasureSpec
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

可以看出这里传入宽高的MeasureSpec即可,具体的测量是由setMeasuredDimension完成的。我们看下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;
    }

逻辑很简单我们只需要来看AT_MOST、EXACTLY
其实getDefaultSize返回值就是MeasureSpec中的specSize这个specSize就是view测量后的大小,view的最终大小在layout阶段确定,但是几乎所有情况下view的测量大小和最终大小相同。

看到这我们知道AT_MOST和EXACTLY下specSize值是一致的了,所以这里我们一般对AT_MOST处理。 上篇栗子处理:(问题参考上篇:View的工作原理(一)初认识ViewRoot、DecorView,理解MeasureSpec)

解决:

package com.example.administrator.androidview;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;

/**
 * Create by SunnyDay on 2019/04/12
 */
public class MyLayout extends View {
    public MyLayout(Context context) {
        super(context);
    }

    public MyLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    /**
     * @param heightMeasureSpec 本view高的MeasureSpec(数值的获取调用参考上一篇文章)
     * @param widthMeasureSpec  本view宽的MeasureSpec
     *
     *                          这两个值在父容器中已经获得,父容器已经计算出。
     *
     * @function 重写view的onMeasure  处理MeasureSpec.AT_MOST这种模式
     *上篇文章: https://blog.csdn.net/qq_38350635/article/details/89230661
     * */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        // 获得子view的宽高
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        int heighthMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

         // 用户宽高都设置为 wrap_content时
        if (widthMode == MeasureSpec.AT_MOST && heighthMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(100,100);//告知系统测量,我们随便写的,这个值由自己决定。
        }
        // 当宽设置了wrap_content时  (处理宽,高使用计算值即可。)
        else if (widthMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(100,heightSize);
        }
         // 当高设置了wrap_content时  (处理高,宽使用计算值即可。)
        else if (heighthMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSize,100);
        }

    }
}


在这里插入图片描述

1、可以看到我们重写了,onMeasure方法,特殊处理了子view设置wrap_content时这种状况。
只是只要我们设置wrap_content 时,就相当于我们设置了100(参看我们的代码)
2、可以看出我们只需对AT_MOST就行处理就行了,其他的模式数值就是MeasureSpec.getSize(MeasureSpec)我们可以直接在setMeasuredDimension中使用

2、viewGroup的measure过程

1、ViewGroup 的 measure 过程 和 View 不同,不仅需要完成自身的 measure 过程,还需要去遍历所有子 View 的 measure 方法,各个子元素之间再递归这个流程。
2、viewGroup为抽象类,没有重写view的onMeasure方法。但是他提供了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);
            }
        }
    }

很容易看懂,循环遍历子view,当子view没有gone时逐个测量子view。继续看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);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

很好看出根据父容器的MeasureSpec、自身布局参数、自身的padding,margain,这些因素得出子view的MeasureSpec。具体使通过getChildMeasureSpec获得的。(上篇已经分析getChildMeasureSpec这个函数)最后根据上面计算出的子view的MeasureSpec测量子view。

注意:viewGroup并没有实现具体的测量过程,因为viewGroup为抽象类,其测量过程需要各个子类实现(比如LinearLayout的onMeasure)

3、LinearLayout 测量源码栗子:
 @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);
            ......
            //测量子view
            measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                        heightMeasureSpec, usedHeight);
            final int childHeight = child.getMeasuredHeight();
            final int totalLength = mTotalLength;//LinearLayout 在竖直方向上的高度
                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                       lp.bottomMargin + getNextLocationOffset(child));
    }

}

......
开始测量自身
/ Add in our padding
        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);
        heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
        ......
         setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                heightSizeAndState);

代码太多了,这里我们看一下整个LinearLayout 的源码几乎都是measureVertical、measureHorizontal这两个方法了。
1、如上我们贴了局部代码,,measureVertical这个方法中会循环子元素,对每个子元素执行measureChildBeforeLayout测量每个view,具体的测量步骤在这个方法内部的measure中。
2、接着用mTotalLength记录竖直方向的高度,没测量一个子元素,这个高度就增加,增加的部分就是子元素高度+子元素在竖直方向上的margain等
3、子元素测量完成后LinearLayout 开始测量自身。

思想借鉴(主要是第2条)
当子元素测量完成后,LinearLayout 会根据子元素情况来测量自身大小
针对竖直的LinearLayout 而言(measureVertical),他在水平方向上的测量过程遵循view的测量过程,在竖直方向上的侧量过程有所处理,具体来讲就是:
1、如果他的布局采用了具体值(dp)或者match_parent,那么他的测量过程和view一致,数值就是SpecSize。
2、如果他的布局采用了wrap_content,他就特殊处理了,高度为所有子元素占用高度总和,但是仍然不能大于父容器的剩余空间,当然他的最终高度还要考虑其竖直方向上的padding。

4、measure小结

measure过程是view的三大流程中最复杂的一个,measure完成后可以获得view的宽高:

  • 宽:getMeasureWidth()
  • 高:getMeasureHeight()
    ps:某些极端情况下系统会多次测量,所以良好的习惯是在onLayout中去获得view的宽高
5、特殊栗子

栗子:我们想在activity启动时就去做某件事,但是这件事需要进行获得view的宽高。
我们想:在onCreate、onResume、onStart中调用getMeasureWidth、getMeasureHeight这so easy,真的是这样吗?实际上在这几个方法中是无法正确获得结果的。
原因解释:view的measure和activity的生命周期不是同步执行的,无法保证activity执行这些生命周期时view已经测量完毕,如果没测量完毕获得宽高就是0。

解决方法(四种):
  • activity、view的onWindowFocusChanged
  • view的post(runnable)
  • ViewTreeObserver
  • View.measure(widthMeasureSpec,heightMeasureSpec)
具体使用

(1)使用activity、view的onWindowFocusChanged方法

该方法会在当前 Activity 的 Window 获得或失去焦点的时候回调,当回调该方法时,表示 Activtiy 是完全对用户可见的,这时候 View 已经初始化完毕、宽/高都已经测量好了,这时就能获取到宽/高了。
ps:注意这个方法会调用多次,每次acticity失去获得焦点都会调用

@Override
public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    if(hasFocus){
        int measuredWidth = view.getMeasuredWidth();
        int measuredHeight = view.getMeasuredHeight();
    }
}

(2)view的post(runnable)

该方案,通过 post 方法将一个 runnable 投递到消息队列的底部,然后等待 Looper 调用该 runnable 时,View 也已经初始化好了,这时就能获取到宽/高了。

view.post(new Runnable() {
    @Override
    public void run() {
        int measuredWidth = view.getMeasuredWidth();
        int measuredHeight = view.getMeasuredHeight();
    }
});

(3)ViewTreeObserver

使用ViewTreeObserver的众多回调可以完成这个功能,比如OnGlobalLayoutListener这个接口。
当view树的状态发生改变,或者view的可见性发生改变onGlobalLayout会被回调。需要注意的时,该回调会被调用多次,所以这里在第一次回调中,就移除了监听,避免多次获取。

ViewTreeObserver treeObserver = view.getViewTreeObserver();
treeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
        int measuredWidth = view.getMeasuredWidth();
        int measuredHeight = view.getMeasuredHeight();
    }
});

(4)(View.measure(widthMeasureSpec,heightMeasureSpec))

该方案是通过手动对 View 进行 measure 来得到 VIew 的宽/高,比较复杂因为要根据 View 的 LayoutParams 分情况来处理(我们还是使用前三种方便

如果 View 的宽高是写死的,比如都是 100px,那么可以通过如下方式获取:

int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);

int measuredWidth = view.getMeasuredWidth();
int measuredHeight = view.getMeasuredHeight();

如果view设置了wrap_content那么可以通过如下方式获取:

int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec( (1 << 30) - 1, View.MeasureSpec.AT_MOST);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec( ( 1 << 30) - 1, View.MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec, heightMeasureSpec);

int measuredWidth = view.getMeasuredWidth();
int measuredHeight = view.getMeasuredHeight();

需要注意的是,这里的 (1 << 30) -1,是 View 尺寸所能支持的最大的值。通过上篇分析 MeasureSpec 我们知道,View 的尺寸是用30位2进制来表示的,也就是说是 30 个 1,也就是 (1 << 30) -1,所以在最大测量模式下,我们用 View 理论上能支持的最大值去构建 MeasureSpec 是合理的。

如果 View 设置了 match_parent,那么是无法获取具体的宽/高的,因为通过前面的了解,我们已经知道,构建 MeasureSpec 时需要 parentSize,也就是父控件的大小,但是我们是不知道的,所以遇到这种情况可以直接放弃使用该方案来获取 View 的宽/高。

二、小结

onMeasure甄姬复杂,看着书迷迷糊糊总结了一遍,以后再回头看就不在出入门庭了。。。
参考文章

The end

本文来自<安卓开发艺术探索>笔记总结

猜你喜欢

转载自blog.csdn.net/qq_38350635/article/details/89279993