View的工作原理

view经过measure、layout、draw三个过程才将一个view绘制出来,其中measure负责测量view,layout负责确定view在父容器中的位置,draw负责将view绘制在屏幕上。在measure、layout方法中又会调用onMeasure、onLayout方法,完成对子view的测量和定位,在draw方法中会调用dispatchDraw方法,对子view的绘制。

1、MeasureSpace

MeasureSpace是一个32位的int值,高两位代表SpecMode,低30位代表SpecSize。对于普通的view,其MeasureSpace由父容器的MeasureSpace和其自身的LayoutParams共同决定。
SpecMode有三类:

  • EXACITY:
    如果在当前view的LayoutParams中,指定了具体的宽高或者宽高是MATCH_PARENT(父容器是精确模式),那么当前view的SpecMode是精确模式,SpecSize是在LayoutParams中指定的具体值或者是父容器的剩余空间。
  • AT_MOST:
    如果在当前view的LayoutParams中,设置的宽高是WARAP_CONTENT,或者父容器的SpecMode是最大模式(但是当前view的LayoutParams不是具体值),那么当前view的SpecMode是最大模式,SpecSize不能超过父容器的剩余空间。
  • UNSPECIFIED:父容器不对view的大小有任何限制,当前view想要多大就给多大,listview,scrollview。

如果view采用固定宽高,其MeasureSpace是精确模式,大小就是在LayoutParams中指定的大小;当父容器的MeasureSpace是精确模式时,如果当前view的LayoutParams是MATCH_PARENT,该view的MeasureSpace是精确模式,大小是父容器的剩余空间;如果当前view的LayoutParams是WARAP_CONTENT,或者当父容器的MeasureSpace是最大模式时,该view的MeasureSpace是最大模式,大不能超过父容器的剩余空间。

2、measure过程

  • measure方法用来测量view的大小,但是实际的测量过程是在onMeasure方法中进行的。
  • measure方法是final,子view不能重写。
  • 主要是通过对View树中的每个View的mMeasuredWidth和mMeasuredHeight进行赋值来完成测量的。
view树的measure过程 来自http://blog.csdn.net/yanbober/
1、view的measure过程
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ......
        //回调onMeasure()方法
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        ......
    }

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

通过setMeasuredDimension方法对view的成员变量mMeasuredWidth、mMeasuredHeight进行赋值

    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        ...
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }

    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

在onMeasure方法中,可以根据父容器的MeasureSpace和view的LayoutParams,计算view的大小

    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的大小等于父容器的剩余空间,可以重写它

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthSpecMode=MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize=MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode=MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize=MeasureSpec.getSize(heightMeasureSpec);
        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);
        }else {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    }
2、viewgroup的measure过程

viewgroup没有实现onMeasure方法,不同的viewgroup子类具有不同的布局特征,这也导致他们的测量细节各不相同。可以在其子类中实现onMeasure方法。

例如, LinearLayout 的measure过程

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

LinearLayout会遍历每个子view,并最终调用每个子view的measure方法。当子元素测量完毕后,LinearLayout会测量自己的大小。

    void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
        mTotalLength = 0;
        ...
        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);
            ...
            final int usedWidth = totalWeight == 0 ? mTotalLength : 0;
            measureChildBeforeLayout(child, i, widthMeasureSpec, usedWidth,heightMeasureSpec, 0);
            ...
            ...
        }
        ...

        mTotalLength += mPaddingTop + mPaddingBottom;
        int heightSize = mTotalLength;
        heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
        int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
        ...
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                heightSizeAndState);

    }

    void measureChildBeforeLayout(View child, int childIndex,
            int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
            int totalHeight) {
        measureChildWithMargins(child, widthMeasureSpec, totalWidth,
                heightMeasureSpec, totalHeight);
    }
    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);
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

3、layout过程

layout方法确定当前view四个顶点相对与父容器的位置,即为mLeft、mTop、mBottom、mRight赋值;在onLayout方法中会循环调用子view的layout方法,确定子view的位置。

1、view的layout过程

view在layout方法中已经得到了四个顶点位置,所以onLayout()是一个空实现

public void layout(int l, int t, int r, int b) {  
    int oldL = mLeft;  
    int oldT = mTop;  
    int oldB = mBottom;  
    int oldR = mRight;  
    // 确定View的位置
    // 即初始化四个顶点的值,然后判断当前View大小和位置是否发生了变化并返回  
    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);  
  ...

}  

private boolean setOpticalFrame(int left, int top, int right, int bottom) {
    Insets parentInsets = mParent instanceof View ?((View) mParent).getOpticalInsets() : Insets.NONE;
    Insets childInsets = getOpticalInsets();

    return setFrame(
            left   + parentInsets.left - childInsets.left,
            top    + parentInsets.top  - childInsets.top,
            right  + parentInsets.left + childInsets.right,
            bottom + parentInsets.top  + childInsets.bottom);
}

protected boolean setFrame(int left, int top, int right, int bottom) {
    ...
    // 确定View的四个顶点
    mLeft = left;
    mTop = top;
    mRight = right;
    mBottom = bottom;

    mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
}
2、viewgroup的layout过程

在layout方法中,可以确定viewgroup自身的位置。但是如果要确定子view在viewgroup的位置,需要实现onLayout方法。由于确定位置与具体布局有关,所以onLayout方法在ViewGroup中是一个抽象的方法:需要具体的子类去实现。
例如,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);
        }
    }


在layoutVertical中遍历view,并根据layoutDirection和gravity计算每个子view的childTop、childLeft,调用setChildFrame,完成对子view的定位。

    void layoutVertical(int left, int top, int right, int bottom) {
        final int paddingLeft = mPaddingLeft;

        int childTop;
        int childLeft;

        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            final int childWidth = child.getMeasuredWidth();
            final int childHeight = child.getMeasuredHeight();
            final LinearLayout.LayoutParams lp =(LinearLayout.LayoutParams) child.getLayoutParams();
            int gravity = lp.gravity;

            ...
            //根据layoutDirection和gravity计算childTop、childLeft
            ...
            childTop += lp.topMargin;
            setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                    childWidth, childHeight);
            childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

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


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

3、draw过程

view的绘制过程如下:

  • 1、绘制背景
  • 2、绘制自己
  • 3、绘制children
  • 4、绘制装饰
    public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);

        // Step 1, draw the background, if needed    
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // Step 2, save the canvas' layers

        // Step 3, draw the content 通常在自定义view的时候,需要子类实现
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children  遍历子View并绘制包含的所有子View
        dispatchDraw(canvas);

        // Step 5, draw the fade effect and restore layers
    
        // Step 6, draw decorations (foreground, scrollbars) (滚动指示器、滚动条、和前景)
        onDrawForeground(canvas);
    }

注意:
viewgroup默认情况下是不需要绘制内容的,如果需要绘制内容,可以在构造函数中设置setWillNotDraw(false)

参考: Android开发艺术探讨Android应用层View绘制流程与源码分析自定义View Measure过程 - 最易懂的自定义View原理系列自定义View Layout过程 - 最易懂的自定义View原理系列自定义View Draw过程- 最易懂的自定义View原理系列

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

推荐阅读更多精彩内容