View体系(一)——体系概览及绘制流程
(注:源代码为android-8.1)
0. 前言
本文主要分析Android的View体系及绘制流程。
1. View体系
在一个窗口里的所有View
会被统一管理在一个树状结构内,如图所示:
继承自ViewGroup
的控件可以拥有子节点,而继承自View
的控件一般只能作为叶节点。在实现自定义控件的时候需要注意到这一点。下图是部分系统自带控件的关系图。
2. 代码解析
ViewGroup
添加一个View
的具体执行内容。
(frameworks/base/core/java/android/view/ViewGroup.java)
public void addView(View child, int index, LayoutParams params) {
...
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
requestLayout();
invalidate(true);
addViewInner(child, index, params, false);
}
分别看两种重绘方法:requestLayout
方式和invalidate
方式。
2.1 requestLayout方式
(frameworks/base/core/java/android/view/View.java)
@CallSuper
public void requestLayout() {
...
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
ViewRootImpl viewRoot = getViewRootImpl();
if (viewRoot != null && viewRoot.isInLayout()) {
if (!viewRoot.requestLayoutDuringLayout(this)) {
return;
}
}
mAttachInfo.mViewRequestingLayout = this;
}
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
if (mParent != null && !mParent.isLayoutRequested()) {
// 调用父节点的requestLayout方法
mParent.requestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}
可以看出requestLayout
方法所做的就是一路向上调用父节点的requestLayout
方法。那么很明显,最终的执行内容要看根节点所重写的requestLayout
方法内做了什么特殊的操作。文章《Android源码阅读分析:从Activity开始(二)——加载布局》中分析过,Activity
加载布局,视图树的根节点是一个DecorView
。但是我们查看DecorView
的源码发现,它根本就没有重写requestLayout
方法,这说明DecorView
仍然不是处理requestLayout
的最终根节点。那么我们就需要找到DecorView
的mParent
是什么。
2.1.1 寻找根节点
DecorView
继承自View
,而View
的mParent
则是在assignParent
方法中进行设置的。经过查找,调用了View.assignParent
方法的地方不多,唯一符合我们查找条件的则是在ViewRootImpl
类的setView
方法调用的。
(frameworks/base/core/java/android/view/ViewRootImpl.java)
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
...
view.assignParent(this);
...
}
ViewRootImpl.setView
方法在WindowManagerGlobal
的addView
方法内被调用。
(frameworks/base/core/java/android/view/WindowManagerGlobal.java)
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
...
root.setView(view, wparams, panelParentView);
...
}
继续向上跟踪。
(frameworks/base/core/java/android/view/WindowManagerImpl.java)
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
...
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
...
}
这个方法在ActivityThread
类的handleResumeActivity
方法中被调用了。
(frameworks/base/core/java/android/app/ActivityThread.java)
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
...
View decor = r.window.getDecorView();
ViewManager wm = a.getWindowManager();
...
wm.addView(decor, l);
...
}
所以,DecorView
的mParent
其实是一个ViewRootImpl
类的对象。也就是说,最终处理requestLayout
的根节点。
(frameworks/base/core/java/android/view/ViewRootImpl.java)
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
scheduleTraversals
方法经过异步调用,最后会执行ViewRootImpl
的performTraversals
方法。
(frameworks/base/core/java/android/view/ViewRootImpl.java)
private void performTraversals() {
...
boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
if (layoutRequested) {
...
windowSizeMayChange |= measureHierarchy(host, lp, res, desiredWindowWidth, desiredWindowHeight);
}
...
if (mFirst || windowShouldResize || insetsChanged || viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
...
relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
...
if (!mStopped || mReportNextDraw) {
if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
|| mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
updatedConfiguration) {
...
// 测量host需求的尺寸
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
}
}
}
...
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
...
if (didLayout) {
performLayout(lp, mWidth, mHeight);
...
}
...
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
if (!cancelDraw && !newSurface) {
...
performDraw();
}
...
}
performTraversals
方法会依次调用performMeasure
、performLayout
和performDraw
方法。
(frameworks/base/core/java/android/view/ViewRootImpl.java)
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
...
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
...
}
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {
...
final View host = mView;
...
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
...
}
private void performDraw() {
...
draw(fullRedrawNeeded);
...
}
private void draw(boolean fullRedrawNeeded) {
...
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
...
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty) {
...
mView.draw(canvas);
...
}
这三个方法内的代码很多,这里只将最关键的代码提出来。可以看到,ViewRootImpl
的这个三个方法,分别调用了View
的measure
、layout
和draw
方法。
2.1.2 measure相关
(frameworks/base/core/java/android/view/View.java)
// 该方法用于计算视图大小,父节点提供宽高参数的限制条件
// 实际的测量工作会在onMeasure方法中执行
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
// 调整widthMeasureSpec和heightMeasureSpec参数
...
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
...
final boolean needsLayout = specChanged && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
if (forceLayout || needsLayout) {
...
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
}
// View的子类需要重写该方法来计算其内容的精确有效的测量值
// 要注意的是,重写该方法需要调用setMeasuredDimension(int, int)方法来记录测量的宽高
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
measure
方法用于计算视图大小,主要作用是提供父节点的限制条件,其内部会调用onMeasure
方法,而onMeasure
方法是实际进行测量的方法。View
的子类(包括TextView
、FrameLayout
等framework自带的控件以及我们自定义的控件)都需要重写onMeasure
方法,来测量精确有效的宽高值。
这里我们分别用继承自View
和ViewGroup
的两个典型子类来做为示例:TextView
和FrameLayout
。
(frameworks/base/core/java/android/widget/TextView.java)
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
...
// 计算宽度
if (widthMode == MeasureSpec.EXACTLY) {
// 父节点已经指定了该控件大小,直接使用
width = widthSize;
} else {
...
// 根据参数设置计算TextView的宽度
...
}
...
// 高度的计算与宽度计算类似
// 对于TextView来说,高度计算简单很多
...
// 记录测量的宽高
setMeasuredDimension(width, height);
}
(frameworks/base/core/java/android/widget/FrameLayout.java)
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
...
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
// 计算所有子节点的尺寸
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
// 该方法会调用子节点的measure方法
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
...
}
}
// 根据参数调整宽高参数
...
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT));
...
}
对于继承自View
的子类只需要对自身进行测量,而对于继承自ViewGroup
的子类来说,则需要先测量所有子节点,然后在根据子节点的测量值和自身参数设置来测量自身。在实现自定义控件时,需要注意到这一点。
2.1.3 layout相关
(frameworks/base/core/java/android/view/View.java)
@SuppressWarnings({"unchecked"})
public void layout(int l, int t, int r, int b) {
...
onLayout(changed, l, t, r, b);
...
}
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
layout
方法用于给控件极其子节点分配位置和尺寸。自定义控件或者系统自带控件都会重写onLayout
方法来实际,如果是继承自ViewGroup
类的控件,则还需要在onLayout
方法内调用子节点的layout
方法。
2.1.4 draw相关
(frameworks/base/core/java/android/view/View.java)
@CallSuper
public void draw(Canvas canvas) {
...
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
...
if (!dirtyOpaque) {
drawBackground(canvas);
}
...
if (!dirtyOpaque) onDraw(canvas);
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
if (!dirtyOpaque) onDraw(canvas);
dispatchDraw(canvas);
...
}
...
}
draw
方法用于在给定的Canvas
上绘制控件。在该方法调用时,该控件的layout
相关方法必须完全执行完毕。draw
方法绘制控件,先通过onDraw
方法进行实际绘制,再通过调用dispatchDraw
方法绘制子节点。我们实现自定义控件时,需要重写onDraw
方法。View
的dispatchDraw
方法本身为空,而在ViewGroup
类中重写了dispatchDraw
方法。
(frameworks/base/core/java/android/view/ViewGroup.java)
@Override
protected void dispatchDraw(Canvas canvas) {
...
for (int i = 0; i < childrenCount; i++) {
...
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
...
}
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
2.1.5 调用流程
通过requestLayout
方式绘制,可以总结为一下流程。
2.2 invalidate方式
invalidate
方法也是常用的重绘界面的方法,在ViewGroup
的addView
方法内,调用requestLayout
方法后也调用了invalidate
方法。
(frameworks/base/core/java/android/view/View.java)
public void invalidate(boolean invalidateCache) {
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache, boolean fullInvalidate) {
...
if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
|| (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
|| (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
|| (fullInvalidate && isOpaque() != mLastIsOpaque)) {
...
// 将损毁的矩形发送到父节点
final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
damage.set(l, t, r, b);
p.invalidateChild(this, damage);
}
...
}
}
这里会调用ViewParent
的invalidateChild
方法。ViewGroup
实现了ViewParent
接口。来看一下ViewGroup
的invalidateChild
方法。
(frameworks/base/core/java/android/view/ViewGroup.java)
@Override
public final void invalidateChild(View child, final Rect dirty) {
...
ViewParent parent = this;
...
do {
...
parent = parent.invalidateChildInParent(location, dirty);
...
} while (parent != null);
...
}
ViewGroup
的invalidateChild
方法内会调用其父节点的invalidateChildInParent
方法。而ViewGroup
的invalidateChildInParent
方法内只是对一些参数进行设置调整。那么按照惯例,查看ViewRootImpl
的invalidateChildInParent
重写方法。
(frameworks/base/core/java/android/view/ViewRootImpl.java)
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
...
if (dirty == null) {
invalidate();
return null;
}
...
invalidateRectOnScreen(dirty);
return null;
}
这里invalidate
和invalidateRectOnScreen
最后都会调用scheduleTraversals
方法。根据上文的代码分析,这里最终会调用performTraversals
方法。但是这里performTraversals
方法只会调用performDraw
方法,因为其mLayoutRequested
变量此时为false
,不会进入调用performLayout
的逻辑。
那么剩下的调用逻辑则和requestLayout
方式的一样了。
3. 总结
本文主要对控件体系进行概览,以及绘制流程的大致解析。后续文章中,会对控件体系进行详细的分析。