【Android】View学习之View的工作原理

【Android】View学习之View的工作原理

下图是Android的UI管理系统的层级关系。

img

  1. PhoneWindow是Android系统中最基本的窗口系统,继承自Windows类,负责管理界面显示以及事件响应。它是Activity与View系统交互的接口
  2. DecorView是PhoneWindow中的起始节点View,继承于View类,作为整个视图容器来使用。用于设置窗口属性。它本质上是一个FrameLayout
  3. ViewRoot在Activtiy启动时创建,负责管理、布局、渲染窗口UI等等

ViewRoot与DecorView

ViewRoot是连接WIndowManager和DecorVIew的纽带。View的三大流程(measure layout draw)都是通过ViewRoot完成的。在ActivityThread中,Activity创建后,DecorView会被添加到Window中,同时创建ViewRootImpl,然后将DecorView与ViewRootImpl建立关联(通过ViewRoot的setView方法)。

View绘制从ViewRoot的performTraversals方法开始,经过measure、layout、draw三个过程,然后将View绘制出来

  • measure方法用于测量View的宽高
  • layout方法用于确定View在父容器中的位置
  • draw方法负责将View绘制在屏幕上

如下是performTraversals的流程图:

img

它会依次调用performMeasure、performLayout、performDraw方法,分别完成顶级View的measure、layout、draw流程。performMeasure会调用measure方法,而measure又会调用onMeasure方法,在onMeasure方法中又会对子元素进行measure,这样重复下去就完成了整个View树的遍历。

performLayout、performDraw传递过程也非常类似,不过performDraw是在draw方法中通过dispatchDraw方法实现的。

measure过程决定了View的宽高,而Layout方法则确定了四个顶点的坐标和实际的宽高(往往等于measure中计算的宽高),draw方法则决定了View的显示。只有完成了draw方法才能正确显示在屏幕上。

MeasureSpec

MeasureSpec很大程度上决定了View的尺寸规格(父容器也会造成影响)。测量过程中,系统会将View的LayoutParams转换为MeasureSpec,然后通过MeasureSpec来测量View的宽高。

MeasureSpec是一个32位的int值,它的最高两位用来存放测量模式SpecMode,而后面的30位则用来存放规格大小SpecSize。

PS:这里用到了位运算进行状态压缩来节省内存。

SpecMode

SpecMode有三类,每一类都有特殊含义

  • UNSPECIFIED:父容器不做任何限制,要多大给多大。通常应用于系统内部,表示一种测量状态
  • EXACTLY:父容器已经检测出View需要的大小,此时View的大小就是SpecSize指定的值,对应了LayoutParams中的match_parent和指定具体数值两种情况
  • AT_MOST:父容器指定了可用大小SpecSize,View的大小不能大于这个值,具体值取决于不同View的具体实现。对应了LayoutParams的wrap_content

与LayoutParams的对应关系

下表是普通View的MeasureSpec的创建规则对应表

childLayoutParams / parentSpecParams EXACTLY AT_MOST UNSPECIFIED
dp/px EXACTLY childSIze EXACTLY childSIze EXACTLY childSIze
match_parent EXACTLY parentSize AT_MOST parentSize UNSPECIFIED 0
wrap_content AT_MOST parentSize AT_MOST parentSize UNSPECIFIED 0

View的工作流程

View的三大流程如下

  • measure:测量,决定了View的宽度和高度
  • layout:布局,决定了View最终的宽高及四个顶点的位置
  • draw:绘制,将View绘制到屏幕上

measure过程

measure过程需要分为两类,普通的View通过measure方法后就完成了它的测量过程,而ViewGroup除了自己的测量过程外,还会遍历所有子元素的measure方法,子元素再递归执行,才能完成

View的measure过程

View的measure过程由measure方法来完成,measure方法是一个final方法,不能重写,它会调用VIew的onMeasure方法。onMeasure方法中会调用getDefaultSize方法,而getDefault方法中又会调用getSuggestedWidth和getSuggestedHeight方法。

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

getDefaultSize方法所返回的就是测量后的View的大小。

我们接着看到getSuggestedWidth和getSuggestedHeight方法

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

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

它在没有指定background的情况下,返回的是minSize这一属性对应的值,而在指定了背景的情况下,返回的是背景drawable的getMinimumWidth / getMinimumHeight方法对应的值

这两个方法在Drawable有原始宽度的情况下返回原始宽度,否则返回0

从getDefaultSize方法可以看出,View的宽高由specSize决定。

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

想要解决这个问题,我们可以参考下面的代码。我们只需要给View指定一个默认宽高。在wrap_content时设置此宽高即可。而非wrap_content我们只需要沿用系统宽高即可。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int widhtSpecSize = MEasureSpec.getSize(widthMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightSpecSize = MEasureSpec.getSize(widthMeasureSpec);
    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);
    }
}

ViewGroup的measure过程

ViewGroup除了完成自己的measure过程,还会遍历调用子元素的measure方法,然后子元素再次递归执行,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);
        }
    }
}

可以看到,ViewGroup执行measure时,会遍历子元素,调用measureChild方法对子元素进行measure。

下面是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);
}

它会取出子元素的LayoutParams,通过getChildMeasureSpec方法创建子元素MeasureSpec,然后传递给View的measure方法进行测量。

ViewGroup没有定义测量具体过程,因为它是个抽象类。具体的测量过程的onMeasure方法需要子类来实现,由于它的子类的特性可能会很大不同,所以没法做统一处理(如LinearLayout和RelativeLayout)。

下面我们研究一下LinearLayout的onMeasure实现:

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

它根据横向和纵向分别调用measureVertical和measureHorizontal方法。我们可以查看measureVertical的一部分代码:

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    ...
    for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);
            ...
            // 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).
            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;
            }
            ...
    }
    ...
}

会发现系统会遍历子元素并对每个子元素调用measureChildBeforeLayout方法。这个方法会调用子元素的measure方法。这样子元素就开始进入measure过程。并且系统会计算子元素的高度并存放在mTotalHeght中

子元素测量完毕后,LinearLayout会测量自己的大小:

// 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来说,它在水平方向测量过程遵循View的测量过程,而竖直方向,如果在布局中采用match_parent或具体值,则和View的测量过程一致,如果采用的是wrap_content,则它的高度是所以子元素占用的高度的和。但仍然不会超过它的父容器的剩余空间。它的最终高度还需要考虑它在竖直方向的padding。

View的measure过程是十分复杂的,在measure完成后,通过getMeasuredWidth或getMeasuredHeight方法即可正确获得View的宽和高。

onCreate等方法中无法获取正确宽高的解决方法

注意,在某些极端情况下,系统可能需要多次measure才能拿到最终的测量宽高。此时的测量结果是不准确的。因此最好在onLayout中去获取View的测量宽高/最终宽高

我们有时候会想要在onCreate、onStart、onResume中获取View的宽高,但是其实是获取不到正确的宽高信息的。因为他们的生命周期与View的measure过程不是同步的,无法保证执行它们时View已经测量完毕。为了获取正确的宽高,可以用下面的方法来获取:

1. Activity/View 的 onWindowFocusChanged

onWindowFocusChanged中,VIew已经初始化完毕了,此时获取宽高是没问题的。

但需要注意的是,onWindowFocusChanged会被调用多次。当Activity窗口得到焦点和失去焦点时都会被调用。如果频繁进行onResume和onPause,则它会被频繁调用

2. View.post(Runnable)

通过post方法可以将Runnable投递到消息队列尾部,等待Looper调用后此Runnable时,View已经初始化完毕了

3. ViewTreeObeserver

用ViewTreeObserver的回调可以完成这个功能,如OnGlobalLayoutListener接口。当View树的状态改变或者内部的View可见性改变时,它都会被回调。这是获取View的宽高的很好的时机。

需要注意的是,随着View树的状态改变等,它会被调用多次

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

通过手动对View进行measure来得到宽高,比较复杂,具体可参考《Android开发艺术探索》P192

layout过程

Layout的作用是ViewGroup确定子元素的位置。当ViewGroup被确定后,在onLayout中会遍历所有子元素并调用layout方法,在layout方法中onLayout方法又会调用。layout方法确定View的位置,而onLayout方法则确定所有子元素的位置。

我们首先看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(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);
            }
        }
    }
    mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
    mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
        mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
        notifyEnterOrExitForAutoFillIfNeeded(true);
    }
}

首先,通过setFrame方法设定View四个顶点的位置(初始化mLeft,mRight,mTop,mBottom)。四个顶点一旦确定,则在父容器中的位置也确定了,接着便会调用onLayout方法,来让父容器确定子容器的位置。onLayout同样和具体布局有关,因此View和ViewGroup均没有实现onLayout方法。

我们查看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);
    }
}

可以看到,和onMeasure的实现逻辑类似,我们进入layoutVertical

void layoutVertical(int left, int top, int right, int bottom) {
    ...
    final int count = getVirtualChildCount();
    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();
        if (hasDividerBeforeChildAt(i)) {
            childTop += mDividerHeight;
        }
        childTop += lp.topMargin;
        setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                childWidth, childHeight);
        childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
        i += getChildrenSkipCount(child, i);
    }
}

它会遍历所有子元素并调用setChildFrame来为子元素指定对应位置。其中childTop会逐渐增大,也就是后面的子元素会放置在靠下的位置。父元素在layout方法中完成自己的定位后,通过onLayout方法调用子元素的layout方法,子元素又通过自己的layout方法确定自己的位置。这样就完成了View树的layout过程。

我们查看setChildFrame的源码,看看它是怎么为子元素指定对应位置的。

private void setChildFrame(View child, int left, int top, int width, int height) {
    child.layout(left, top, left + width, top + height);
}

根据layoutVertical的代码可以看出,这里的width和height就是子元素的测量宽高。而在layout方法中会通过setFrame来设置子元素四个顶点位置。在setFrame方法中有如下几句赋值语句,就确定了子元素的位置

mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;

getWidth和getMeasuredWidth的区别

我们看到getWidth的源码

public final int getWidth() {
    return mRight - mLeft;
}

由于之前的mLeft、mRight都是由Measure过程赋值过来来看,它的返回值其实就是测量宽度。不过测量宽高形成在Measure过程,实际宽高形成于Layout过程。

在日常开发中,我们可以认为View的测量宽高与实际宽高是相等的。

但是如果我们故意重写了onLayout方法,可能就会导致两个宽高不等

public void layout(int l, iny t, int r, int b){
    super.layout(l, t, r + 100,b + 100);
}

draw过程

draw过程是将View绘制到屏幕上,它遵循下面几步:

  1. 绘制背景 background.draw(canvas);
  2. 绘制自己 onDraw
  3. 绘制children(dispathDraw)
  4. 绘制装饰(onDrawScrollBars)

View绘制过程的传递是通过dispatchDraw实现的。dispatchDraw会遍历调用所有子元素的draw方法。这样draw事件就一层层传递了下来。它有个比较特殊的setWillNotDraw方法。

如果一个View不需要绘制任何内容,在我们设定这个标记为true后,系统就会对其进行相应优化。一般View没有启用这个标记位。但ViewGroup是默认启用的。

它对实际开发的意义在于:我们的自定义控件继承与ViewGroup并且不具备绘制功能时,可以开启这个标记位方便系统进行后续优化。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

%d 博主赞过: