View绘制的三大方法分析
在上一篇文章《View的绘制流程之三:View的绘制流程源码分析》中,我们已经知道了 ViewRootImpl调用 performTraversals() 方法绘制 View的整个流程,现在我们对其中的绘制 View的三大方法:performMeasure() 方法、performLayout() 方法、performDraw() 方法进行分析
一、performMeasure()方法
1、知识储备
performMeasure() 方法的流程图:
2、流程分析
Step 1:
我们看到 ViewRootImpl 的 performMeasure() 方法
★ ViewRootImpl # 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 就是我们的 DecorView ,由于 measure() 方法是一个 final 方法,所以 DecorView 、FrameLayout 、ViewGroup 都无法重写,所以我们直接看到 View 的 measure() 方法
Step 2:
继续看到 View 的 measure() 方法
★ View # measure()
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
....
if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {
....
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// 这个方法会由子View进行重写,用于测量自身的宽高
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}....
....
}
....
}
View 的 measure() 方法会调用其 onMeasure() 方法,View 一般都会重写这个方法,在这个方法中加入自己的测量逻辑,所以我们现在直接看到 DecorView 的 onMeasure() 方法
★ DecorView # onMeasure()
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 1、获取屏幕的宽高
final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
// 通过屏幕宽高判断当前手机是横屏还是竖屏,isPortrait为true就是竖屏
final boolean isPortrait = metrics.widthPixels < metrics.heightPixels;
// 2、会根据宽高的模式是AT_MOST还会对其进行额外的处理
final int widthMode = getMode(widthMeasureSpec);
final int heightMode = getMode(heightMeasureSpec);
boolean fixedWidth = false;
if (widthMode == AT_MOST) {
....
}
if (heightMode == AT_MOST) {
....
}
....
// 3、DecorView完成了自己的测量逻辑后就直接交给父类FrameLayout遍历子View进行测量
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 4、因为已经在前面对宽度进行了测量,所以这里可以获取到测量后的宽度
// 下面会对宽度进行一些适配
int width = getMeasuredWidth();
boolean measure = false;
widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, EXACTLY);
if (!fixedWidth && widthMode == AT_MOST) {
....
}
// TODO: Support height?
if (measure) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
首先,获取屏幕的宽高,对于竖屏和横屏,DecorView 有不一样的处理机制,所以要进行判断
然后,如果宽高的测量模式是 AT_MOST,还会对 DecorView进行一些额外的处理
接着,当 DecorView 完成了自己的测量逻辑后就会把测量流程交给父类 FrameLayout,让其遍历测量子View 的宽高
最后,当完成了子View 的测量后,DecorView 会对测量好的宽度进行一些适配
Step 3:
那么现在我们看到 FrameLayout ,看一下它是怎么测量子View的
★ FrameLayout # onMeasure()
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
// 1、如果父布局的宽高有一个不是EXACTLY模式(也就是大小是MATCH_PARENT),那么会对子View中
// 同样有 MATCH_PARENT的宽或高进行重新测量
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
mMatchParentChildren.clear();
....
// 2、遍历所有子View,也就是说这里会对 Activity的Layout布局进行测量
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
// 3、只要View不是GONE都会对该View进行测量
if (mMeasureAllChildren || child.getVisibility() != GONE) {
// 4、测量子View的宽高
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 5、统计所有子View所占据的宽高
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
// 6、判断是否需要添加宽或高为 MATCH_PARENT的子View到列表中,然后稍后对这些View重新测量
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
....
// 7、测量完所有子View的宽高后就设置父布局的宽高
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
// 8、对宽或高为 MATCH_PARENT的子View重新测量一遍
count = mMatchParentChildren.size();
if (count > 1) {
for (int i = 0; i < count; i++) {
final View child = mMatchParentChildren.get(i);
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
....
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
FrameLayout 的测量流程如下:
第一步:如果父布局的宽高有一个不是 EXACTLY 模式(也就是大小为 MATCH_PARENT),那么就设置 measureMatchParentChildren 为true,作用是在完成测量子 后会对宽或高同样是 MATCH_PARENT 的 View 重新进行测量;这就意味着一个 View有可能会测量两次以上
第二步:遍历测量所有子 View
第三步:判断子View是否为 GONE,如果是那么就不会对 View 进行测量,除非通过 setMeasureAllChildren() 方法设置 mMeasureAllChildren 为true, mMeasureAllChildren 默认为 false,这样即使 View 是 GONE也会对其进行测量
第四步:通过 measureChildWithMargins() 方法测量子View的宽高
第五步:统计所有子View的宽高,maxWidth 和 maxHeight 最后会用于设置 FrameLayout (也就是 DecorView)的宽高
第六步:判断 measureMatchParentChildren 是否为true,如果是就将宽或高为 MATCH_PARENT 的子View加入到集合,等待重新测量
第七步:测量完所有的子View就通过 setMeasuredDimension() 方法设置 FrameLayout (也就是 DecorView)的宽高
第八步:对宽或高为 MATCH_PARENT 的子View重新测量一遍
小结:
- 如果View的宽或高是MATCH_PARENT ,那么这个View会测量至少两次
- View 是 GONE 是不会对其进行测量,除非通过在该View的父布局中使用 setMeasureAllChildren(true) 方法
Step 4:
现在我们继续看到 ViewGroup 的 measureChildWithMargins() 方法
★ ViewGroup # measureChildWithMargins()
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// 第1个参数:父布局的宽度测量规格
// 第2个参数:View的外面的padding值,包括父布局的padding值、子View的margin值、已经使用了的宽度值
// 第3个参数:子View的宽度
// 该方法用于根据以上参数获取子View的宽度测量规格
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);
// 调用子View进行测量宽高
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
首先,会调用 getChildMeasureSpec() 方法计算出子View宽高的测量规格
然后,调用子View的 measure() 方法完成最后的测量工作(子View:如我们 Activity的布局文件的根布局,该方法就是根布局的 measure() 方法)
Step 5:
最后,我们看到 ViewGroup 的 getChildMeasureSpec() 方法,这个方法会根据父布局的宽高测量规格为子View创建不同的 MeasureSpec
★ ViewGroup # getChildMeasureSpec()
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;
// 根据父布局的测量模式指定子View的宽高
switch (specMode) {
case MeasureSpec.EXACTLY:
....
break;
case MeasureSpec.AT_MOST:
....
break;
case MeasureSpec.UNSPECIFIED:
....
break;
}
// 最后生成一个宽度或高度的测量规格
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
3、总结
performMeasure() 方法的绘制流程如下:
ViewRootImpl # performMeasure() -->
View # measure() -->
PhoneWindow.DecorView # onMeasure() -->
FrameLayout # onMeasure() -->
ViewGroup # measureChildWidthMargins() -->
┖→ ViewGroup # getChildMeasureSpec()
Activity布局中的根布局 # measure()
二、performLayout()方法
1、知识储备
第一:流程图
performLayout() 方法的流程图:
第二:问题解答
View 的 layout()方法和 onLayout()方法,这个两个方法有什么区别???
layout() 方法是 View里面的方法,它会对 View的大小和位置进行检测,同时调用 onLayout()方法将 View大小和位置是否发生变化、View的左上右下四个位置传递给View,所以一般来说我们自定义 View都是重写 onLayout()方法,而不是直接重写 layout() 方法
2、流程分析
Step 1:
我们看到 ViewRootImpl 的 performLayout() 方法
★ ViewRootImpl # performLayout()
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
....
try {
// 调用view的Layout()方法进行摆放布局
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
....
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
mInLayout = false;
}
由于已经对 host 进行了测量,所以这里可以直接获取测量的宽高,因为我们分析的是 Activity 布局的加载流程,所以这里的 host 是一个 DecorView,但是 DecorView 以及其父类 FrameLayout 都没有重写这个方法,所以我们直接看到 View 的 layout() 方法
Step 2:
看到 View 的 layout() 方法
★ View # layout()
@SuppressWarnings({"unchecked"})
public void layout(int l, int t, int r, int b) {
.....
// 如果View的大小或者位置发生改变,那么changed就为true
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
// 调用View子类的onLayout()方法,如果View继承了ViewGroup,那么就会要求重写这个方法
// 对其子View进行摆放,所以DecorView会重写这个方法
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>) 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);
}
}
}
....
}
onLayout()方法在 View和 ViewGroup中的区别:
(1)对于单一View :一般都不会重写 onLayout()方法,因为不会对 View的位置进行更改,除非是要在这个方法完成某些操作,如在该方法中获取 View测量的宽高
(2)对于ViewGroup :onLayout()方法在 ViewGroup被重定义为抽象方法,所以子类必须要实现该方法,完成对子View位置的摆放
由于我们分析的是 Activity布局的加载流程,所以当前的 View就是 DecorView,所以我们看到 DecorView的 onLayout()方法中
Step 3:
看到 DecorView的 onLayout()方法
★ PhoneWindow.DecorView # onLayout()
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
getOutsets(mOutsets);
if (mOutsets.left > 0) {
offsetLeftAndRight(-mOutsets.left);
}
if (mOutsets.top > 0) {
offsetTopAndBottom(-mOutsets.top);
}
}
★ FrameLayout # onLayout()
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
★ FrameLayout # layoutChildren()
void layoutChildren(int left, int top, int right, int bottom,
boolean forceLeftGravity) {
final int count = getChildCount();
....
// 遍历子View,对子View进行摆放
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
// 只要子View不是GONE对会进行摆放,也就是说当我们设置了View为GONE,有可能
// 就会对布局进行重新摆放,除去设置为GONE的View
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
int childLeft;
int childTop;
....
// 下面是计算摆放位置的left和top
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
....
}
switch (verticalGravity) {
....
}
// 最后调用子View的layout()方法,该子View有可能是一个View或者一个ViewGroup,所以
// 它又会重新走一遍layout()方法的流程,不断递归下去,直到子View不是ViewGroup
// 现在分析的Activity的布局加载,所以这个child应该也是我们Activity布局文件中的根布局
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
可以看到 FrameLayout会遍历调用子 View的 layout()方法对子 View进行摆放,也就是说这里会调用到 Activity布局中的根布局的 layout()方法,但是一般来说,无论是单个 View或者ViewGroup都不会重写 layout()方法的,而是将 layout()方法交个 View进行处理,然后让 View调用单个 View或者ViewGroup的 onLayout()方法,所以在这之后就会调用 Activity布局中根布局 onLayout()方法,至此,performLayout() 方法分析完毕
3、总结
performLayout() 方法的调用顺序:
ViewRootImpl # performLayout()
View # layout()
DecorView # onLayout()
FrameLayout # onLayout()
FrameLayout # layoutChildren()
Activity布局中的根布局 # layout()
三、performDarw()方法
1、知识储备
第一:流程图
performDraw() 方法的流程图:
第二:问题解答
View 的 draw()方法和 onDraw()方法,这个两个方法有什么区别???
draw()方法:
- 对于View:会绘制背景、内容、装饰(如滚动条);
- 对于ViewGroup:会绘制背景、内容、分发绘制事件给子View、装饰(如滚动条);
onDraw()方法:
- 这个方法只是完成 draw()方法功能中内容绘制这一块的工作,实现View自身内容的绘制(不包含子View)
2、流程分析
Step 1:
我们看到 performDraw() 方法
★ ViewRootImpl # performDraw()
private void performDraw() {
....
// 标记正在绘制View
mIsDrawing = true;
try {
// 调用 ViewRootImpl的 draw()方法
draw(fullRedrawNeeded);
} finally {
mIsDrawing = false;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
....
if (mReportNextDraw) {
....
try {
mWindowSession.finishDrawing(mWindow);
} catch (RemoteException e) {
}
}
}
我们继续看到 ViewRootImpl的 draw() 方法
★ ViewRootImp # draw()
private void draw(boolean fullRedrawNeeded) {
// 获取 ViewRootImpl中的 Surface对象
Surface surface = mSurface;
if (!surface.isValid()) {
return;
}
....
// 通知注册了观察View绘制的监听器,将要开始绘制view
mAttachInfo.mTreeObserver.dispatchOnDraw();
....
if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) {
....
} else {
....
// 调用 drawSoftware()方法绘制View
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
}
}
....
}
可以看到,View的绘制跟测量、摆放的流程有些不一样
首先,会获取 ViewRootImpl中的 Surface对象,这个对象用于获取 canvas画布
然后,会通知注册了View draw过程的监听器,传递View开始绘制这一事件
最后,会调用 drawSoftware()方法开始绘制View
Step 2:
接着,我们看到 drawSoftware()方法
★ ViewRootImpl # drawSoftware()
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
final Canvas canvas;
try {
....
// 1、从suface对象中获取canvas对象,同时锁住画布,保证canvas对象的线程安全
canvas = mSurface.lockCanvas(dirty);
....
// 2、指定canvas布局支持的bitmap的密度
canvas.setDensity(mDensity);
} ....
try {
....
try {
....
// 3、设置屏幕密度
canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
attachInfo.mSetIgnoreDirtyState = false;
// 4、如果子View没有重写该方法的话就会调用View的draw()方法
// 但是 DecorView对该方法进行了重写
mView.draw(canvas);
drawAccessibilityFocusedDrawableIfNeeded(canvas);
} ....
} finally {
try {
// 5、解锁画布
surface.unlockCanvasAndPost(canvas);
} ....
}
return true;
}
该方法会完成以下操作:
(1)从 Surface中获取canvas画布,并锁定该画布,保证画布的线程安全
(2)指定 canvas画布支持的 bitmap位图的密度
(3)指定屏幕密度
(4)调用 View的draw()方法绘制View
(5)解锁canvas画布
现在,一般来说我们都不会重写View的draw()方法,但是 DecorView有些特殊,它重写了draw()方法,所以接下来我们看到该方法
Step 3:
我们看到 DecorView的 draw()方法中
★ PhoneWindow.DecorView # draw()
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
// 如果有菜单背景那么就绘制菜单背景
if (mMenuBackground != null) {
mMenuBackground.draw(canvas);
}
}
在 DecorView的 draw()方法中会判断是否需要绘制菜单背景;由于其父类没有重写 draw()方法,所以我们看到 View的 draw()方法
★ View # draw()
@CallSuper
public void draw(Canvas canvas) {
....
// 1、如果有需要就会绘制背景,所以优化View可以尝试尽量避免给View设置不必要的背景
if (!dirtyOpaque) {
drawBackground(canvas);
}
// 2、如果有必要,保存画布的图层以准备渐变
int paddingLeft = mPaddingLeft;
final boolean offsetRequired = isPaddingOffsetRequired();
.....
if (solidColor == 0) {
final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;
if (drawTop) {
canvas.saveLayer(left, top, right, top + length, null, flags);
}
if (drawBottom) {
canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
}
if (drawLeft) {
canvas.saveLayer(left, top, left + length, bottom, null, flags);
}
if (drawRight) {
canvas.saveLayer(right - length, top, right, bottom, null, flags);
}
} else {
scrollabilityCache.setFadeColor(solidColor);
}
// 3、调用onDraw()方法开始绘制内容,这个方法会由子View进行重写
if (!dirtyOpaque) onDraw(canvas);
// 4、分发draw事件给所有子View,然后绘制子View
dispatchDraw(canvas);
// 5、绘制渐变效果并恢复画布的图层
final Paint p = scrollabilityCache.paint;
final Paint p = scrollabilityCache.paint;
final Matrix matrix = scrollabilityCache.matrix;
final Shader fade = scrollabilityCache.shader;
if (drawTop) {
....
}
if (drawBottom) {
....
}
if (drawLeft) {
....
}
if (drawRight) {
....
}
canvas.restoreToCount(saveCount);
....
// 6、绘制装饰,如:前景、滚动条
onDrawForeground(canvas);
}
View的 draw()方法会完成以下操作:
(1)判断是否需要绘制背景,如果需要就先绘制背景,所以如果要优化View可以尝试避免给View设置不必要的背景
(2)如果有必要,保存画布的图层以准备渐变
(3)调用 onDraw()方法开始绘制View的内容,这个方法由子View进行重写,View中默认为空实现
(4)调用方法分发绘制事件给子View,该方法会由ViewGroup方法进行重写,在View中默认为空实现
(5)绘制渐变效果并恢复画布的图层
(6)绘制一些装饰,如前景、滚动条
draw()方法中有两个比较重要的方法 onDraw()方 和 dispatchDraw();由于 DecorView是实现了 onDraw()方法,我们先看到该方法
★ DecorView # onDraw()
@Override
public void onDraw(Canvas c) {
super.onDraw(c);
mBackgroundFallback.draw(mContentRoot, c, mContentParent);
}
其父类都没有重写该方法,我们继续看到 View的 onDraw()方法,这是一个空实现
★ View # onDraw()
protected void onDraw(Canvas canvas) {
}
Step 4:
继续看到 dispatchDraw()方法,由于 DecorView、FrameLayout都没有重写该方法,所以我们直接看到ViewGroup中
★ ViewGroup # dispatchDraw()
@Override
protected void dispatchDraw(Canvas canvas) {
....
// 遍历所有子View,调用 drawChild()方法绘制子View
for (int i = 0; i < childrenCount; i++) {
while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
// 获取子View
final View transientChild = mTransientViews.get(transientIndex);
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
// 绘制子View
more |= drawChild(canvas, transientChild, drawingTime);
}
transientIndex++;
if (transientIndex >= transientCount) {
transientIndex = -1;
}
}
....
}
....
}
继续看到 drawChild()方法
★ ViewGroup # drawChild()
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
可见,这里会调用子View的 draw()方法,也就是说这里会调用 Activity布局中根布局的 draw()方法,但是一般View都不会重写该方法,所以接着会调用 Activity布局中根布局的 onDraw()方法
3、总结
performDraw() 方法的绘制流程如下:
ViewRootImpl # performDraw() -->
ViewRootImpl # draw() -->
ViewRootImpl # drawSoftware() -->
DecorView # draw() -->
View # draw() -->
drawBackground()
onDraw()
dispatchDraw() -->
onDrawForeground()
ViewGroup # dispatchDraw() -->
ViewGroup # drawChild() -->
ViewGroup # draw(3个参数) -->
View # draw() -->
Activity布局中的根布局 # onDraw()