欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

View的测量、布局及绘制过程

程序员文章站 2022-07-13 14:56:13
...

一、Activity、Window及DecorView关系

  • Activity:页面
  • Window:交互窗口,是一个抽象类
  • PhoneWindow:Window的唯一实现类
  • DecorView:一个页面的根View,继承自FrameLayout
  • 既然我们知道整个View的Root是DecorView,那么View的绘制是从哪里开始的呢,我们知道每个Activity 均会创建一个 PhoneWindow对象,是Activity和整个View系统交互的接口,每个Window都对应着一个View和一个ViewRootImpl,Window和View通过ViewRootImpl来建立联系。对于Activity来说,ViewRootImpl是连接WindowManager和DecorView的纽带,绘制的入口是由ViewRootImpl的performTraversals方法来发起Measure,Layout,Draw等流程的。
private void performTraversals() { 
    ...... 
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); 
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); 
    ...... 
    // mView就是DecorView
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); 
    ......
    mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
    ...... 
    mView.draw(canvas); 
    ......
}

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

二、setContentView后的ViewTree

View的测量、布局及绘制过程

View的测量、布局及绘制过程

上述的两个图已经很清楚地说明了setContentView后的结构。
View decorView = getWindow().getDecorView();
id是content的就是我们设置的布局的根布局,而DecorView是整个页面的根布局。
FrameLayout contentRootLayout = (FrameLayout)findViewById(android.R.id.content);
content下面id是linear的即是我们XML的布局结构。

三、MeasureSpec参数的理解

对于View的测量,肯定会和MeasureSpec接触,MeasureSpec是两个单词组成,翻译过来就是“测量规格”或者“测量参数”,更精确的说法应该这个MeasureSpec是由父View的MeasureSpec和子View的LayoutParams通过简单的计算得出一个针对子View的测量要求,这个测量要求就是MeasureSpec。
大家都知道一个MeasureSpec是一个大小跟模式的组合值,MeasureSpec中的值是一个整型(32位)将size和mode打包成一个Int型,其中高两位是mode,后面30位存的是size,是为了减少对象的分配开支。

MeasureSpec一共有三种模式:

  • EXACTLY: 父容器已经为子容器设置了尺寸,子容器应当服从这些边界,不论子容器想要多大的空间。
  • AT_MOST:子容器可以是声明大小内的任意大小
  • UPSPECIFIED : 父容器对于子容器没有任何限制,子容器想要多大就多大。这种一般用于系统内部,表示一种测量状态。

如果从代码上来看view.measure(int widthMeasureSpec, int heightMeasureSpec) 的两个MeasureSpec是父类传递过来的,但并不是完全是父View的要求,而是父View的MeasureSpec和子View自己的LayoutParams共同决定的,而子View的LayoutParams其实就是我们在xml写的时候设置的layout_width和layout_height 转化而来的。
View的测量、布局及绘制过程

四、Measure过程

1、单纯一个View的Measure过程

View的测量过程其实是由measure方法来完成,这个方法是一个final类型,就说明是不可以被重写的,因此所有ViewGroup及Layout里是没有该方法的。
measure方法中会调用onMeasure方法,我们来看看View里的onMeasure方法源码

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

/**
* 根据父控件传递下来的spec,来计算出当前这个View的尺寸
* 当模式是EXACTLY和AT_MOST时,View设置的大小即为父控件里传递下来的specSize
* 当模式是UNSPECIFIED时,使用当前View的默认大小(这个默认大小和是否设置背景及minWidth属性有关)
*/
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;
}

可以得出一个结论:直接继承View的自定义控件,假如我们没有重写onMeasure去处理大小,那么当我们在布局里使用这个自定义控件时,使用wrap_content和match_parent是没有区别的,结果都是match_parent。需要结合上面的4X4表格以及View的getDefaultSize源码来进行分析,当我们的自定义View设置wrap_content时,这个View的测量规格应该是AT_MOST + parentSize,而上述源码我们也知道AT_MOST模式下,这个parentSize即为这个自定义控件设置的最终大小。
解决方案就是,自己重写onMeasure方法,在AT_MOST模式下自己去设置对应宽高即可

2、ViewGroup的Measure过程

ViewGroup是一个抽象类,并没有重写View的onMeasure方法,这是因为其子类(LinearLayout、FrameLayout等)都有不同布局特性及表现形式,因此ViewGroup没法做到统一实现,而交由其各个布局自己去实现onMeasure。
我们就以LinearLayout作为例子看源码:

/**
* LinearLayout的onMeasure方法
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (mOrientation == VERTICAL) {
        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
}

/**
* LinearLayout的measureVertical方法
*/
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    ...
    for (int i = 0; i < count; ++i) {
        final View child = getVirtualChildAt(i);
        final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
        // 去测量和设置子控件的宽高
        measureChildBeforeLayout(child, i, widthMeasureSpec, 0,heightMeasureSpec, usedHeight);
        ...
        // 最后才设置LinearLayout的宽高
        // 通过mTotalLength这个变量来存储LinearLayout在竖直方向累加的高度
        // 最终假如自己是wrap_content,就用这个mTotalLength来设置成自己的高度
        // 最后部分看看resolveSizeAndState源码
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),heightSizeAndState);
        ...
    }
}

/**
* LinearLayout的measureChildBeforeLayout方法
*/
void measureChildBeforeLayout(View child, int childIndex,
        int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
        int totalHeight) {
    measureChildWithMargins(child, widthMeasureSpec, totalWidth,
            heightMeasureSpec, totalHeight);
}

/**
* ViewGroup的measureChildWithMargins方法(不是LinearLayout的)
*/
protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);
    // 最终这里就是调用View的measure方法,View的measure方法上面有介绍
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

/**
* LinearLayout的resolveSizeAndState方法
* 假如该LinearLayout是AT_MOST模式,那么就使用当前内容的累加高度和其父控件给
* 他的最大高度进行比较,取其最小的值。
*/
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);
}

3、测量后获取控件的宽高

在Activity中要去获取控件的宽高,很多时候我们调用getMeasureHeight/Width获取到是0,这是因为我们在onCreate、onStart、onResume中获取时,ViewTree并一定已测量完毕,因为View和Activity的生命周期兵没有任何关系,不能保证。有四种获取方法供参考:

3.1 Activity/View#onWindowFocusChanged,这个方法的含义是View已经初始化完毕了,宽高已经准备好了,这个时候去获取宽高是没有问题的。但是要注意的是onWindowFocusChanged会被调用多次,当Activity的窗口获取的焦点和失去焦点的时候均会调用。
3.2 view.post(runnable),把一个消息放到消息队列的尾部,然后等待主线程的Looper去执行。
3.3 ViewTreeObserver,view树的状态发生改变时,会进行回调,注意其里的方法(比如onGlobalLayoutListener会被多次回调)
mView.getViewTreeObserver().addOnGlobalLayoutListener(new XXXListener) {
    mView.getViewTreeObserver().removeGlobalLayoutListener();
    int width = mView.getMeasuredWidth();
}
3.4 通过调用View的measure方法进行测量,比如
/*
* 具体数值
*/
int widthSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec(200, View.MeasureSpec.EXACTLY);
mActiveAreaLayout.measure(widthSpec, heightSpec);

/*
* wrap_content
* 1<<30 - 1 = 2的30次方-1 = Spec能存放的最大值 
*/
int widthSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST);
int heightSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST);
mActiveAreaLayout.measure(widthSpec, heightSpec);

五、Layout过程

1、单纯一个View的Layout过程

/**
* View的layout方法
*/
public void layout(int l, int t, int r, int b) {
    if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
        onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }

    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;
    // 设置自己的左上右下位置
    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        // 调用onLayout方法,而View自己是没有实现onLayout方法的
         onLayout(changed, l, t, r, b);

        if (shouldDrawRoundScrollbar()) {
            if(mRoundScrollbarRenderer == null) {
                mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
            }
        } else {
            mRoundScrollbarRenderer = null;
        }

        mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLayoutChangeListeners != null) {
            ArrayList<OnLayoutChangeListener> listenersCopy =
                    (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
            int numListeners = listenersCopy.size();
            for (int i = 0; i < numListeners; ++i) {
                listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
            }
        }
    }

2、ViewGroup的Layout过程

和measure一样,ViewGroup也是没有实现onLayout方法,而是交由其子类(LinearLayout/FrameLayout等)来实现,我们仍然以LinearLayout作为例子:

/**
* LinearLayout的onLayout方法
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    if (mOrientation == VERTICAL) {
        layoutVertical(l, t, r, b);
    } else {
        layoutHorizontal(l, t, r, b);
    }
}

/**
* LinearLayout的layoutVertical方法
*/
void layoutVertical(int left, int top, int right, int bottom) {
    ...
    for (int i = 0; i < count; i++) {
        final View child = getVirtualChildAt(i);
        if (child == null) {
            childTop += measureNullChild(i);
        } else if (child.getVisibility() != GONE) {
            final int childWidth = child.getMeasuredWidth();
            final int childHeight = child.getMeasuredHeight();

            final LinearLayout.LayoutParams lp =
                    (LinearLayout.LayoutParams) child.getLayoutParams();

            int gravity = lp.gravity;
            if (gravity < 0) {
                gravity = minorGravity;
            }
            final int layoutDirection = getLayoutDirection();
            final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
            switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                case Gravity.CENTER_HORIZONTAL:
                    childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                            + lp.leftMargin - lp.rightMargin;
                    break;

                case Gravity.RIGHT:
                    childLeft = childRight - childWidth - lp.rightMargin;
                    break;

                case Gravity.LEFT:
                default:
                    childLeft = paddingLeft + lp.leftMargin;
                    break;
            }

            if (hasDividerBeforeChildAt(i)) {
                childTop += mDividerHeight;
            }

            childTop += lp.topMargin;
            // 核心方法,调用setChildFrame去设置子控件的位置
            setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                    childWidth, childHeight);
            childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

            i += getChildrenSkipCount(child, i);
        }
    }
}

/**
* LinearLayout的setChildFrame方法
* 最终调用的就是View的layout方法,上面已经介绍过了
*/
private void setChildFrame(View child, int left, int top, int width, int height) {
    child.layout(left, top, left + width, top + height);
}

六、Draw过程

draw绘制就比较简单了,直接看源码

public void draw(Canvas canvas) {
    final int privateFlags = mPrivateFlags;
    final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
            (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
    mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

    /*
     * Draw traversal performs several drawing steps which must be executed
     * in the appropriate order:
     *
     *      1. Draw the background
     *      2. If necessary, save the canvas' layers to prepare for fading
     *      3. Draw view's content
     *      4. Draw children
     *      5. If necessary, draw the fading edges and restore layers
     *      6. Draw decorations (scrollbars for instance)
     */

    // Step 1, draw the background, if needed
    int saveCount;

    if (!dirtyOpaque) {
        // 第一步,绘制背景
        drawBackground(canvas);
    }

    // skip step 2 & 5 if possible (common case)
    final int viewFlags = mViewFlags;
    boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
    boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
    if (!verticalEdges && !horizontalEdges) {
        // 第二步,绘制自己
        if (!dirtyOpaque) onDraw(canvas);

        // 第三步,绘制子控件
        dispatchDraw(canvas);

        drawAutofilledHighlight(canvas);

        // Overlay is part of the content and draws beneath Foreground
        if (mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }

        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);

        // Step 7, draw the default focus highlight
        drawDefaultFocusHighlight(canvas);

        if (debugDraw()) {
            debugDrawFocus(canvas);
        }

        // we're done...
        return;
    }

    // Step 4, draw the children
    dispatchDraw(canvas);
}


@Override
 protected void dispatchDraw(Canvas canvas) {
       ...
        if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) {
            for (int i = 0; i < count; i++) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                    more |= drawChild(canvas, child, drawingTime);
                }
            }
        } else {
            for (int i = 0; i < count; i++) {
                final View child = children[getChildDrawingOrder(count, i)];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                    more |= drawChild(canvas, child, drawingTime);
                }
            }
        }
      ......
}

View的测量、布局及绘制过程