该文章参考了
伯努力不努力(https://blog.csdn.net/u012124438/article/details/71435787)
Android_韦鲁斯(https://blog.csdn.net/sinat_27154507/article/details/79748010)
黑暗终将过去(https://www.jianshu.com/p/3654ab59b908)
博主的文章,如有侵权请通知删除
在介绍VIew绘制原理之前,简单介绍一下Window,ViewRootImpl,DecorView之间的联系。
一个 Activity 包含一个Window,Window是一个抽象基类,是 Activity 和整个 View 系统交互的接口,只有一个实现子类PhoneWindow,提供了一系列窗口的方法,比如设置背景,标题等。一个PhoneWindow 对应一个DecorView 跟 一个 ViewRootImpl,DecorView 是ViewTree 里面的顶层布局,是继承于FrameLayout,包含两个子View,一个id=statusBarBackground 的 View 和 LineaLayout,LineaLayout 里面包含 title 跟content,title就是平时用的TitleBar或者ActionBar, contenty也是FrameLayout,activity通过 setContent()加载布局的时候加载到这个View上。ViewRootImpl就是建立 DecorView 和 Window 之间的联系。
那么针对于View 绘制中measure,layout, draw 三个阶段他们的入口是什么呢?
measure :根据父 view 传递的 MeasureSpec 进行计算大小。
layout :根据 measure 子 View 所得到的布局大小和布局参数,将子View放在合适的位置上。
draw :把 View 对象绘制到屏幕上。
这三个阶段的核心入口是在 ViewRootImpl 类的 performTraversals() 方法中。
performTraversals会依次调用performMeasure、performLayout和performDraw三个方法,这三个方法分别完成顶级View的measure、layout和draw这三大流程。其中performMeasure中会调用measure方法,在measure方法中又会调用onMeasure方法,在onMeasure方法中则会对所有子元素进行measure过程,这样就完成了一次measure过程;子元素会重复父容器的measure过程,如此反复完成了整个View数的遍历。
private void performTraversals() {
......
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
......
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
......
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
......
mView.draw(canvas);
......
}
在源码中这个方法贼长,但是核心还是这三个步骤,就是判断根据之前的状态判断是否需要重新 measure,是否需要重新 layout ,是否需要重新 draw。
measure过程决定了View的宽/高,完成后可通过getMeasuredWidth/getMeasureHeight方法来获取View测量后的宽/高。
Layout过程决定了View的四个顶点的坐标和实际View的宽高,完成后可通过getTop、getBotton、getLeft和getRight拿到View的四个定点坐标。
Draw过程决定了View的显示,完成后View的内容才能呈现到屏幕上。
下面我们就逐个进行分析,先从measure
开始
measurespeac
在介绍 measure 方法之前,需要了解一个很核心的概念:measureSpeac 。在 Google 官方文档中是这么定义 measureSpeac 的
A MeasureSpec encapsulates the layout requirements passed from parent to child. Each MeasureSpec represents a requirement for either the width or the height. A MeasureSpec is comprised of a size and a mode.
大概意思是:MeasureSpec 封装了从父View 传递给到子View的布局需求。每个MeasureSpec代表宽度或高度的要求。每个MeasureSpec都包含了size(大小)和mode(模式)。
首先我们根据翻译一句一句进行解析:
MeasureSpec 封装了从父View 传递给到子View的布局需求。
View 的 MeasureSpec 并不是父 View 独自决定,它是根据父 view 的MeasureSpec加上子 View 的自己的LayoutParams,通过相应的规则转化。
View 测量流程是父 View 先测量子 View,等子 View 测量完了,再来测量自己。在ViewGroup 测量子 View 的入口就是 measureChildWithMargins
总而言之,第一句话的含义就是父View的大小宽高不是只由自身决定的,也需要根据子View来决定,这也就引出了上面第二三句话的,看懂了上面第二三句话,再结合自己平时的布局文件就能明白了。
MeasureSpec代表宽度或高度的要求。每个MeasureSpec都包含了size(大小)和mode(模式)。
MeasureSpec 一个32位二进制的整数型,前面2位代表的是mode,后面30位代表的是size。mode 主要分为3类,分别是
EXACTLY:父容器已经测量出子View的大小。对应是 View 的LayoutParams的match_parent 或者精确数值。
AT_MOST:父容器已经限制子view的大小,View 最终大小不可超过这个值。对应是 View 的LayoutParams的wrap_content
UNSPECIFIED:父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。(这种不怎么常用,下面分析也会直接忽略这种情况)
onMeasure()
由于measure是final类型的,所以子类不能覆盖,但是onMeasure方法可以被重写,所以我们可以在onMeasure方法中重写测量设置View尺寸。onMeasure也是测量View的核心代码。
在这个方法中测量流程是会判断如果父类传递过来的模式是否是MeasureSpec.UNSPECIFIED,如果是就会获取到最小建议值,如果不是有返回值AT_MOST或者EXACTLY模式,则设置父类传递过来的大小。 然后调用setMeasuredDimension 方法进行存储大小。
其次是layout
方法
对 View 进行排版布局,还是要看父 View,也就是 ViewGroup。
大致流程是判断 View 是否在执行动画,如果是在执行动画,则等待动画执行完调用 requestLayout (),如果没有添加动画或者动画已经执行完了,则调用 layout(),也就是调用View的 layout()。
接下来就是跳入了View的layout方法中,设置 View 的在父 View 的位置,然后判断位置是否发生变化,是否需要重新调用排版布局,如果是需要重新布局则用了 onLayout()方法。
onLayout
在OnLayout 方法中,View 里面是一个空实现,而 ViewGroup 则是一个抽象方法。为什么这么设计呢?因为onLayout中主要就是为了给遍历View然后进行排版布局,分别设置View在父View中的位置。既然如此,那么View的意义就不大了,而ViewGruop 必须实现,不然没法对子View进行布局。那么如何对 View 进行排版呢?
其实很简单,就是ViewGruop会遍历它里面的所有View然后调用每个view 的layout(l,t,r,b)方法进行位置设置。
draw 过程中一共分成7步,其中两步我们直接直接跳过不分析了。
第一步:drawBackground(canvas): 作用就是绘制 View 的背景。
第三步:onDraw(canvas) :绘制 View 的内容。View 的内容是根据自己需求自己绘制的,所以方法是一个空方法,View的继承类自己复写实现绘制内容。
第三步:dispatchDraw(canvas):遍历子View进行绘制内容。在 View 里面是一个空实现,ViewGroup 里面才会有实现。在自定义 ViewGroup 一般不用复写这个方法,因为它在里面的实现帮我们实现了子 View 的绘制过程,基本满足需求。
第四步:onDrawForeground(canvas):对前景色跟滚动条进行绘制。
第五步:drawDefaultFocusHighlight(canvas):绘制默认焦点高亮