Android开发艺术探索读书笔记: View的工作原理

ViewRoot和DectorView

ViewRoot与ViewRootImpl类实现的功能是一样的,DectorView和Windowmanger的桥梁,只是Android4.0以后改了名而已,可以看下图:

ViewRoot和ViewRootImpl关系

The top of a view hierarchy, implementing the needed protocol between View and the WindowManager.

当Activity创建完毕,DectorView会被添加到Window中并创建ViewRoot对象与DectorView建立关联。
View的三大流程都是通过ViewRoot来完成的:

st=>start: Start e=>end: End op1=>operation: performTraversals op2=>operation: performMeasure sub21=>subroutine: measure sub22=>subroutine: onMeasure(遍历所有子View测量自己) op3=>operation: performLayout sub31=>subroutine: layout sub32=>subroutine: onLayout(遍历所有ViewGroup摆放自己子View) op4=>operation: performDraw sub41=>subroutine: draw sub42=>subroutine: onDraw(遍历所有子View绘制自己) st->op1->op2->sub21->sub22->op3->sub31->sub32->op4->sub41->sub42

注:onLayout可以获取onMeasure测量好的值了 getMeasuredHeight/Width


DectorView做为顶级View被添加到Window(PhoneWindow)中,measure,layout,draw都是从这里开始,继承于FrameLayout,会根据主题加载不同的layout(例如R.layout.screen_title),内容id为 android.R.id.content,我们通过Activity#setContentView就是把我们的layout填充到里面。可以通过findViewById(android.R.id.content)获取这个ViewGroup,再通过getChildAt(0),就可以获得我们设置的内容了。

MeasureSpec 测量规格

系统会将我们设置的View的LayoutParams和父容器施加的规则转换为对应的MesureSpec,根据这个来测量那个View的宽和高。
MeasureSpec 表示为一个int,高2位表示测量模式SpecMode,低30位表示某种测量模式的大小,节省内存开销,因为很多地方都要使用。通过代码里面的常量可以看得出来有三种测量模式,并且提供了 打包 和 解包 这两样数据的方法。

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;

    //父容器不对View有任何限制,要多大,有多大,系统内部使用,基本不用
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;

    //父容器已经测量出了View所需要的精确大小,View的大小就是Spec
    //对应LayoutParams的具体数值和,match_parent
    public static final int EXACTLY     = 1 << MODE_SHIFT;

    //父容器指定一个可用大小的SpecSize,View的大小不能超过
    //对应LayoutParams的wrap_parent
    public static final int AT_MOST     = 2 << MODE_SHIFT;
}

MeasureSpec与LayoutParams对应关系

对于DoctorView其MeasureSpec是由屏幕尺寸和自身的LayoutParams决定的,得到的MeasureSpec当然是传递给子View使用,然后子View再根据此参数和自身的LayoutParams来确定自己的MeasureSpec。

DoctorView MeasureSpec的获取过程:

//class ViewRootImpl
//当然是在performTraversal里面调用的:P
//lp View本身的LayoutParams参数
//desiredWindowWidth/desiredWindowHeight 屏幕的宽高
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp, final Resources res, 
                                  final int desiredWindowWidth, final int desiredWindowHeight) {
    //...
    childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
    childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    //...
}

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    //...
    //mView就是与ViewRootImpl关联的DectorView!
    //通过public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView)设置的
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    //...
}

//根据屏幕宽高,DectorView的LayoutParams计算Dector的MeasureSpec
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;
    case ViewGroup.LayoutParams.WRAP_CONTENT:
        // Window can resize. Set max size for root view.
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
        break;
    default:
        // Window wants to be an exact size. Force root view to be that size.
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
        break;
    }
    return measureSpec;
}

对于View来说,MeasureSpec是由自身的LayoutParams和父View传递过来的MeasureSpec决定的。

//ViewGroup里面遍历测量所有的子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);
        }
    }
}

//不包含Margin,对应的有measureChildWithMargins
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);
}

//根据子View的LayoutParams和父View的MeasureSpec来确定子View的MeasureSpec
//spec:父View的MeasureSpec
//childDimension:可能值有match_parent,wrap_parent,精确值
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);
    int size = Math.max(0, specSize - padding);
    int resultSize = 0;
    int resultMode = 0;
    switch (specMode) {
    // Parent has imposed an exact size on us
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size. So be it.
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent has imposed a maximum size on us
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            // Child wants a specific size... so be it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size, but our size is not fixed.
            // Constrain child to not be bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent asked to see how big we want to be
    case MeasureSpec.UNSPECIFIED:
        if (childDimension >= 0) {
            // Child wants a specific size... let him have it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size... find out how big it should
            // be
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size.... find out how
            // big it should be
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

总结(记住这是View#onMeasure里面得到的参数):

View的工作过程

1.measure过程

上面提到的 measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) 函数,里面会调用 child.measure(parentWidthMeasureSpec,parentHeightMeasureSpec) 传递进去就是根据子View和父View的MeasureSpec计算出来的MeasureSpec。

public final void measure(int widthMeasureSpec, int heightMeasureSpec)

这是一个final方法,实际的测量是在onMeasue里面,我要实现的是这个方法,View里面有默认的实现:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
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;
    //wrap_content和match_content都是父View传递过来的建议大小
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

当我们给View设置wrap_content的时候,默认还是父View传递过来的建议大小,所以自定义控件的时候,需要实现这种情况,对于非wrap_cotent,沿用系统默认的即可。

计算大小的时候,可以调用View#resolveSizeAndState(int size, int measureSpec, int childMeasuredState),可以加上测量结果的状态,目前有两个一个正常和MEASURED_STATE_TOO_SMALL,View#getMeasuredWidthAndState()获取子View测量的结果,包括测量状态(高8位表示)。

+-------------------+
+ 0x00 ffffffff     +
+-------------------+
前面两位为01表示子View的测量结果大于父View所给的空间

对于 MeasureSpec.UNSPECIFIED 的情况,传递过来的是 getSuggestedMinimumWidth/Height:

  protected int getSuggestedMinimumWidth() {
        //没有设置View的背景mMinWidth对应android:minWidth这个属性所指定的值
        //如果设置的View的背景就是背景Drawable的最小宽度
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }


  public int getMinimumWidth() {
        //就是drawable原始宽度
        final int intrinsicWidth = getIntrinsicWidth();
        return intrinsicWidth > 0 ? intrinsicWidth : 0;
  }

此种模式系统内部使用,基本不用。
ViewGroup是一个抽象类,里面没有默认实现 onMeasure,我们需要根据布局的特性来测量其宽高。具体的测量可以参考前面DectorView测量子View的过程。

另外在 onMeasure 里面测量完毕之后一定要调用 setMeasuredDimension(int measuredWidth, int measuredHeight) 把测量结果存储,否则将会在执行measure的时候抛 IllegalStateException。

Activity的生命周期和View的measure测量过程是不同步的,所以在onCreate,onStart,onResume时刻是获取不到View的宽高的,有四种方法获取:

protected void onStart() {
    super.onStart();
    ViewTreeObserver observer = view.getViewTreeObserver();
    observer.addOnGlobalLayoutListener(new ObGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                int width = view.getMeasureWidth();
                int height = view.getMeasureHeight();
            }
        });
}
2.Layout过程

layout方法确定View本身的位置,里面调用setFrame来确定View四个顶点的位置,即mLeft,mTop,mRight,mBottom;而onLayout方法是确定子元素的位置,内部又会调用layout来确定View的位置。

getMeasuredWidth/getMeasuredHeight测量宽度/高度是在View测量完成后产生的,而最终的宽高getWidth/getHeight是在layout后产生的。

public final int getMeasuredWidth() {
    return mMeasuredWidth & MEASURED_SIZE_MASK;
}

public final int getMeasuredHeight() {
    return mMeasuredHeight & MEASURED_SIZE_MASK;
}

public final int getHeight() {
    return mBottom - mTop;
}

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

View四个定点的值会在layout->setFrame里面赋值,如果我们重写了layout方法,就可以使最终宽高和测量宽高不一致。一般情况中两者的值都是相等的。


在定义ViewGroup的时候可以根据需要实现这个方法:

protected LayoutParams generateDefaultLayoutParams(){
    //...
}

子View就可以通过LayoutParams类告诉其父视图它想要地大小,位置,方向等,就像 RelativeLayout 里面的各种位置属性(toLeftOf,toRight…),ViewGroup默认返回的是支持layout_width和layout_height属性的LayoutParams。这个书中没有说明,更详细的可以参考这个

3.draw过程
  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)

主要是3和4,调用 onDraw 方法来绘制自己,dispatchDraw 里遍历调用子View的draw一层一层地传递。

View#setWillNotDraw,用于设置是否绘制以便系统做优化,View默认是没有启用这个标志的,而ViewGroup默认启用。

自定义View须知

1.让View支持wrap_content

2.支持padding。自定义View(不是ViewGroup)margin是由父容器控制的,不需要做特殊处理,但是ViewGroup就需要写onMeasure和onLayout里面处理了

3.不要在View里面使用Handler,处理不当会内存泄漏,里面有post已经能完成相关功能。

4.如果有线程或者动画,考虑在 onDetachedFromWindow 里面停止

5.处理滑动冲突

更标准的自定义View的流程可以参考官方文档ViewGroup,还有Github上那些优秀的控件。