为了研究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进行量算、布局、绘图,具体来说经过以下过程:
Android自上而下对所有View进行量算,这样Android就知道了每个View想要的尺寸大小,即宽高信息
在完成了对所有View的量算工作后,Android会自上而下对所有View进行布局,Android就知道了每个View在其父控件中的位置,即View到其父控件四边的left、right、top、bottom
在完成了对所有View的布局工作后,Android会自上而下对所有View进行绘图,这样Android就将所有的View渲染到屏幕上了
以下是涉及到的相关类的源码:
View源码
ViewGroup源码
ViewRootImpl源码
PhoneWindow$DecorView源码
LinearLayout源码
FrameLayout源码
RelativeLayout源码
TextView源码
关于Measure:
View用measure()方法进行量算,量算的目的是View让其父节点知道它想要多大的尺寸,所以说量算是后面对View进行布局以及绘图的基础。
View的measure()方法中会执行onMeasure()方法,View类本身的onMeasure()方法不是空方法,其将量算完的结果保存到View中。View的子类不应该重写measure()方法,如果需要的话应该重写onMeasure()方法,ViewGroup的子类都应该重写onMeasure()方法,比如PhoneWindow$DecorView
、RelativeLayout、FrameLayout、RelativeLayout都重写了onMeasure()方法,这些类都在onMeasure()方法中遍历child,并调用child的measure()方法,对child进行量算,纵向递归进行,从而实现自上而下对View树进行量算,直至完成对叶子节点View的量算。
量算的起点是ViewRootImpl类,ViewRootImpl是根View,即View树上面的根结点,严格来说ViewRootImpl不属于View,其实现了ViewParent接口, 其下才是PhoneWindow$DecorView
。
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:
布局的前提是已经对View进行了量算,View通过调用layout()方法进行布局,布局的目的是让Android知道View在其父控件中的位置,即距父控件四边的距离left、right、top、bottom。布局是绘图的基础,只有完成了布局,才能对View进行绘图。
View的layout()方法中会执行onLayout()方法,View类本身的onLayout()是空方法。View的子类不应该重写layout()方法,如果需要的话应该重写其onLayout()方法,ViewGroup的子类都应该重写onLayout()方法,比如PhoneWindow$DecorView、RelativeLayout、FrameLayout、RelativeLayout都重写了onLayout()方法,这些类都在onLayout()方法中遍历child,并调用child的layout()方法,对child进行布局,纵向递归进行,从而实现自上而下对View树进行布局,直至完成对叶子节点View的布局。
布局的起点也是ViewRootImpl类,ViewRootImpl是根View,即View树上面的根结点,严格来说ViewRootImpl不属于View,其实现了ViewParent接口, 其下才是PhoneWindow$DecorView
。
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:
绘图的前提是已经对View进行了量算和布局,View通过调用draw()方法进行绘图,绘图的目的就是让View在UI界面上呈现出来。
View的draw()方法中会依次onDraw()和dispatchDraw()方法,View类本身的onDraw()和dispatchDraw()方法都是空方法。View的子类不应该重写draw()方法,如果需要的话应该按具体情况选择重写onDraw()方法或dispatchDraw()方法,具体来说:
绘图的起点也是ViewRootImpl类,ViewRootImpl是根View,即View树上面的根结点,严格来说ViewRootImpl不属于View,其实现了ViewParent接口, 其下才是PhoneWindow$DecorView。
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进行量算、布局、绘图:
量算、布局、绘图的起点都是ViewRootImpl
通过调用ViewRootImpl的performMeasure() 方法,开始驱动Android自上而下对所有View进行量算,这样Android就知道了每个View想要的尺寸大小,即宽高信息
在完成了对所有View的量算工作后,通过调用ViewRootImpl的performLayout()方法,开始驱动Android会自上而下对所有View进行布局,Android就知道了每个View在其父控件中的位置,即View到其父控件四边的left、right、top、bottom
在完成了对所有View的布局工作后,通过调用ViewRootImpl的performDraw()方法,开始驱动Android会自上而下对所有View进行绘图,这样Android就将所有的View渲染到屏幕上了
希望本文对大家理解Android中View的布局和绘图机制有所帮助。
相关阅读:
《Android相关博文整理汇总》
《源码解析Android中View的measure量算过程》
《源码解析Android中View的layout布局过程》