Android开发艺术(4)——View的工作原理

初识ViewRoot和DecorView

低版本——2.3中是ViewRoot,高版本——4.0以上是ViewRootImpl,看名字感觉跟是View的root,实际跟View没有这种关系,View树的根是DecorView,DecorView又是在PhoneWindow中的。

WindowManagerImpl中的addView()中有一段这样的代码root.setView(view, wparams, panelParentView);,root就是ViewRootImpl,所以ViewRootImpl其实是view树的管理者,他把DecorView和WindowManager联系起来,它用来管理View树

ViewRootImpl中的performTraversals方法是View绘制的起始点(View的绘制流程是measure、layout、draw)

performTraversals中:

  • 先调用performMeasure然后内部调用, mView.measure,内部调用onMeasure(5.0、4.0版本中略有差异)
  • performLayout同上
  • performDraw同上,不过通过dispatchDraw分发draw而不是onDraw(onDraw是画自己,dispatchDraw是画孩子)

关于view的三个流程:

  • measure之后,就可以通过getMeasureWidth获取到测量的宽高,一般情况他是view的宽高。(之所以说是一般情况是因为看下面)
  • layout之后,view的左上右下就固定了,可以通过getLeft/getRight/getTop/getBottom获取到这些信息,getWidth就是根据这个计算的。
  • draw之后,view才会可见

DecorView是根View,是FrameLayout,内部通常是Linearlayout,然后里面是titlebar、… 、content,content就是我们setContentView所设置的View的容器。具体这个结构还和Activity主题有关

理解MeasureSpec

MeasureSpec是一个类,它可以描述成一个32位的二进制数,高二位表示MODE,是测量模式(未知、精确、取最大三种,对应的英文就不说了),低30位是SIZE(数字、包裹内容、匹配父容器,英文也不说了,就是xml中我们常用的三种)

通过各种逻辑与或非操作,可以把两个数字mode和size包装成一个MeasureSpec,还可以把MeasureSpec解包成mode和size,这些操作谷歌已经写好了,直接可以用

一个View,有MeasureSpec,通过MeasureSpec,调用measure(),然后调用onMeasure(),在onMeasure中,对于普通的view,直接就可以完成测量,对于ViewGroup,还要测量他的孩子(注意,是测量的宽高,上一小节说了,最终的宽高在layout之后才能确定,不过一般情况是一样的)

那么MeasureSpec是怎么来的呢?MeasureSpec是View自身的LayoutParams结合父亲的一些规则(其实就是父View的MeasureSpec,毕竟父View的MeasureSpec就可以决定父View的测量宽高了,就像这个View的MeasureSpec确定之后,测量宽高就确定了),计算来的,换句话说就是,给一个View设置Layoutparams,不能保证他在任何情况下都这么大,因为还和他的父View有关系。(注意,还有一种特殊情况,DecorView没有父View,所以他的MeasureSpec是自己的Layoutparams和窗口——Window的尺寸决定的,DecorView是在Window中的,之前说过)。具体的DecorView、普通View,他们的MeasureSpec是如何创建的呢,下面就来分析:

  • DecorView的MeasureSpec创建:

    DecorView的MeasureSpec是在ViewRootImpl中创建的,在measureHierarchy中调用getRootMeasureSpec创建,看代码可知,这个MeasureSpec是由DecorView的layoutparams和窗口大小决定的

//注:这两个参数分别是窗体宽高、DecorView的layoutparams的width
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;
    }

measureHierarchy中创建完成宽高的MeasureSpec之后,就开始执行performMeasure

private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
            final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {

      childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
      childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
      //内部调用decorView的measure方法
      performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

        return windowSizeMayChange;
}
  • 普通View的MeasureSpec的创建:

    DecorView的MeasureSpec是从ViewRootImpl中创建的,然后让DecorView调用measure方法

    普通的View是在他的父View中创建MeasureSpec,然后让子View(自己)调用measure方法

    和ViewRootImpl中的measureHierarchy类似,在ViewGroup中有个measureChildWithMargins方法,里面通过getChildMeasureSpec创建孩子的MeasureSpec,然后调用child.measure(),整个过程跟ViewRootImpl中的measureHierarchy很相似

protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        //这里可知,孩子的measureSpec的创建是由父View的measurespec、父view的padding,以及孩子自己的layoutparams(margin、width等)决定的
        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);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

重点是getChildMeasureSpec,下面这段代码需要十分熟悉:

//三个参数分别是
//1.父View的measurespec
//2.父View已经占用的尺寸,也就是孩子不能使用的(这个是父View的padding+孩子的margin,看上面那段代码可知)
//3.子view的width(MATCH_PARENT、WARP_CONTENT、具体数值)
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        //父容器的可用尺寸(去掉了padding),如果是负的,那就是0
        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:
            //孩子的尺寸是具体数值(大于等于0就是具体数值)
            if (childDimension >= 0) {
                //如下
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
              //孩子是MATCH_PARENT
            } 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.
              //孩子是包裹内容,那么孩子的测量模式就是AT_MOST,并且此时size的含义就是孩子最大可能的尺寸,而不是孩子的具体尺寸了
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
            //父亲是AT_MOST,说明父亲的尺寸不确定,但是父亲最大不能超过某个数值,这个数值是已知了
        case MeasureSpec.AT_MOST:
            //孩子是具体数值,那孩子就像下面那样,精确模式、具体尺寸
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
              //孩子是MATCH_PARENT,那么孩子不是精确的,但是孩子可以确定他最大尺寸,那就是父亲的最大尺寸,模式是AT_MOST
            } 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:
            //这种情况
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

网上各种表就不看了,上述代码理解了,其他表什么的都是浮云

综上:View的创建过程是在它的父View中的,DecorView是特殊的,没有父View,也不是在PhoneWindow中,而是在ViewRootImpl中,ViewRootImpl是DecorView和WindowManager的纽带,所以他引用了DecorView和WindowManager

View的工作流程

view的工作流程主要指的是:measure、layout、draw,其中measure最为复杂,一个一个来分析

Measure

View的测量分两种情况:View和ViewGroup,对于View测量完就好了。对于ViewGroup,还要测量孩子。

View的测量

由上一节可知,在View的父容器中会为View创建MeasureSpec,然后child.measure(),就进行测量了。

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
     //....  
     onMeasure(widthMeasureSpec, heightMeasureSpec);
}

measure是一个final,所以没法重写,内部调用了onMeasure()

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  //这个方法就是给View设置测量值,所以着重看参数是如何拿到的getDefaultSize()
    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) {
        //不考虑UNSPECIFIED
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
        //所以一般情况下,尺寸就是测量的值,由此可知,如果我们继承自View的一个自定义View,在测量的时候,我们需要重写onMeasure(如果是AT_MOST,那么我们就根据情况,提供一个数值,设置为宽高),因为如果是WARP_CONTENT,那么在获取尺寸的时候,就会拿到specSize,而由于MODE是AT_MOST,所以这个尺寸是父容器中可用的最大尺寸,所以效果跟使用MATCH_PARENT一样了。
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

ViewGroup的测量

ViewGroup本身就是一个View,所以他也一样,measure中调用onMeasure,具体的测量逻辑是在onMeasure中,但是ViewGroup是一个抽象类,他的不同的子类,测量规则都没法统一,所以他没有实现onMeasure,在线性布局、相对布局等中都有实现,稍后分析。

在ViewGroup中有个measureChildren方法,这可以说是一个默认的方法,在我们自己实现onMeasure时可以调用它,也可以不调用(一般测量孩子都调用他),相当于一个模板。看看代码。

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

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,内部再让孩子去measure,于是就到了View的测量

Layout

layout方法用来决定View自身的位置,在layout中调用了onLayout方法,这个方法没有具体的实现,需要子类自己实现,主要是为了决定子View的位置

public void layout(int l, int t, int r, int b) {
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;
  //设置自身的位置
    boolean changed = setFrame(l, t, r, b);
    if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
        if (ViewDebug.TRACE_HIERARCHY) {
            ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
        }
        //调用onLayout,具体的实现都不一样
        onLayout(changed, l, t, r, b);
        mPrivateFlags &= ~LAYOUT_REQUIRED;

        if (mOnLayoutChangeListeners != null) {
            ArrayList<OnLayoutChangeListener> listenersCopy =
                    (ArrayList<OnLayoutChangeListener>) 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 &= ~FORCE_LAYOUT;
}

在Linearlayout中,onLayout中主要就是遍历孩子,然后调用setChildFrame方法,这个方法内部就是调用child的layout方法,所以又回到了上面那一步。

在调用setChildFrame时,会传入宽高,最终到了child.layout,这个宽高就是通过getMeasureWidth获取的,也就是说,onLayout中的宽高就是测量宽高,所以说,一般情况下,测量宽高和最终的宽高一样,但是测量宽高先被赋值。而且测量宽高可能被多次赋值,所以有时候和最终宽高不一样。

另一种情况就是layout参数传进来的宽高没有直接使用,比如如下,也会导致测量和最终不同。

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

Draw

public void draw(Canvas canvas) {
  
    /*
     * 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
   
    // 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) {
        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);

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

        // Step 6, draw decorations (scrollbars)
        onDrawScrollBars(canvas);

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

   
    // Step 2, save the canvas' layers
   
    // Step 3, draw the content
    if (!dirtyOpaque) onDraw(canvas);

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

    // Step 5, draw the fade effect and restore layers
   

    // Step 6, draw decorations (scrollbars)
    onDrawScrollBars(canvas);
}

看注释可知,View的draw过程主要有以下几步:

  • 画背景
  • 画内容
  • 画孩子
  • 画装饰

draw是通过dispatchDraw将绘画分发给孩子的

有个方法是setWillNotDraw();可以设置当前view不绘制内容,一般继承自ViewGroup,并且确保自身不需要绘制,就设为true,可以优化。默认为false。

自定义View

注意事项

  • 直接继承自View或者ViewGroup需要处理padding和wrap_parent
  • 尽量不要使用handler,因为view自带post
  • 利用onDetachedFromWindow和onAttachedToWindow,维护线程、动画的起止

自定义View实例

继承自View的自定义View

  • 在onMeasure中处理wrap_parent
  • 在onDraw中处理padding
  • 自定义xml属性,文件名字不一定要交attrs。自定义属性获取完数据之后记得调用recycle。

继承自ViewGroup的自定义View

  • 在onMeasure中调用measureChildren测量孩子(也可以自己写逻辑),然后分析自己的measurespec,最后调用setMeasuredDimension
  • onLayout中根据测量宽高,遍历孩子,为其布局。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容