接上篇 绘制优化-原理篇2-DecorView布局加载流程 讲到的ViewRootImpl,在ViewRootImpl的setView()方法里主要做两件事:
1.执行requestLayout()方法完成view的绘制流程
2.通过WindowSession将View和InputChannel添加到WmS中,从而将View添加到Window上并且接收触摸事件。
2的部分 window加载视图已经介绍了,那么今天就来讲讲1的部分:执行requestLayout()方法完成view的绘制流程
//ViewRootImpl
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
//在 Window add之前调用,确保 UI 布局绘制完成 --> measure , layout , draw
requestLayout();//View的绘制流程
...
//通过WindowSession进行IPC调用,将View添加到Window上
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
}
...
}
一、从requestLayout开始
从requestLayout代码一层层往下追(具体源码不贴了,非常简单),最终确认view的绘制流程是从performTraversals开始。顺一下整个流程:
1.1 performTraversals
private void performTraversals() {
//获得view宽高的测量规格,mWidth和mHeight表示窗口的宽高,lp.width和lp.height表示DecorView根布局宽和高
WindowManager.LayoutParams lp = mWindowAttributes;
...
//顶层视图DecorView所需要窗口的宽度和高度
int desiredWindowWidth;
int desiredWindowHeight;
...
//在构造方法中mFirst已经设置为true,表示是否是第一次绘制DecorView
if (mFirst) {
mFullRedrawNeeded = true;
mLayoutRequested = true;
//如果窗口的类型是有状态栏的,那么顶层视图DecorView所需要窗口的宽度和高度就是除了状态栏
if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL
|| lp.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD) {
// NOTE -- system code, won't try to do compat mode.
Point size = new Point();
mDisplay.getRealSize(size);
desiredWindowWidth = size.x;
desiredWindowHeight = size.y;
} else {//否则顶层视图DecorView所需要窗口的宽度和高度就是整个屏幕的宽高
DisplayMetrics packageMetrics =
mView.getContext().getResources().getDisplayMetrics();
desiredWindowWidth = packageMetrics.widthPixels;
desiredWindowHeight = packageMetrics.heightPixels;
}
}
...
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
...
// Ask host how big it wants to be
//执行测量操作
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
//执行布局操作
performLayout(lp, mWidth, mHeight);
...
//执行绘制操作
performDraw();
}
performTraversals()中做了非常多的处理,代码接近800行,这里我们重点关注绘制相关流程。
1.2 MeasureSpec
在分析绘制过程之前,我们需要先了解MeasureSpec,它是干什么的呢?简而言之,MeasureSpec 是View的尺寸一种封装手段。
MeasureSpec代表一个32位int值,高2位代表SpecMode,低30位代表SepcSize. 这样的打包方式好处是避免过多的对象内存分配。为了方便操作,其提供了打包和解包的方法:
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
...
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
...
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK); //高2位运算
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);//低30位运算
}
}
getMode方法中ModeMask 为 0x3 << 30 转换成二进制为 0011 << 30 ,也就是向左移动30位 则 ModeMask高两位为1,低三十位为0,整形measureSpec 为32位, measureSpec & mode_mask 就是高2位的运算,getSize方法中 ~Mode_MASK 则是除了高两位为0 外剩下的低30位均为 1,那么和measure 进行 & 运算就是在求得低30为中存储的值。
SpecMode:测量模式
模式 | 描述 |
---|---|
UNSPECIFIED | 父容器不作限制,一般用于系统内部 |
EXACTLY | 精确模式,大小为SpecSize,对应LayoutParams中的match_parent或者具体数值 |
AT_MOST | 最大模式,大小不能大于SpecSize,对应于LayoutParams中的warp_content |
SpecSize:对应某种测量模式下的尺寸大小
下面针对DecorView和普通View分别来看看其MeasureSpec的组成:
//ViewRootImpl
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
DecorView, 其MeasureSpec由窗口尺寸和其自身LayoutParams共同决定,
}
//ViewGroup
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定。
}
对普通View的MeasureSpec的创建规则进行总结:
这个表怎么用呢?举个例子:
如果View在布局中使用wrap_content,那么它的specMode是AT_MOST,这种模式下,它的宽高为specSize, 而查表可得View的specSize是parentSize,而parentSize是当前父容器剩余空间大小,这种效果和在布局中使用match_parent完全一致,所以如果是对尺寸有具体要求的自定义控件需要指定specSize大小。
注:
LayoutParams类是用于子视图向父视图传达自己尺寸意愿的一个参数包,包含了Layout的高、宽信息。LayoutParams在LayoutInflater.inflater过程中与View一起被解析成对象,保存在WindowManagerGlobal集合中。
二、View绘制流程
performTraversals里面执行了三个方法,分别是performMeasure()、performLayout()、performDraw()这三个方法,这三个方法分别完成DecorView的measure、layout、和draw这三大流程,其中performMeasure()中会调用measure()方法,在measure()方法中又会调用onMeasure()方法,在onMeasure()方法中会对所有子元素进行measure过程,这个时候measure流程就从父容器传递到子元素中了,这样就完成了一次measure过程。接着子元素会重复父容器的measure过程,如此反复就实现了从DecorView开始对整个View树的遍历测量,measure过程就这样完成了。同理,performLayout()和performDraw()也是类似的传递流程。针对performTraveals()的大致流程,可以用以下流程图来表示:
以上的流程图只是一个为了便于理解而简化版的流程,真正的流程应该分为以下五个工作阶段:
预测量阶段:这是进入performTraversals()方法后的第一个阶段,它会对View树进行第一次测量。在此阶段中将会计算出View树为显示其内容所需的尺寸,即期望的窗口尺寸。(调用measureHierarchy())
窗口布局阶段:根据预测量的结果,通过IWindowSession.relayout()方法向WMS请求调整窗口的尺寸等属性,这将引发WMS对窗口进行重新布局,并将布局结果返回给ViewRootImpl。(调用relayoutWindow())
测量阶段:预测量的结果是View树所期望的窗口尺寸。然而由于在WMS中影响窗口布局的因素很多,WMS不一定会将窗口准确地布局为View树所要求的尺寸,而迫于WMS作为系统服务的强势地位,View树不得不接受WMS的布局结果。因此在这一阶段,performTraversals()将以窗口的实际尺寸对View树进行最终测量。(调用performMeasure())
布局阶段:完成最终测量之后便可以对View树进行布局了。(调用performLayout())
绘制阶段:这是performTraversals()的最终阶段。确定了控件的位置与尺寸后,便可以对View树进行绘制了。(调用performDraw())
下面分别来阐述:
2.1 预测量阶段(performTraversals())
这个阶段在performTraversals中最先发生,对View树进行第一次测量,会判断当前期望窗口尺寸是否能满足布局要求。
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
int childWidthMeasureSpec;
int childHeightMeasureSpec;
// 表示测量结果是否可能导致窗口的尺寸发生变化
boolean windowSizeMayChange = false;
//goodMeasure表示了测量是否能满足View树充分显示内容的要求
boolean goodMeasure = false;
//测量协商仅发生在LayoutParams.width被指定为WRAP_CONTENT的情况下
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
//第一次协商。measureHierarchy()使用它最期望的宽度限制进行测量。
//这一宽度限制定义为一个系统资源。
//可以在frameworks/base/core/res/res/values/config.xml找到它的定义
final DisplayMetrics packageMetrics = res.getDisplayMetrics();
res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);
// 宽度限制被存放在baseSize中
int baseSize = 0;
if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
baseSize = (int)mTmpValue.getDimension(packageMetrics);
}
if (baseSize != 0 && desiredWindowWidth > baseSize) {
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
//第一次测量。调用performMeasure()进行测量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
//View树的测量结果可以通过mView的getmeasuredWidthAndState()方法获取。
//View树对这个测量结果不满意,则会在返回值中添加MEASURED_STATE_TOO_SMALL位
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
goodMeasure = true; // 控件树对测量结果满意,测量完成
} else {
//第二次协商。上次的测量结果表明View树认为measureHierarchy()给予的宽度太小,在此
//在此适当地放宽对宽度的限制,使用最大宽度与期望宽度的中间值作为宽度限制
baseSize = (baseSize+desiredWindowWidth)/2;
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
//第二次测量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
// 再次检查控件树是否满足此次测量
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
// 控件树对测量结果满意,测量完成
goodMeasure = true;
}
}
}
}
if (!goodMeasure) {
//最终测量。当View树对上述两次协商的结果都不满意时,measureHierarchy()放弃所有限制
//做最终测量。这一次将不再检查控件树是否满意了,因为即便其不满意,measurehierarchy()也没
//有更多的空间供其使用了
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
//如果测量结果与ViewRootImpl中当前的窗口尺寸不一致,则表明随后可能有必要进行窗口尺寸的调整
if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
windowSizeMayChange = true;
}
}
// 返回窗口尺寸是否可能需要发生变化
return windowSizeMayChange;
}
measureHierarchy()方法最终也是调用了performMeasure()方法对View树进行测量,只是多了协商测量的过程。
2.2 窗口布局阶段(relayoutWindow())
调用relayoutWindow()来请求WindowManagerService服务计算Activity窗口的大小以及过扫描区域边衬大小和可见区域边衬大小。计算完毕之后,Activity窗口的大小就会保存在成员变量mWinFrame中,而Activity窗口的内容区域边衬大小和可见区域边衬大小分别保存在ViewRoot类的成员变量mPendingOverscanInsets和mPendingVisibleInsets中。
这部分不在这细讲了,有耐心的可以把老罗的文章看完:Android窗口管理服务WindowManagerService计算Activity窗口大小的过程分析
2.3 测量过程(performMeasure())
WMS的布局结果已经确定了,不管是否满意都得开始终极布局过程了,下面介绍下measure:
measure是对View进程测量,确定各View的尺寸的过程,这个过程分View和ViewGroup两种情况来看,对于View,通过measure完成自身的测量就行了,而ViewGroup除了完成自身的测量外,还需要遍历去调用所有子view的measure方法,各个子view递归去执行这个过程。
那么先从performMeasure开始:
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
这里的mView是 ViewRootImpl setView传进来的rootView.
接下来我们看下View的measure()方法
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
//根据widthMeasureSpec和heightMeasureSpec计算key值,在下面用key值作为键,缓存我们测量得到的结果
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
…
//forceLayout 是通过上次的mPrivateFlags标记位来判断这次是否需要触发重绘
//View中有个forceLayout()方法可以设置mPrivateFlags.
// needsLayout 简单看就是spec发生了某些规则约束下的变化
if (forceLayout || needsLayout) {
// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
//在View真正进行测量之前,View还想进一步确认能不能从已有的缓存mMeasureCache中读取缓存过的测量结果 //如果是强制layout导致的测量,那么将cacheIndex设置为-1,即不从缓存中读取测量结果 //如果不是强制layout导致的测量,那么我们就用上面根据measureSpec计算出来的key值作为缓存索引cacheIndex,这时候有可能找到相应的值,找到就返回对应索引;也可能找不到,找不到就返回-1
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
//在缓存中找不到相应的值或者需要忽略缓存结果的时候,重新测量一次 //此处调用onMeasure方法,并把尺寸限 制条件widthMeasureSpec和heightMeasureSpec传入进去 //onMeasure方法中将会进行实际的测量工作,并把测量的结果保存到成员变量中
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
//如果运行到此处,那么表示当前的条件允许View从缓存成员变量mMeasureCache中读取测量过的结果
//用上面得到的cacheIndex从缓存mMeasureCache中取出值,不必在调用onMeasure方法进行测量了
long value = mMeasureCache.valueAt(cacheIndex);
//一旦我们从缓存中读到值,我们就可以调用setMeasuredDimensionRaw方法将当前测量的结果保存到成员变量中
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
...
}
...
}
...
}
先判断一下是否有必要进行测量操作,如果有,先看是否能在缓存mMeasureCache中找到上次的测量结果,如果找到了那直接从缓存中获取就可以了,如果找不到,那么乖乖地调用onMeasure()方法去完成实际的测量工作,并且将尺寸限制条件widthMeasureSpec和heightMeasureSpec传递给onMeasure()方法。
另外,measure()这个方法是final的,因此我们无法在子类中去重写这个方法,说明Android是不允许我们改变View的measure框架. 主要看onMeasure()方法,这里才是真正去测量并设置View大小的地方。
2.3.1 View的measure过程:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
setMeasuredDimension方法会设置View宽高的测量值,因此主要看下getDefaultSize方法:
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
很显然看出:
AT_MOST 和 EXACTLY两种情况返回的就是specSize。
UNSPECIFIED返回的是size, 即getSuggestedMinimumWidth 和 getSuggestedMinimumHeight的返回值。
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
mMinWidth 对应于android:minWidth属性指定的值。总结:如果View没有设置背景,那么返回mMinWidth的值,否则返回mMinWidth和背景最小宽度的最大值。
2.3.2 ViewgGroup的measure过程:
ViewGroup 继承自 View,我们知道View的 measure是 final方法,那这个方法是肯定会走的,但是具体实现是在onMeasure中,ViewGroup提供了几个方法来帮助ViewGroup的子类来实现onMeasure逻辑,包括:
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
仔细看其实最终还是让child去执行自己对于的measure,只是getChildMeasureSpec有差别,这里加上了margin 和 padding.
具体onMeasure的实现可以参考LinearLayout、FrameLayout、RelativeLayout等。
另外需要关注的是ViewGroup 的 getChildMeasureSpec方法,我们从上面代码中很明显看出,传入的Spec是父容器的measureSpec
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
很明显看出来,对于普通View来说,getChildMeasureSpec的逻辑是通过其父View提供的MeasureSpec参数得到specMode和specSize,然后根据计算出来的specMode以及子View的childDimension(layout_width或layout_height)来计算自身的measureSpec 。
measure总结:
MeasureSpec 由specMode和specSize组成:
DecorView, 其MeasureSpec由窗口尺寸和其自身LayoutParams共同决定。
普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定。View的measure方法是final的,不允许重载,View子类只能重载onMeasure来完成自己的测量逻辑。
ViewGroup类提供了measureChild,measureChild和measureChildWithMargins方法,以及getChildMeasureSpec方法 ,供具体实现ViewGroup的子类重写onMeasure的时候方便使用。
只要是ViewGroup的子类就必须要求LayoutParams继承子MarginLayoutParams,否则无法使用layout_margin参数。
使用View的getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值。
比较常用的方式:
view.post(runnable)
view.measure(0,0)之后 get
measure整体执行流程:
2.4 布局过程 (performLayout())
Layout的作用是ViewGroup用来确定子view的位置,当ViewGroup的位置被确定之后,它在onLayout中会遍历所有子view并调用其layout方法,在layout方法中onLayout又被调用。
先从performLayout看起:
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
...
//标记当前开始布局
mInLayout = true;
//mView就是DecorView
final View host = mView;
...
//DecorView请求布局 layout参数分别是 左 上 右 下
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
//标记布局结束
mInLayout = false;
...
}
跟踪代码进入View类的layout方法
public void layout(int l, int t, int r, int b) {
//判断是否需要重新测量
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
//保存上一次View的四个位置
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
//设置当前视图View的左,顶,右,底的位置,并且判断布局是否有改变
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
//如果布局有改变,条件成立,则视图View重新布局
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//调用onLayout,将具体布局逻辑留给子类实现
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList listenersCopy =
(ArrayList)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
首先关注下需要重新layout的条件:
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
其中setOpticalFrame内部也会调用setFrame,所以就看下setFrame好了:
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
...
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
// Remember our drawn bit
int drawn = mPrivateFlags & PFLAG_DRAWN;
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
// Invalidate our old position
invalidate(sizeChanged);
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
...
}
return changed;
}
通过setFrame方法设定View的四个顶点的位置,并更新本地值,同时判断顶点位置较之前是否有变化,并return是否有变化的boolean值,如果有变化还会执行invalidate(sizeChanged)。
然后,咱们再看看onLayout方法:
View中的onLayout是个空方法,可实现可不实现
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
ViewGroup中是个抽象方法,子类必须实现
@Override
protected abstract void onLayout(boolean changed,int l, int t, int r, int b);
下面以RelativeLayout为例,对onLayout具体实现做简单的分析:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// The layout has actually already been performed and the positions
// cached. Apply the cached values to the children.
final int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
if (child.getVisibility() != GONE) {//只有不为GONE的才会执行布局
RelativeLayout.LayoutParams st =
(RelativeLayout.LayoutParams) child.getLayoutParams();
child.layout(st.mLeft, st.mTop, st.mRight, st.mBottom);
}
}
}
遍历所有子view,并通过其LayoutParams 获取四个方向的位置值,将位置信息传入子view的layout方法进行布局。
layout总结:
Layout的作用是ViewGroup用来确定子view的位置, 是ViewGroup需要干的活,View不需要,所以View中是空方法,而ViewGroup中是抽象方法,但是View你也可以重写,大多数是利用这个生命周期阶段加写逻辑操作。
当我们的视图View在布局中使用 android:visibility=”gone” 属性时,是不占据屏幕空间的,因为在布局时ViewGroup会遍历每个子视图View,判断当前子视图View是否设置了 Visibility==GONE,如果设置了,当前子视图View就不会添加到父容器上,因此也就不占据屏幕空间。具体可以参考RelativeLayout的onLayout.
必须在View布局完之后调用getHeight( )和getWidth( )方法获取到的View的宽高才大于0.
layout的整体执行流程:
2.5 绘制过程 (performDraw())
Draw作用是将View绘制到屏幕上.过程相对比较简单。
draw是从performDraw开始
//ViewRootImpl
private void performDraw() {
...
draw(fullRedrawNeeded);
...
}
然后看ViewRootImpl的draw方法:
//ViewRootImpl
private void draw(boolean fullRedrawNeeded) {
Surface surface = mSurface;
...
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
...
}
再看ViewRootImpl的drawSoftware方法:
//ViewRootImpl
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
...
mView.draw(canvas);
...
}
最终在drawSoftware方法中,会走到View的draw并传入了canvas画布。这部分先不细说,之后的Surface部分会分析。
那么接着往下的话就是真正View绘制的部分了:
//View
public void draw(Canvas canvas) {
......
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1\. Draw the background
* 2\. If necessary, save the canvas' layers to prepare for fading
* 3\. Draw view's content
* 4\. Draw children
* 5\. If necessary, draw the fading edges and restore layers
* 6\. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
......
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
......
// Step 2, save the canvas' layers
......
if (drawTop) {
canvas.saveLayer(left, top, right, top + length, null, flags);
}
......
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
......
if (drawTop) {
matrix.setScale(1, fadeHeight * topFadeStrength);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, right, top + length, p);
}
......
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
......
}
从摘要可以看出,绘制过程分如下几步:
- 绘制背景 background.draw(canvas)
private void drawBackground(Canvas canvas) {
//获取xml中通过android:background属性或者代码中setBackgroundColor()、setBackgroundResource()等方法进行赋值的背景Drawable
final Drawable background = mBackground;
......
//根据layout过程确定的View位置来设置背景的绘制区域
if (mBackgroundSizeChanged) {
background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
rebuildOutline();
}
......
//调用Drawable的draw()方法来完成背景的绘制工作
background.draw(canvas);
......
}
- 绘制自己(onDraw)
View中onDraw是一个空方法,ViewGroup也没有重新实现。
protected void onDraw(Canvas canvas) {
}
- 绘制children(dispatchDraw)
View的dispatchDraw()方法是一个空方法,而且注释说明了如果View包含子类需要重写他。所以ViewGroup肯定重写了,来看看:
@Override
protected void dispatchDraw(Canvas canvas) {
......
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
......
for (int i = 0; i < childrenCount; i++) {
......
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
......
// Draw any disappearing views that have animations
if (mDisappearingChildren != null) {
......
for (int i = disappearingCount; i >= 0; i--) {
......
more |= drawChild(canvas, child, drawingTime);
}
}
......
}
可以看见,ViewGroup确实重写了View的dispatchDraw()方法,该方法内部会遍历每个子View,然后调用drawChild()方法,我们可以看下ViewGroup的drawChild方法,如下:
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
- 绘制装饰(onDrawScrollBars)
可以看见其实任何一个View都是有(水平垂直)滚动条的,只是一般情况下没让它显示而已。这部分不做详细分析了。
draw拓展点:
1)View的setWillNotDraw方法:
如果一个View不需要绘制任何内容,那么设置这个标记位为true后,系统会进行相应的优化,如果需要通过onDraw绘制内容,则需要设置为false。(默认View是false ViewGroup是true)这是个优化手段。
2)区分View动画和ViewGroup布局动画,前者指的是View自身的动画,可以通过setAnimation添加,后者是专门针对ViewGroup显示内部子视图时设置的动画,可以在xml布局文件中对ViewGroup设置layoutAnimation属性(譬如对LinearLayout设置子View在显示时出现逐行、随机、下等显示等不同动画效果)。
3)默认情况下子View的ViewGroup.drawChild绘制顺序和子View被添加的顺序一致,但是你也可以重载ViewGroup.getChildDrawingOrder()方法提供不同顺序。
draw整体执行流程:
三、forceLayout 、invalidate 、requestLayout简述
在之前分析的绘制流程中,我们或多或少都见过这三个方法,他们到底是干什么的,下面做下简单说明:
View#forceLayout( )
public void forceLayout() {
if (mMeasureCache != null) mMeasureCache.clear();
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
}
官方描述:强制此视图在下一次布局传递期间进行布局。此方法不调用父类的requestLayout()或forceLayout()。
每个View都有个成员变量:mPrivateFlags,在不同的绘制执行路径会对它赋值。在measure方法内部forceLayout用来判断是否执行onMeasure:
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
View#invalidate( ) 和 View#postInvalidate( )
invalidate和postInvalidate:都是用来重绘View,区别就是invalidate只能在主线程中调用,postInvalidate可以在子线程中调用.
View#requestLayout
requestLayout: 当前view及其以上的viewGroup部分都重新走ViewRootImpl 重新绘制 ,分别重新onMeasure onLayout onDraw ,其中onDraw比较特殊,有内容变化才会触发。
最后一张图总结下invalidate/postInvalidate 和 requestLayout
参考:
https://blog.csdn.net/yanbober/article/details/46128379
https://blog.csdn.net/feiduclear_up/article/details/46772477
https://www.jianshu.com/p/4a68f9dc8f7c
https://www.jianshu.com/p/a65861e946cb
《Android开发艺术探索》