View工作流程

鸽了好久还是弄完了,相当于看书记的笔记了......

1、基础概念

  • ViewRoot: ViewRootImpl 类,连接 WindowManager 和 DecorView,三大流程均通过 ViewRoot 完成。
  • decorView:本质上是一个FrameLayout,是Activity中所有View祖先。

View绘制流程是从ViewRoot的performTraversals方法开始,经过measure,layout,draw三步完成。

  • measure 用于测量 View 的宽高
  • layout 用于确定 View 在父容器中的位置‘
  • draw 用于绘制

performTraverslas会依次调用performMeasure,performLayout,performDraw三个方法,分别完成顶级View的三大流程。在performMeasure中调用measure方法,在measure中调用onMeasure对所有子元素测量,完成对View所有子元素的测量。performLayout和performDraw与performMeasure类似。

2、MeasureSpec

MeasureSpec决定了一个View的尺寸规格,但还受父容器影响。在测量过程中,系统会将View的LayoutParams根据父容器施加的规则转化成对应的MeasureSpec,然后再测量View的宽高。
MeasureSpec代表了一个32位int值,高2位代表SpecMode,即测量模式,低30位代表SpecSize,规格大小。
SpecMode有三类,如下:

  • UNSPECIFIED:父容器不对View有任何限制,一般用于系统内部
  • EXACTLY:父容器已经检测出View的精确大小,此时最终大小就是SpecSize的值,对应LayoutParams中的match_parent和具体数值两种情况。
  • AT_MOST:父容器指定SpecSize,View的大小不能大于这个值,对应于LayoutParams中的wrap_content。

3、View的工作流程

View的工作流程主要指measure,layout,draw这三大流程。

3.1、measure

measure的测量分为对ViewGroup的测量和对View的测量,对View测量时,通过measure即可完成,如果是一个ViewGroup时,除了完成自己的测量,还会遍历所有子元素发的measure方法。

3.1.1、View

view的measure由其measure方法完成,在View的measure方法中会去调用onMeasure方法。

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

setMeasureDimension会设置View的宽高测量值,需要调用getDefaultSize方法

  • getgetDefaultSize
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方法中,在AT_MOST和EXACTLY状态,getDefaultSize返回MeasureSpec中的SpecSize,即View测量后的大小。在UNSPECIFIED状态下,返回值为size的值,即宽高分别为getSuggestedMinimumWidth和getSuggestedMinimumHeight的返回值。

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

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

在上述代码中,如果mBackground是null,即View没有设置背景,那么宽度为mMinWidth,高度为mMinHeight。而mminWidth对应android:minWidth属性对应的值,默认为0,如果View指定了背景,则View的宽度为max(mMinWidth,mBackground.getMinimumWidth()),即mMinWidth和mBackground.getMinimumWidth()中较大的值。

  • mBackground.getMinimumWidth
public int getMinimumWidth(){
    final int intinsicWidth=getIntrinsicWidth();
    return intrinsicWidth > 0 ? intrinsicWidth :0 ; 
}

则getMinimumWidth返回的是Drawable的原始宽度。

由此可见,View的宽高有SpecSize决定。直接继承View的空间需要重写onMeasure方法并设置wrap_content时的自身大小,否则View的SpecSize是parentSize。

3.1.2、ViewGroup

对于ViewGroup来说,除了完成自己的measure过程以外,还需要遍历所有子元素的measure,子元素递归执行。ViewGroup没有重写inMeasure方法,同时它提供了一个measureChildren的方法。

  • measureChildren
protected void measureChildren(int widthMeasureSprc,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);
        }
    }   
}

在measureChildren方法中,会遍历所有子元素,并测量所有子元素的大小。

  • measureChild
protect 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中取出子元素的LayoutParams,之后通过getChildMeasureSpec创建子元素的MeasureSpec,之后将MeasureSpec传递给View的measure方法测量。

ViewGroup是一个抽象类,不同的布局有不同的测量方法,所以其测量过程onMeasure需要有各个子类的实现。

分析LinearLayout的onmeasure方法

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

在LinearLayout的measure方法中会根据orientation选择是measureVertical还是measureHerizontal方法,在相应方法中会遍历子元素执行measureChildBeforeLayout方法,该方法内部会调用子元素的measure方法,并且通过mTotalLength来储存LinearLayout在数值方向的初步高度,在MTotalLength中包含了子元素的高度和在竖直方向上的margin,子元素测量完毕后,LinearLayout会测量自己的大小。

3.2、Layout

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

  • layout
public void layout(int l,int t,int r.int b){
    if((mPrivateFlags3 & PFLAGS3_MEASURE_NEEDED_BEFORE_LAYOUT)!+0){
        onMeasure(mOldWidthMeasureSpec,mOldHeightMeasureSpec);
        mPrivateFlags3 &= ~PFLAGS3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }
    int oldL=mLeft;
    int oldT=mTop;
    int oldB=mButtom;
    int oldR=mRight;

    boolean changed=isLayoutModeOptical(mParent)? setOpticalFrame(l,t,r,b): setFrame(l,t,r,b);

    if(changed || (mPrivateFlags & PLAG_LAYOUT_REQUIRED)==PFLAG_LAYOUT_REQUIRED){
        onLayout(changed,l,t,r,b);
        mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
        ListenerInfo li=mListenerInfo;
        if(li != null && li.mOnLayoutChangedListeners != 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;
}

layout方法的流程如下,首先通过setFrame方法设定四个顶点的位置,初始化mLeft,MRight,mTop,mBottom四个值,之后调用onLayout方法,父容器确定子元素的位置,和onMeasure方法类似,onLayout具体的实现取决于布局。
示例LinearLayout的onLayout方法。

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

在onLayout中,依旧会根据不同的布局(vertical和horizontal)选择不同的方法。

  • layoutVertical
void layoutVretical(int left,int top,int right,int bottom){
    ...
    final int count=getVerticalChildCount();
    for(int i=0;i<count;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.cjildTop+getLocaltionOffset(child),childWidth,childHeight);
            childTop+=childHeight + lp.bottomMargin+getNextLocationOffset(child);
            i+=getChildrenSkipCount(child,i);            
        }
    
    }

}

layoutVertical会遍历所有子元素,并且会调用getChildFrame方法位子元素指定相应的位置,其中childTop会逐渐增大,即后面的子元素会放在靠下的位置。setChildFrame调用了元素的layout方法,这样父元素在layout方法中完成自己的定位以后,通过onLayout方法调用子元素的layout方法,子元素又通过自己的layout方法来确定自己的位置。

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

在setChildFrame中的width和height实际就是子元素的测量的宽高。
而在layout方法中会通过setFrame去设置子元素四个顶点的位置。

3.3、Darw

Draw的绘制步骤如下:

  • 绘制背景 background.draw(canvas)
  • 绘制自己 onDraw
  • 绘制children dispatchDraw
  • 绘制装饰 onDrawScrollBars
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;

    int saveCount;

    //绘制背景
    if(!dirtyOpaque){
        drawBackground(canvas);
    }

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

        onDrawScrollBars(canvas);

        if(mOverlay != null && ! mOverlayl.isEmpty()){
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }
        return;
    }

    ...
}

View的绘制是通过dispatchDraw实现的,dispatchDraw会遍历所有子元素的draw方法。

参考来源:

《Android开发艺术探索》

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