参考链接:
Android LayoutInflater原理分析,带你一步步深入了解View(一)
Android视图绘制流程完全解析,带你一步步深入了解View(二)
Android视图状态及重绘流程分析,带你一步步深入了解View(三)
本篇文章会从源码(基于Android 8.0 API 26)角度分析Android中View的绘制流程,侧重于对整体流程的分析,对一些难以理解的点加以重点阐述,目的是把View绘制的整个流程把握好,而对于特定实现细节则可以日后再对相应源码进行研读。
DecorView是一个应用窗口的根容器,它本质上是一个FrameLayout。DecorView有唯一一个子View,它是一个垂直LinearLayout,包含两个子元素,一个是TitleView(ActionBar的容器),另一个是ContentView(窗口内容的容器)。关于ContentView,它是一个FrameLayout(android.R.id.content),我们平常用的setContentView就是设置它的子View。上图还表达了每个Activity都与一个Window(具体来说是PhoneWindow)相关联,用户界面则由Window所承载。
一、Window
Window即窗口,这个概念在Android Framework中的实现为android.view.Window这个抽象类,这个抽象类是对Android系统中的窗口的抽象。
这个抽象类包含了三个核心组件:
WindowManager.LayoutParams: 窗口的布局参数;
Callback: 窗口的回调接口,通常由Activity实现;
ViewTree: 窗口所承载的控件树。
二、PhoneWindow
我们先从AppCompatActivity的setContentView开始,其实这个getDelegate是个代理类,AppCompat的内在逻辑现在可以通过AppCompatDelegate实现-这是一个可以在所有Activity中包含的类,与合适的生命周期方法挂钩。
其中AppCompatDelegateImplV9这个是类是AppCompatDelegate的一个实现类。他实现了setContentView方法。其中mSubDecor就是根布局DecorView。
在ensureSubDecor方法中,创建了mSubDecorView.
在createSubDecor()这个方法中加载了DecorView。方法中调用了LayoutInflater的inflate()方法来填充布局。有WindowTitle的情况下加载了R.layout.abc_dialog_title_material布局。
根布局其实是一个LinearLayout。最终把DecorView放到了PhoneWindow中。
在PhoneWindow.setContentView方法如下。然后调用installDecor方法
在PhoneWindow的generateLayout方法中找到了根布局文件
R.layout.screen_simple
DecorView又通过onResourcesLoaded,将跟布局添加在DecorView中,实际上是一个frameLayout容器。
走到这里,我们再来AppCompatDelegateImplV9.createSubDecor中的方法。从PhoneWindow找到R.id.content布局,然后通过一个while循环,R.id.content布局中的View全部添加在AppCompatActivity所在的DecorView中,并把DecorView中的contentView 的id设置为R.id.content,彻底将R.id.content中的View进行更换。
while(windowContentView.getChildCount() >0) {
finalView child = windowContentView.getChildAt(0);
windowContentView.removeViewAt(0);
contentView.addView(child);
}
布局替换完成之后,我们再来看看AppCompatDelegateImplV9的setContentView方法。
之后通过mLayoutInflater.inflate(layoutResID,mContentParent),房布局文件解析成View
在LayoutInflater的inflate方法中,通过Resource.getLayout方法获取一个XmlResourceParser
调用*/
publicViewinflate(XmlPullParser parser,@Nullable ViewGroup root, booleanattachToRoot) 方法将xml 解析为View。
Resources.loadXmlResourceParser方法
LayoutInflater的rinflate方法中 经常看到一下两句话。
一、View 树的绘制流程
二、measure
文章参考: Android视图绘制流程完全解析
1、ViewGroup.LayoutParams
封装了很多布局参数,布局参数。
2、MeasureSpec 测量规格
MeasureSpec代表一个32位int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而specSize是指在某种测量模式下的规格大小。
1. EXACTLY exactly
表示父视图希望子视图的大小应该是由specSize的值来决定的,系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。它对应于LayoutParams中的match_parent和具体的数值这两种模式
2. AT_MOST
表示子视图最多只能是specSize中指定的大小,开发人员应该尽可能小得去设置这个视图,并且保证不会超过specSize。系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。它对应于LayoutParams中的Wrap_content
3. UNSPECIFIED unspecified
表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这种情况比较少见,不太会用到。
3、mesaure一些重要方法
measure
measure是测量的意思,那么onMeasure()方法顾名思义就是用于测量视图的大小的。View系统的绘制流程会从ViewRoot的performTraversals()方法中开始,在其内部调用View的measure()方法。measure()方法接收两个参数,widthMeasureSpec和heightMeasureSpec,这两个值分别用于确定视图的宽度和高度的规格和大小。private voidperformTraversals()
onMeasure
setMeasuredDimension()
这里传入的measureSpec是一直从measure()方法中传递过来的。然后调用MeasureSpec.getMode()方法可以解析出specMode,调用MeasureSpec.getSize()方法可以解析出specSize。接下来进行判断,如果specMode等于AT_MOST或EXACTLY就返回specSize,这也是系统默认的行为。之后会在onMeasure()方法中调用setMeasuredDimension()方法来设定测量出的大小,这样一次measure过程就结束了。
需要注意的是,在setMeasuredDimension()方法调用之后,我们才能使用getMeasuredWidth()和getMeasuredHeight()来获取视图测量出的宽高,以此之前调用这两个方法得到的值都会是0。
在ViewGroup中 定义了一个measureChildred()方法,进行子View的测量。
调用measureChild()进行测量子View。
三、layout
在onLayout()过程结束后,我们就可以调用getWidth()方法和getHeight()方法来获取视图的宽高了。说到这里,我相信很多朋友长久以来都会有一个疑问,getWidth()方法和getMeasureWidth()方法到底有什么区别呢?它们的值好像永远都是相同的。其实它们的值之所以会相同基本都是因为布局设计者的编码习惯非常好,实际上它们之间的差别还是挺大的。
首先getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。另外,getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的。
四、draw 两个容易混淆的方法
ViewRootImpl方法中调用performDraw()方法。
在performDraw()方法中,最终调用了view.draw方法。
View有两个很重要的方法:invalidate和requestLayout,常用于View重绘和更新。
1、invalidate()方法
该方法的调用会引起View树的重绘,常用于内部调用(比如 setVisiblity())或者需要刷新界面的时候,需要在主线程(即UI线程)中调用该方法。那么我们来分析一下它的实现。
2.requestLayout()方法
当View的边界,也可以理解为View的宽高,发生了变化,不再适合现在的区域,可以调用requestLayout方法重新对View布局。
View执行requestLayout方法,会向上递归到顶级父View中,再执行这个顶级父View的requestLayout,所以其他View的onMeasure,onLayout也可能会被调用。
View绘制分三个步骤,顺序是:onMeasure,onLayout,onDraw。经代码亲测,log输出显示:调用invalidate方法只会执行onDraw方法;调用requestLayout方法只会执行onMeasure方法、onLayout方法和onDraw方法。
所以当我们进行View更新时,若仅View的显示内容发生改变且新显示内容不影响View的大小、位置,则只需调用invalidate方法;若View宽高、位置发生改变且显示内容不变,只需调用requestLayout方法;若两者均发生改变,则需调用两者,按照View的绘制流程,推荐先调用requestLayout方法再调用invalidate方法。
invalidate和postInvalidate:invalidate方法只能用于UI线程中,在非UI线程中,可直接使用postInvalidate方法,这样就省去使用handler的烦恼。
执行postInvalidate()方法时,会调用 postInvalidateDelayed,
紧接着就会调用ViewRootImpl中的dispatchInvalidateDelayed,代码中可以看出使用Handler发送了一个消息,最终还是执行View 的invalidate方法。
参考文章