为了研究Android中View的布局及绘图机制,我创建了一个非常简单的App,该App只有一个Activity,该Activity对应的layout如下所示:

"http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"    android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"    android:paddingRight="@dimen/activity_horizontal_margin"    android:paddingTop="@dimen/activity_vertical_margin"    android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">    "@string/hello_world" android:layout_width="wrap_content"        android:layout_height="wrap_content" /></RelativeLayout>

该布局文件很简单,RelativeLayout下面就一个TextView。

我们启动App后,通过Hierarchy Viewer查看App中的布局层级,如下所示:

从上图我们可以看出,App的根结点是PhoneWindow$DecorView,此处的$表示DecorView是PhoneWindow下面的内部类实例。PhoneWindow$DecorView下面有三个child,分别是LinearLayout实例、View@49da043和View@44ff410。View@49da043表示的是navigationBarBackground,View@44ff410表示的是statusBarBackground。LinearLayout下面有两个child,分别是ViewStub实例和FrameLayout实例,其中ViewStub不需要绘制,所以我们在下面的讨论中可以直接对其忽略。FrameLayout下有一个child,RelativeLayout实例,该RelativeLayout实例对应的就是布局文件activity_main.xml中的RelativeLayout,RelativeLayout下有一个child,即TextView。

以上提到的控件都是View的实例,有的则是ViewGroup的实例,ViewGroup继承自View,PhoneWindow$DecorView、RelativeLayout、FrameLayout、RelativeLayout都直接或间接继承自ViewGroup,只有ViewGroup实例才能有子节点。

当我们在onCreate()方法中调用setContentView(R.layout.activity_main)方法后,Android会从layout的树形结构中自上而下开始对所有的View进行量算、布局、绘图,具体来说经过以下过程:

  1. Android自上而下对所有View进行量算,这样Android就知道了每个View想要的尺寸大小,即宽高信息

  2. 在完成了对所有View的量算工作后,Android会自上而下对所有View进行布局,Android就知道了每个View在其父控件中的位置,即View到其父控件四边的left、right、top、bottom

  3. 在完成了对所有View的布局工作后,Android会自上而下对所有View进行绘图,这样Android就将所有的View渲染到屏幕上了

以下是涉及到的相关类的源码:
View源码
ViewGroup源码
ViewRootImpl源码
PhoneWindow$DecorView源码
LinearLayout源码
FrameLayout源码
RelativeLayout源码
TextView源码


量算

关于Measure:

  1. View用measure()方法进行量算,量算的目的是View让其父节点知道它想要多大的尺寸,所以说量算是后面对View进行布局以及绘图的基础。

  2. View的measure()方法中会执行onMeasure()方法,View类本身的onMeasure()方法不是空方法,其将量算完的结果保存到View中。View的子类不应该重写measure()方法,如果需要的话应该重写onMeasure()方法,ViewGroup的子类都应该重写onMeasure()方法,比如PhoneWindow$DecorView、RelativeLayout、FrameLayout、RelativeLayout都重写了onMeasure()方法,这些类都在onMeasure()方法中遍历child,并调用child的measure()方法,对child进行量算,纵向递归进行,从而实现自上而下对View树进行量算,直至完成对叶子节点View的量算。

  3. 量算的起点是ViewRootImpl类,ViewRootImpl是根View,即View树上面的根结点,严格来说ViewRootImpl不属于View,其实现了ViewParent接口, 其下才是PhoneWindow$DecorView

  4. Android在对View树进行自上而下的量算时,采用的是深度优先算法,而非广度优先算法,即遍历到某个View时,Android会首先沿着该View一直纵向遍历并量算到处于叶子节点的View,只有对该View及其所有子孙View(如果存在子孙View的话)完成量算后,才会量算该View的兄弟节点View。

以下是Android对所有View自上而下量算的调用过程:

  • 由上我们可以看出,首先ViewRootImpl执行了doTraversal()和performTraversals() 方法,然后执行ViewRootImpl的performMeasure()方法,该方法是Android对所有View进行量算的起点。在该方法中会从ViewRootImpl开始自上而上对View树进行遍历,首先ViewRootImpl对PhoneWindow$DecorView进行量算,在执行到PhoneWindow$DecorView的onMeasure()方法时,其遍历所有的child,对依次它们进行量算,首先对调用LinearLayout的measure()方法,对第一个子节点LinearLayout进行量算。

  • LinearLayout在measure()方法中会调用onMeasure()方法,在该方法中LinearLayout调用了measureVertical()方法,该方法会遍历其child并对其进行量算,由于其子节点ViewStub不用于渲染,所以此处不对其量算,对其忽略,对另一个child FrameLayout进行量算,调用FrameLayout的measure()方法。

  • FrameLayout在执行measure()方法时会执行onMeasure()方法,在该方法中会遍历所有的child,并对它们进行量算。其下只有一个child,即RelativeLayout,调用RelativeLayout的measure()方法,对其进行量算。

  • RelativeLayout在measure()方法中会执行onMeasure()方法,在该方法中会遍历所有的child,并对它们进行量算。其下只有一个child,即TextView,调用TextView的measure()方法对其进行量算,在其中会执行onMeasure()方法。

  • 以上完成了对View树中LinearLayout及其所有子算View的量算工作,之后会对PhoneWindow$DecorView中的另外两个View进行量算,这也体现了Android采用深度优先算法对View树进行遍历量算的过程。View@49da0d3和View@44ff410会依次执行measure()方法和onMeasure()方法。

这样整个View树自上而下的量算过程就结束了,经过量算Android知道了各个View想要渲染的尺寸大小,即宽度和高度信息。

关于量算中measure()和onMeasure()方法的一些细节可参见博文《 源码解析Android中View的measure量算过程》。


布局

关于Layout:

  1. 布局的前提是已经对View进行了量算,View通过调用layout()方法进行布局,布局的目的是让Android知道View在其父控件中的位置,即距父控件四边的距离left、right、top、bottom。布局是绘图的基础,只有完成了布局,才能对View进行绘图。

  2. View的layout()方法中会执行onLayout()方法,View类本身的onLayout()是空方法。View的子类不应该重写layout()方法,如果需要的话应该重写其onLayout()方法,ViewGroup的子类都应该重写onLayout()方法,比如PhoneWindow$DecorView、RelativeLayout、FrameLayout、RelativeLayout都重写了onLayout()方法,这些类都在onLayout()方法中遍历child,并调用child的layout()方法,对child进行布局,纵向递归进行,从而实现自上而下对View树进行布局,直至完成对叶子节点View的布局。

  3. 布局的起点也是ViewRootImpl类,ViewRootImpl是根View,即View树上面的根结点,严格来说ViewRootImpl不属于View,其实现了ViewParent接口, 其下才是PhoneWindow$DecorView

  4. Android在对View树进行自上而下的布局时,采用的是深度优先算法,而非广度优先算法,即遍历到某个View时,Android会首先沿着该View一直纵向遍历并布局到处于叶子节点的View,只有对该View及其所有子孙View(如果存在子孙View的话)完成布局后,才会布局该View的兄弟节点View。

Android中的布局过程与之前上面提到的量算过程很类似,以下是Android对所有View自上而下布局的调用过程:

  • 由上我们可以看出,首先ViewRootImpl执行了doTraversal()和performTraversals() 方法,然后执行ViewRootImpl的performLayout()方法,该方法是Android对所有View进行布局的起点。在该方法中会从ViewRootImpl开始自上而下对View树进行遍历,首先ViewRootImpl执行PhoneWindow$DecorView的layout()方法,对其进行布局。

  • PhoneWindow$DecorView在其layout()方法中会执行onLayout()方法,PhoneWindow$DecorView会在onLayout()方法中遍历其所有的child,并依次调用child的layout()方法,实现对child的布局。首先调用其第一个child LinearLayout的layout()方法。

  • LinearLayout在layout()方法中会执行onLayout()方法,在该方法中会调用layoutVertical()方法,该方法会遍历其所有的child并依次调用child的layout()方法进行布局。由于其子节点ViewStub不用于渲染,所以此处不对其进行布局,对其忽略,对另一个child FrameLayout进行布局,调用FrameLayout的layout()方法。

  • FrameLayout在layout()方法中会执行onLayout()方法,在该方法中会调用layoutChildren()方法,该方法会遍历其所有的child并依次调用child的layout()方法进行布局。其下只有一个child,即RelativeLayout,执行RelativeLayout的layout()方法,对其进行布局。

  • RelativeLayout在layout()方法中会执行onLayout()方法,在该方法中会遍历所有的child并依次调用child的layout()方法进行布局。其下只有一个child,即TextView,调用TextView的layout()方法对其进行布局,在其中会执行onLayout()方法。

  • 以上完成了对View树中LinearLayout及其所有子孙View的布局工作,之后会对PhoneWindow$DecorView中的另外两个View进行布局,这也体现了Android采用深度优先算法对View树进行遍历布局的过程。View@49da043和View@44ff410会依次执行layout()方法和onLayout()方法。

这样整个View树自上而下的布局过程就结束了,经过布局Android知道了各个View在其父控件中的位置。

关于布局layout的细节可参见博文《源码解析Android中View的layout布局过程》。


绘图

关于Draw:

  1. 绘图的前提是已经对View进行了量算和布局,View通过调用draw()方法进行绘图,绘图的目的就是让View在UI界面上呈现出来。

  2. View的draw()方法中会依次onDraw()和dispatchDraw()方法,View类本身的onDraw()和dispatchDraw()方法都是空方法。View的子类不应该重写draw()方法,如果需要的话应该按具体情况选择重写onDraw()方法或dispatchDraw()方法,具体来说:

    • 当我们需要自定义一个View(而非ViewGroup)时,我们需要重写View的onDraw()方法以实现对自定义View的绘制,即onDraw()用于绘制View自身UI。
    • Android中的ViewGroup类重写了View中的dispatchDraw()方法,ViewGroup.dispatchDraw()方法会遍历其所有的child,并依次调用child的draw()方法,即dispatchDraw()用于绘制ViewGroup的所有子孙View的UI,这是与onDraw()不同的。由于ViewGroup已经具体实现了dispatchDraw()方法,所以大部分情况下ViewGroup的子类无需再对其进行重写,例如PhoneWindow$DecorView、RelativeLayout、FrameLayout、RelativeLayout都没有重写dispatchDraw()方法。只有在极少数情况下,为了实现某些特殊需求,我们才有可能重写ViewGroup的dispatchDraw()方法,但是即便重写该方法我们也应该在我们的实现中调用super.dispatchDraw()方法以便实现对子孙View进行绘制。
  3. 绘图的起点也是ViewRootImpl类,ViewRootImpl是根View,即View树上面的根结点,严格来说ViewRootImpl不属于View,其实现了ViewParent接口, 其下才是PhoneWindow$DecorView。

  4. Android在对View树进行自上而下的绘图时,采用的也是深度优先算法,而非广度优先算法,即遍历到某个View时,Android会首先沿着该View一直纵向遍历并绘图到处于叶子节点的View,只有对该View及其所有子孙View(如果存在子孙View的话)完成绘图后,才会渲染该View的兄弟节点View。

Android中的绘图过程与之前上面提到的量算、布局过程类似,以下是Android对所有View进行自上而下绘图的调用过程:

  • 由上我们可以看出,首先ViewRootImpl执行了doTraversal()和performTraversals() 方法,然后执行ViewRootImpl的performDraw()方法,该方法是Android对所有View进行绘图的起点。在该方法中会从ViewRootImpl开始自上而下对View树进行遍历,首先ViewRootImpl执行PhoneWindow$DecorView的draw()方法,对其绘图。

  • PhoneWindow$DecorView在其draw()方法中会依次执行onDraw()方法和dispatchDraw()方法,在dispatchDraw()方法中会遍历所有的child,调用child的draw()方法,对child进行绘图。首先调用其第一个child LinearLayout的draw()方法。

  • LinearLayout在darw()方法中也会依次执行onDraw()方法和dispatchDraw()方法,在dispatchDraw()方法中会遍历所有的child,调用child的draw()方法,对child进行绘图。由于其子节点ViewStub不用于渲染,所以此处不对其进行绘图,对其忽略,对另一个child FrameLayout进行绘图,调用FrameLayout的draw()方法。

  • FrameLayout在draw()方法中也会依次执行onDraw()方法和dispatchDraw()方法,在dispatchDraw()方法中会遍历所有的child,调用child的draw()方法,对child进行绘图。其下只有一个child,即RelativeLayout,执行RelativeLayout的draw()方法,对其进行绘图。

  • RelativeLayout在draw()方法中也会依次执行onDraw()方法和dispatchDraw()方法,在dispatchDraw()方法中会遍历所有的child,调用child的draw()方法,对child进行绘图。其下只有一个child,即TextView,执行TextView的draw()方法,对其进行绘图,并在其中执行TextView的onDraw()方法,对TextView进行实际的渲染。

  • 以上完成了对View树中LinearLayout及其所有子孙View的绘图工作,之后会对PhoneWindow$DecorView中的另外两个View进行绘图,这也体现了Android采用深度优先算法对View树进行遍历绘图的过程。View@49da043和View@44ff410会依次执行draw()方法和onDraw()方法。


总结

当我们在onCreate()方法中调用setContentView(R.layout.activity_main)方法后,Android会从layout的树形结构中自上而下开始对所有的View进行量算、布局、绘图:

  1. 量算、布局、绘图的起点都是ViewRootImpl

  2. 通过调用ViewRootImpl的performMeasure() 方法,开始驱动Android自上而下对所有View进行量算,这样Android就知道了每个View想要的尺寸大小,即宽高信息

  3. 在完成了对所有View的量算工作后,通过调用ViewRootImpl的performLayout()方法,开始驱动Android会自上而下对所有View进行布局,Android就知道了每个View在其父控件中的位置,即View到其父控件四边的left、right、top、bottom

  4. 在完成了对所有View的布局工作后,通过调用ViewRootImpl的performDraw()方法,开始驱动Android会自上而下对所有View进行绘图,这样Android就将所有的View渲染到屏幕上了

希望本文对大家理解Android中View的布局和绘图机制有所帮助。

相关阅读:
《Android相关博文整理汇总》
《源码解析Android中View的measure量算过程》
《源码解析Android中View的layout布局过程》

更多相关文章

  1. Android网络请求库android-async-http使用
  2. Android面试题合集【上】
  3. [Android] SQLite的使用入门
  4. android 的layout
  5. 关于正确使用Android(安卓)AsyncTask学习整理
  6. Android(安卓)SystemUI源代码分析和修改
  7. 干货 | 彻底理解ANDROID BINDER通信架构(上)
  8. Android的存储----重新认识Android(9)
  9. Android(安卓)如何禁止屏幕灭屏

随机推荐

  1. 这个Spring高危漏洞,你修补了吗?
  2. 美团点评酒旅数据仓库建设实践
  3. LsLoader——通用移动端Web App离线化方
  4. IntelliJ IDEA母公司JetBrains遭美国调查
  5. Spring Data REST 远程代码执行漏洞(CVE-2
  6. 日产汽车源码遭泄露
  7. 孵化业务快速落地与优化
  8. iPhone X 刘海打理指北
  9. 封杀两年后,GitHub恢复伊朗开发者使用权限
  10. 美团点评酒店后台故障演练系统