Android进阶技术:View绘制流程!
转载于: https:// juejin.cn/post/69137430 20244336653
Android View的绘制流程分为三大流程:测量、布局、绘制。三大流程都开始于ViewRootImpl的
performTraversals
函数。通过了解三大流程的顺序和原理,支撑日常开发工作。绘制是Android进阶拦路虎之一。
一、测量流程
三大流程都是始于ViewRootImpl的
performTravels
函数,先是从调用View的
performMeasure
函数开始测量流程,再是调用
performLayout
函数开始布局流程,进而是调用
performDraw
函数开始绘制流程。本节从
performMeasure
函数开始,讲View的测量流程。
正式开始测量流程了~
performMeasure
函数会调用View的
measure
函数。
measure
函数第一行会调用
isLayoutModeOptical
函数,用来判断当前View是否ViewGroup ,是ViewGroup的话,判断
layoutModel
属性是否
LAYOUT_MODE_OPTICAL_BOUNDS
,即
opticalBounds
。该属性默认为
clipBounds
,还可取值
opticalBounds
,前者在获取ViewGroup的四边(
getLeft
,
getTop
,
getRight
,
getBottom
)将返回原始的值,而
opticalBounds
表示给ViewGroup加一些特殊的效果,例如阴影或高亮效果,因为返回的四边也将比
clipBounds
小。
measure
函数接下来的这一段主要是为了判断是否需要进行重新测量,毕竟每次测量也不容易。
//用于存储上次测量的结果
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
//view是否需要强行刷新,调用froceLayout
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
//判断此次的widthMeasureSpec与heightMeasureSpec是否与上次相等
final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
|| heightMeasureSpec != mOldHeightMeasureSpec;
//判断此次测量模式是否精确,不是精确的可能需要重新测量
final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
//判断此次测量大小是否与已保存的大小一致,不是一致可能需要重新测量
final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
//如果specChanged为false,即宽高measureSpec与上次都相等,不需要重新测量;true则进一步检查其他条件
//sAlwaysRemeasureExactly主要用于判断LinearLayout在旧版本的不同测量模式都会返回不同的测量结果,小于Android 6.0为true,大于为false;所以但小于Android 6.0需要重新测量
//如果isSpecExactly测量模式是非精确模式需要重新测量
//如果matchesSpecSize与已保存大小不一致需要重新测量
final boolean needsLayout = specChanged
&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
needsLayout
就是根据上面相关变量的值共同判断是否需要重新测量的最终结果。也可以通过下图一览上面的注释。
接着
measure
函数的内容,当调用
forceLayout
或
requestLayout
函数,
mPrivalteFlags
就会添加
PFLAG_FORCE_LAYOUT
标记,那么
forceLayout
就是true,无论后面其他判断条件怎么样,一定会调用
onMeasure
函数进行测量。而
needsLayout
就在上文刚分析了。
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
语句重置所有的已设置的测量信息,毕竟要准备重新开始测量了。
resolveRtlPropertiesIfNeeded()
主要是处理文本从右到左的情况,因为并不是所有国家文字书写顺序都是从左到右。
LongSparseLongArray
是key与value都为Long,类似HashMap的数据结构。这里正是通过这种结构用来存储测量的宽和高,如果
mMeasureCache.indexOfKey(key)
返回值小于0,表示不存在对应的宽高,需要测量。
sIgnoreMeasureCache
表示为了性能优化而忽略测量缓存,其实是为了兼容旧版本,因为在Android4.4前,APP总是希望
onMeasure
函数被调用,所以该变量总是true,而Android 4.4和后续版本,该标志总是false。
因此,如果需要测量,则调用当前View的
onMeasure
函数;不需要重新测量,则从缓存mMeasureCache获取已缓存宽高。
measure
函数的最后代码就是保存父View对当前View的宽高要求和往mMeasureCache存值,以供下次测量作为判断条件使用。
measure函数总结一下:
measure函数主要是为了性能优化,根据缓存(已缓存)、父类约束是不与上次一致,和行为(刷新布局)来判断是否重新测量大小。
接下来看看View的
onMeasure
函数做了什么事:
看着简单,其实还是要拆解看看:
getSuggestedZMininumWidth
函数主要判断当前是否设置背景,如果没有设置背景,则取最小宽度;设置了背景,则取最小宽度和背景最小宽度的两者之间的最大值。最小宽度就是我们设置的
minWidth
属性。高度的测量亦是如此。
getDefaultSize
函数主要是根据测量模式,计算出默认的尺寸大小。
到这里,就应该需要对MeasureSpec的大小和测量模式解释一下,不然有的同学真一脸懵逼。MeasureSpec是View的静态内部类,代表一个32位的整型,高2位表示测量模式,低30位表示尺寸大小。
measure
函数的两个参数widthMeasureSpec,heightMeasureSpec,分别代表着父View对子View的宽高约束。从这里也可以看出,子View的大小由父View约束和子View自身自身约束共同确定。
通过MeasureSpec提供的一些静态方法,如
int getSize(int measureSpec)
、
int getMode(int measureSpec)
,可以获取到测量模式mode和大小size,分别为:
-
EXACTLY
:当View的layout_width或者layout_height设置为
match_parent
或具体的值时,该测量模式就是EXACTLY,表示父View对当前View的尺寸要求大小是size; -
AT_MOST
:当View的
layout_width
或者layout_height
属性设置为wrap_content
,该测量模式就是AT_MOST,表示父View能给予当前View的最大的可用尺寸是size,具体用多少当前View自己决定; - UNSPECIFIED :表示父View对当前View没有任何约束,想要多大的尺寸当前View自己决定。
从
getDefaultSize
函数对测量模式
AT_MOST
和
EXACTLY
的处理方式看,自定义View继承View时,要格外注意
layout_width
或
layout_height
属性值为
wrap_content
的情况,因为它的表现就跟
match_parent
是一样的,有时需要根据具体情况去更改这种行为。
setMeasureDimension
函数开始跟
measure
函数类似,先判读一下
layoutModel
是否
optical bound
,进行宽高的调整,并调用
setMeasureDimensionRaw
函数。
setMeasureDimension
则是简单的赋值,设置
mPrivateFlags
标志位。这样就可以通过
getMeasuredWidth
与
getMeasuredHeight
函数来获取测量的宽高了。
注意:
重写
onMeasure
函数需要调用
setMeasureDimension
函数进行数据缓存。
测量流程也就到此结束了。但仔细一想,发现不对劲,这里测量指的是View,那么ViewGroup呢?
ViewGroup是View的子类,而View的
measure
函数被被声明成了final,所以ViewGroup测量自身或者测量子View只能重写
onMeasure
函数。但在ViewGroup类仔细寻找,却没有发现重写
onMeasure
函数的痕迹。具体原因是因为具体的ViewGroup,如LinearLayout和RelativeLayout它们各自的测量方式是不一样的,
onMeasure
需要它们具体去实现。但ViewGroup类提供了一些便捷的api,如
measureChildren
、
measureChildWithMargins
、
measureChild
等等。 翻翻LinearLayout的
onMeasure
函数,最终也会调用View的
measure
函数,走View的测量流程。
因此自定义View或者ViewGroup,需要根据自身实现的功能去重写omMeasure函数,来测量自身或子View的大小
二、布局流程
上一节分析了测量流程,得知了每个View的宽高大小,这一节紧跟着分析布局流程,判断子View如何在父View进行定位。
performLayout
函数同样是在ViewRootImpl类的
performTraversals
函数中,
performMeasure
函数之后。
可以看到,
performLayout
函数很快就调用了View的
layout
函数进行布局流程。这里先不跟进去,只需要知道已经进行了一次布局,然后看
performLayout
函数的后续内容。
mLayoutRequesters
是一个保存了在布局过程中所有请求布局的View的列表。当列表不为空时候,需要对这些View进行处理。
在布局的过程中,可能View请求布局(即设置了
PFLAG_FORCE_LAYOUT
),将它们存到列表
mLayoutRequesters
中,然后在布局结束后,第一次通过
getValidLayoutRequesters
函数判断这些View是否需要重新布局,判断条件就是当前View是否可见和设置了
PFLAG_FORCE_LAYOUT
标志。
如果返回值
validLayoutRequesters
不为空,重新设置他们的标志位
PFLAG_FORCE_LAYOUT
,并调用
measureHierarchy
函数,对它们进行View层级的测量,测量流程和整个界面测量流程是一致。然后再跟着重新布局一次
host.layout()
。
进行第二次判断是否还有在布局过程中,有View请求布局,如果有的话,判断有效的需要重新布局的View,这次判断忽略了
PFLAG_FORCE_LAYOUT
标志位,除了不可见的View,其他都列为需要有效的。然后留到下次帧再重新来过。
总结一下
在第一次布局的过程中,如果有View需要
requestLayout
函数(一般发生在ListView等的子View),则需要判断这些View是否可见或已经处理了
requestLaout
。如果有可见的、未处理
requestLayout
的View则需要进行View层次级别的测量,然后重新布局一次。然后进行第二次判断是否有View需要
requestlayout
,这次只判断是否可见。如果还有,这些View就留到下一帧进行吧,老子不管了。
再回到第一次布局
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
。
setOpticalFrame
函数最终也会调用setFrame,只是追加了点效果边距长度。
setFrame
函数主要是对当前View在父View的位置进行确定,如果此时定位位置有变(四边有不一致),则changed返回的是true。在
setFrame
函数会调用
sizeChanged
函数,而
sizeChanged
函数会调用
onSizeChanged
函数。
onLayout
函数在View中是一个空实现,而在ViewGroup未重写该方法。因为子View在父View位置,在不同的ViewGroup表现也是不同的,所以需要具体的ViewGroup根据自己的特性去重写。但这里我们注意到一个时机,
onSizeChanged
函数的回调在
onMeasure
函数之后,
onLayout
函数之前,在尺寸大小发生变化时会回调该方法。
在调用
onLaout
函数后的主要进行
OnLayoutChangeListener
的回调和焦点的处理。
isLayoutValid
函数表示至少已经经历过一次布局了或者不会再进行其他布局了,就返回true。
到这里,布局流程基本也就结束了。
本节小结
布局流程始于ViewRootImpl的
performTraversals
函数,然后调用自身的
performLayout
函数,对View进行布局,布局结束后对布局过程有请求布局的View进行View层级测量和布局。在View的
layout
函数中,通过
setFrame
对自身进行布局定位,如果位置发生变化则回调
onSizeChanged
函数。再而是调用
onLayout
函数。因此自定View无需重写
onLayout
函数,自定义ViewGroup则需要重写
onLayout
函数进行子View的布局。
三、绘制流程
经过测量、绘制,已经知道了View的大小,在父View的位置,那么接下来就是如何将View绘制出来,展现在屏幕。
绘制流程始于ViewRootImpl的
performTraversals
函数,调用自身的
performDraw
函数。
//ViewRootImpl.java
performTraversals=>performDraw=>draw=>drawSoftware=>View.draw
在
draw
函数中,主要是绘制区域
dirty
的确定,例如是否滚动、全部绘制等。
drawSoftware
函数就是通过软件去绘制的地方,主要根据dirty区域,生成并锁定canvas,而canvas就是绘制内容的区域。
而在View的
draw
函数,则是View的绘制的开始:
drawBackground=>onDraw=>dispatchDraw=>onDrawForeground=>drawDefaultFocusHighlight
在View的draw流程中,View一般重写
onDraw
函数,
super.onDraw
后绘制自己的内容,表示所绘制内容在系统绘制的内容之后。而在ViewGroup中,如果需要覆盖在子View之上,应该是重写
dispatchDraw
函数,并调用
super.dispatchDraw
之后,因为
dispatchDraw
函数会去绘制所有子View的内容,在之前绘制的内容都会被覆盖。当然,也可以以
dispatchDraw
作为分界点,根据需要重写其他函数,绘制内容。
如果重写ViewGroup的
onDraw
函数,绘制的内容一般显示不出来,因为ViewGroup会优化从而跳过
onDraw
函数,可以通过设置背景或
setWillNotDraw(false)
来解决这个问题。
四、总结
通过学习Android的绘制流程,需要知道几点情况:
-
自定View时,需要考虑宽高设置
wrap_content
的情况,因为它的表现在测量阶段和match_parent
是一致的。 -
重写View的
onDraw
函数,要避免在onDraw创建对象,因为onDraw会被调用多次,可以考虑在onSizeChanged
函数创建。 -
如果View或ViewGroup需要改变自身大小,应该在
onMeasure
函数实现,并通过setMeasureDimension
保存下来。 -
重写ViewGroup的
onDraw
函数时,要注意onDraw函数在整个draw流程的地位,以及它并不是都会被调用。
番外篇
1、MeasureSpec是什么?作用
MeasureSpec
在View中的一个静态内部类,能将一个32位整型拆分成测量模式和测量大小,代表着父View对子View的约束。32位的整型,高两位代表着测量模式,低三十位代表测量大小。通过位位运算,可以分别获取测量模式和大小,而合并成一个32位整型,只需要相加即可。
例如,给宽设置10,此时测量模式是精确模式EXACTLY,即01,用32位的整型表示应该是(暂且用xxx表示中间所有的0)
若求测试模式model,只需要和高两位都是1,低三十位都是0的
MODE_MASK
按位与即可。
代码:
public static int getMode(int measureSpec) {