View的工作原理

作者: susion哒哒 | 来源:发表于2018-01-22 09:30 被阅读78次

ViewRoot和DecorView

ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成的。

在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DeocorView关联。

    root = new ViewRootImpl(view.getContext(), display)
    root.setView(view, wparams, panelParentView)

onMeasure、onLayout、onDraw

View的绘制流程是从ViewRoot的performTraversals方法开始的。并依次经历measure、layout、draw。

  • measure决定了View的宽高,Measure完成后,可以通过getMeasuredWidthgetMeasuredHeight方法来获取到View测量后的宽高,在几乎所有的情况下它都等同于View最终的宽高

  • layout决定了View的四个顶点的坐标和实际的View的宽高。完成后,可以通过getTop getBottom getLeft getRight来拿到View的四个顶点的位置,并可以通过getWidth getHeight来拿到View的最终宽和高

  • draw决定了View的显示,只有draw方法完成以后View的内容才能呈现在屏幕上。

View的测量

View的宽高由MeasureSpec决定。对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定。

特殊的对于DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutPrams来共同决定。

MeasureSpec

MeasureSpec代表一个32位的int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode指测量模式,而SpecSize是指在某种测量模式下的规格大小。

SpecMode有3类

  • UNSPECIFIED:父容器不对View有任何限制,要多大给多大。

  • EXACTLY:父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应LayoutParams中的math_parent和具体数值这两种模式。

  • AT_MOST:父容器指定了y一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么要看不同View的具体实现。它对应于LayoutParams中的wrap_content。

从 measureChildWithMargins()方法可用看出:
即根据父容器的MeasureSpec同时结合View本身的LayoutParams来确定子元素的MeasureSpec

    final int childWidthMeasureSpec = 
    getChildMeasureSpec(parentWidthSMeasureSpec, 
    parentPaddingLeft+parentPadingRight+childLp.leftMargin+childLp.rightMargin+widthUesed,
    childLp.width)   

简单说一下 getChildMeasureSpec() 这个方法的实现:
当View采用固定宽/高的时候,不管父容器的MeasureSpec是什么,View的MeasureSpec都是精确模式并且其大小遵循LayoutParams中的大小;
当View的宽/高是match_parent时,如果父容器是精确模式,那么View也是精确模式并其其大小是父容器的剩余空间,如果父容器是最大模式,那么View也是最大模式并且其大小不会超过父容器的剩余空间;
当View的宽/高是wrap_content时,不管父容器的模式是精准还是最大化,View的模式总是最大化并且大小不能超过父容器的剩余空间。

measure()

measure是一个final方法,最终会调用onMeasure(),看一下View的onMeasure方法的实现:

setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth()), widthMeasureSpec)
,getDefaultSize(getSuggestedMinimumHeight()), heightMeasureSpec)

对于getDefaultSize方法:
在AT_MOST和EXACTLY时,返回的大小就是 widthMeasureSpec中的size,即这个size就是View测量后的大小。
在UNSPECIFIED时,以宽为例,如果View没有设置背景,则View的宽度即为android:minWidth。如果没指定则为0。如果View设置了背景,则去android:minWidth和背景的最小宽度这两者中的最大值。

测量小结

结合measureChildWithMargins()和onMeasure()的实现可以得到如下结论:直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时自身大小。否则在布局中 使用wrap_content就相当于使用math_parent。对于这个问题,我们可以这样解决:在View使用wrap_content时,我们可以指定一个给它指定一个默认的宽高。

    setMeasuredDimension(mDefaultWidth, mDefaultHeight)

在onMeasure中拿到的测量宽高很可能是不准确的,一个比较好的习惯是在onLayout方法中去获取View的测量宽/高或者最终的宽高。

获取宽高

有时我们想再Activity的生命周期中获取View的宽高,但是Activity的生命周期和View的measure过程并不是同步的,这时候我们可以通过以下方法确保获得准确的View宽高:

1. View.post(runnable) 在Looper调用此runnable时,View已经初始化完毕。
2. ViewTreeObserver的onGlobalLayoutListener, 这个接口会在View树的状态发生改变或者View树内部的View的可见性发生改变时调用。但需要注意的是这个方法会被调用多次,如果你对View的宽高有实时性的要求,这个方法不错。
3. view.measure(widthMeasureSpec,heightMeasureSpec) 手动测量
    这里根据View的LayoutParams分情况看一下参数如何传递:
    
    math_parent:
    无法measure出具体的宽高,这是因为构造此种MeasureSpec需要知道parentSize,
    即父容器剩余空间大小,但这时是无法知道的。
    
    具体数值:
    widthSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY)
    view.measure(widthSpec, heightSpec)
    
    wrap_content:
    widthSpec = MeasureSpec.makeMeasureSpec((1<<30) -1, MeasureSpec.AT_MOST)
    view.measure(widthSpec, heightSpec)
    即假设剩余可使用的空间为最大值,在这个值的情况下去测量View的大小。

View的Layout

layout方法确定View本身的位置。onLayout方法则会确定所有子元素的位置。

Layout方法首先会通过setFrame方法来设定View的四个顶点的位置,即初始化mLeft,mRight,mTop和mBottom这四个值,View的四个顶点一旦确定,那么View在父容器中的位置也就确定了。

对于ViewGroup的onLayout方法,已LinearLayout为例,在他的layoutVertical中,此方法会遍历所有子元素并调用setChildFrame方法来为子元素指定对应的位置,其childTop会逐渐增大,即后面的子元素会被放置在靠下的位置。 setChildFrame的实现仅仅是调用子元素的layout方法而已。

View的getMeasureWidth和getWidth方法是没有区别的:

    childWidth = child.getMeasuredWidth()
    setChildFrame(child, childLeft,childTop+getLocationOffset(child), childWidth,
    childHeight)
    
    setChildFrame的具体实现:
        child.layout(left, top, left + width, top + height)
        
        mRight = left + width; mLeft = left 
        
    getWidth的实现:
        return mRight - mLeft

那么在那些特殊情况下会不同呢?

```
    如果重写layout方法:
    
    public void layout(int l, int t, int r, int b){
        super.layout(l, t, r+100, b+100)
    }
    
    即故意使View的宽、高各变大100。
    
    另外一种情况是:
    
    在View需要多次measure才能确定自己测量宽高,在前几次的测量过程中,
    其得出的测量宽高可能和最终宽高不一致,但最终来说,测量宽高还是和最终宽高相同。
```

View的draw过程

View的绘制过程分为如下几步:

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

在View的绘制过程中有一个标记为setWillNotDraw,如果设置了这个标记位,系统会认为这个View不会绘制任何内容,会进行相应的优化。View不会启用这个标记为,但是ViewGroup会默认启用这个标记位,因此,当我们自定义的ViewGroup需要通过onDraw来绘制内容时,我们需要显示地关闭这个标记位。

总结自《Android开发艺术探索》

相关文章

  • View 的测量

    接着上篇 View 基础 来讲 View 的工作原理,View 的工作原理中最重要的就是测量、布局、绘制三大过程,...

  • 【Android】自定义ViewGroup

    关于View的工作原理、绘制流程等,在第4章 View的工作原理[https://www.jianshu.com/...

  • View 工作原理

    1、 ViewRoot 和 DecorView 介绍 ViewRoot 对应于 ViewRootImpl 类,它...

  • View工作原理

    参考书籍:Android开发艺术探索注:京东链接https://item.jd.com/11760209.html...

  • View工作原理

    View工作原理 首先先来说明一下要掌握的知识 View绘制工作整体流程 Measure Layout Draw ...

  • View工作原理

    1、起步分析 在Activity启动分析中 知道,Activity的创建是在ActivityThread.perf...

  • View工作原理

  • View工作原理

    view有三大工作流程:测量、布局、绘制,分别对应着方法mesure、layout、draw ViewRoot和D...

  • View工作原理

    ViewRoot对应ViewRootImpl类,它是连接WindowManager和DecorView的纽带,Vi...

  • View的工作原理

    ViewRoot对应于ViewRootImpl类,是连接Windowmanager和DecorView的纽带,Vi...

网友评论

  • 许渺:我最近也在看这本书 进度和你差不多:innocent:

本文标题:View的工作原理

本文链接:https://www.haomeiwen.com/subject/nbakaxtx.html