structure

image.png

  • DecorView是一个应用窗口的根容器,它本质上是一个FrameLayout
  • DecorView有唯一一个子View,它是一个垂直LinearLayout,包含两个子元素,
    • 一个是TitleView(ActionBar的容器),
    • 另一个是ContentView(窗口内容的容器)


setContentView

这个方法只是完成了Activity的ContentView的创建,而并没有执行View的绘制流程。

  • 实际上调用到了PhoneWindow的setContentView()方法。
  • PhoneWindow的setContentView()方法中
    • 调用了LayoutInflater的inflate()方法来填充布局
    • 传入了decorView作为LayoutInflater.inflate()的root参数
    • 最终调用的是inflate(XmlPullParser, ViewGroup, boolean)方法来填充布局
      • 单独处理merge标签
      • 调用rInflate()方法来递归填充布局
        • inflate()和rInflate()方法中都调用了rInflateChildren()方法
        • rInflateChildren()方法实际上调用了rInflate()方法


inflate方法总结

  • XML中保存了ViewTree的结构和View的相关标签信息(包括View的类型和一些属性值)
    • 这些信息会在后面通过反射的方式(如果没有Factory2和Factory的话)创建实例对象
      • 如果创建的是ViewGroup,则会对它的子View遍历重复创建步骤
      • 创建完View对象后,会add到对应的ViewGroup中
  • inflate->rInflate->createViewFromTag->createView

view绘制的起点

  • View的绘制是由ViewRoot来负责的。每个应用程序窗口的decorView都有一个与之关联的ViewRoot对象,这种关联关系是由WindowManager来维护的。
  • 当建立好了decorView与ViewRoot的关联后,ViewRoot类的requestLayout()方法会被调用,以完成应用程序用户界面的初次布局。实际被调用的是ViewRootImpl类的requestLayout()方法

    • setContentView() 只是把 View 添加到 DecorView 上
    • onResume() 中 ViewRootImpl 和 DecorView 做了关联
    • requestLayout() 和 invalidate() 会触发 ViewRootImpl 绘制 View
    • 在 Activity 的 onResume() 方法执行后,DecorView 会被添加带 ViewRootImpl 中。然后执行 requestlayout()

      1. //H:Handler->handleMessage(RESUME_ACTIVITY)->handleResumeActivity->windowmanager.addView
      2. public void addView(View view, ViewGroup.LayoutParams params,
      3. Display display, Window parentWindow) {
      4. ……
      5. ViewRootImpl root;
      6. View panelParentView = null;
      7. synchronized (mLock) {
      8. ……
      9. root = new ViewRootImpl(view.getContext(), display);
      10. view.setLayoutParams(wparams);
      11. mViews.add(view);
      12. mRoots.add(root);
      13. mParams.add(wparams);
      14. }
      15. // do this last because it fires off messages to start doing things
      16. try {
      17. root.setView(view, wparams, panelParentView); //invoke requestLayout
      18. } ……
      19. }

MeasureSpec

exactly优先,at_most次之

MeasureSpec代表一个32位int值,

  • 高2位代表SpecMode,SpecMode是指测量模式,
    • UNSPECIFIED, 指父容器不对view有任何限制,要多大给多大,一般用于系统内部,表示测量的状态
    • EXACTLY, 父容器检测出view需要的精确大小,view的最终大小就是SpecSize的大小。对应LayoutParams中的match_parent模式
    • AT_MOST,父容器指定了一个SpecSize, view的大小不能大于这个值, 对应LayoutParams.wrap_content模式
  • 低30位代表SpecSize,而SpecSize是指在某种测量模式下的规格大小
  • MeasureSpec 不是 唯一 由 LayoutParams 决定 的, LayoutParams 需 要和 父 容器 一起 才能 决定 View 的 MeasureSpec, 从而 进一步 决定 View 的 宽/ 高
    • if(view固定宽/高)— 不受父容器,View的MeasureSpec==EXACTLY,并且大小遵循LayoutParams中的大小
    • if(view宽/高是match_parent)
      • if 父容器是EXACTLY, view=EXACTLY,大小是父类的剩余空间
      • if 父容器是AT_MOST,view=AT_MOST,不能超过父容器的剩余空间
    • if(view宽/高是wrap_content),不受父容器影响,view=AT_MOST,不能超过父容器的剩余空间

      UNSPECIFIED-主要 用于 系统 内部 多次 Measure 的 情形, 一般来说, 我们 不需要 关注 此 模式。

  • 对于DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams来共同确定
  • 对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定,MeasureSpec一旦确定后,onMeasure中就可以确定View的测量宽/高

三阶段

image.png
View绘制流程 - 图3

获取宽高的时机

在onCreate、onStart、onResume中均无法正确得到某个View的宽/高信息,这是因为View的measure过程和Activity的生命周期方法不是同步执行的,因此无法保证Activity执行了onCreate、onStart、onResume时某个View已经测量完毕了,如果View还没有测量完毕,那么获得的宽/高就是0

  • Activity/ View# onWindowFocusChanged,
  • view. post( runnable),
  • ViewTreeObserver,
  • view. measure( int widthMeasureSpec, int heightMeasureSpec), 根据 View 的 LayoutParams 来 分情况,是否有效
    • match_parent, 直接放弃,无效
    • 具体数值(dp/px)—ok
    • wrap_content—ok
  1. object ViewUtil {
  2. /**
  3. * view的layoutparams指定了宽高,可以通过这个方法来获取真实宽高, 譬如100px
  4. */
  5. fun measureViewOfExactly(view: View) {
  6. val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, EXACTLY)
  7. val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
  8. view.measure(widthMeasureSpec, heightMeasureSpec)
  9. }
  10. /**
  11. * view的layoutparams是wrap_content,可以通过这个方法来获取真实宽高
  12. */
  13. fun measureViewOfWrapContent(view: View) {
  14. val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 shl 30) - 1, AT_MOST)
  15. //view的 尺寸 使用 30 位 二进制 表示, 也就是说 最大 是 30 个 1( 即 2^ 30 - 1), 也就是( 1 << 30) - 1, 在最 大化 模式 下, 我们 用 View 理论上 能 支持 的 最大值 去 构造 MeasureSpec 是 合理 的。
  16. val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 shl 30) - 1, AT_MOST)
  17. view.measure(widthMeasureSpec, heightMeasureSpec)
  18. }
  19. }
  • 错误用法关于 View 的 measure, 网络 上有 两个 错误 的 用法。 为什么 说是 错误 的, 首先 其 违背 了 系统 的 内部 实现 规范( 因为 无法 通过 错误 的 MeasureSpec 去 得出 合法 的 SpecMode, 从而 导致 measure 过程 出错), 其次 不能 保证 一 定能 measure 出 正确 的 结果。
  1. //第一 种 错误 用法:
  2. int widthMeasureSpec = MeasureSpec. makeMeasureSpec(- 1, MeasureSpec. UNSPECIFIED);
  3. int heightMeasureSpec = MeasureSpec. makeMeasureSpec(- 1, MeasureSpec. UNSPECIFIED);
  4. view. measure( widthMeasureSpec, heightMeasureSpec);
  5. //第二 种 错误 用法:
  6. view. measure( LayoutParams. WRAP_ CONTENT, LayoutParams. WRAP_ CONTENT)

View的测量宽/高和最终宽/高有什么区别

在View的默认实现中,View的测量宽/高和最终宽/高是相等的,只不过测量宽/高形成于View的measure过程,而最终宽/高形成于View的layout过程,即两者的赋值时机不同,测量宽/高的赋值时机稍微早一些。因此,在日常开发中,我们可以认为View的测量宽/高就等于最终宽/高,但是的确存在某些特殊情况会导致两者不一致

layout

  • 当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用其layout方法,在layout方法中onLayout方法又会被调用。
  • layout方法确定View本身的位置,而onLayout方法则会确定所有子元素的位置,

draw

  • view的绘制过程
    • 绘制 背景 background. draw( canvas)
    • 绘制 自己( onDraw)
    • 绘制 children( dispatchDraw)
    • 绘制 装饰( onDrawScrollBars)
  • invalidate(), 请求重绘 View 树,即 draw 过程,假如视图发生大小没有变化就不会调用layout()过程,并且只绘制那些调用了invalidate()方法的 View
  • requestLayout(), 当布局变化的时候,比如方向变化,尺寸的变化,会调用该方法,在自定义的视图中,如果某些情况下希望重新测量尺寸大小,应该手动去调用该方法,它会触发measure()layout()过程,但不会进行 draw。