前言
在Android知识体系中,View扮演了很重要的角色;它是Android在视觉上的呈现,Android本身提供了一套GUI库,但是我们的需求不止于系统自带的GUI,因此我们还需要自定义View。而自定义View过程中我们势必要对View的底层工作原理有所了解。这篇文章就记录一下View的测量、布局、绘制三大流程。
1.一些必要的基本概念
1.1 ViewRoot和DecorView
ViewRoot
对应于ViewRootImpl类,连接WindowManager和DecorView的纽带;View的三大流程基于RootView来完成的;View的绘制流程是从ViewRoot的performTraversals
方法开始的。
DecorView
顶级View,内部一般为一个竖直方向的LinearLayout,分为2部分:titlebar以及android.R.id.content的FrameLayout。
结构图如下:
在这里插入图片描述1.2 MeasureSpec
字面意思为“测量规格”;
它代表一个32位int值,高2位代表SpecMode(测量模式),低30位代表SpecSize(某种测量模式下的规格大小)。
SpecMode一共有3类:
-
EXACTLY:match_parent和具体数值,父容器已经检测出View的精确大小
-
AT_MOST:父容器制定一个可用大小,View的大小不能大于它
-
UNSPECIFIED: 父容器不对View有任何限制
一般来说,View的MeasureSpec由本身的LayoutParams与父容器的MeasureSpec共同决定。DecorView的MeasureSpec由LayoutParams决定。父容器通过measure传递MeasureSpec给子View.
引用刚哥的《Android 开发艺术探索》的图:这张图涵括了View Measure中最核心的过程。
在这里插入图片描述它在View的底层原理中起到的作用下面的篇幅在做具体的介绍。源码如下:所在包import android.view.View.MeasureSpec;
/**
* A MeasureSpec encapsulates the layout requirements passed from parent to child.
* Each MeasureSpec represents a requirement for either the width or the height.
* A MeasureSpec is comprised of a size and a mode. There are three possible
* modes:
* <dl>
* <dt>UNSPECIFIED</dt>
* <dd>
* The parent has not imposed any constraint on the child. It can be whatever size
* it wants.
* </dd>
*
* <dt>EXACTLY</dt>
* <dd>
* The parent has determined an exact size for the child. The child is going to be
* given those bounds regardless of how big it wants to be.
* </dd>
*
* <dt>AT_MOST</dt>
* <dd>
* The child can be as large as it wants up to the specified size.
* </dd>
* </dl>
*
* MeasureSpecs are implemented as ints to reduce object allocation. This class
* is provided to pack and unpack the <size, mode> tuple into the int.
*/
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/** @hide */
@IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
@Retention(RetentionPolicy.SOURCE)
public @interface MeasureSpecMode {}
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 makeSafeMeasureSpec(int size, int mode) {
if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
return 0;
}
return makeMeasureSpec(size, mode);
}
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
static int adjust(int measureSpec, int delta) {
final int mode = getMode(measureSpec);
int size = getSize(measureSpec);
if (mode == UNSPECIFIED) {
// No need to adjust size for UNSPECIFIED mode.
return makeMeasureSpec(size, UNSPECIFIED);
}
size += delta;
if (size < 0) {
Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
") spec: " + toString(measureSpec) + " delta: " + delta);
size = 0;
}
return makeMeasureSpec(size, mode);
}
public static String toString(int measureSpec) {
int mode = getMode(measureSpec);
int size = getSize(measureSpec);
StringBuilder sb = new StringBuilder("MeasureSpec: ");
if (mode == UNSPECIFIED)
sb.append("UNSPECIFIED ");
else if (mode == EXACTLY)
sb.append("EXACTLY ");
else if (mode == AT_MOST)
sb.append("AT_MOST ");
else
sb.append(mode).append(" ");
sb.append(size);
return sb.toString();
}
}
2.View的工作原理
上面介绍了View底层工作原理所必备的一些知识后,现在重点介绍View的工作原理:
View三大流程的入口(通过Log可以得知):是从onResume()之后开始的。跟踪ActivityThread的源码,在handleResumeActivity()
中开始.
2.1 handleResumeActivity()
@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
......
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (r.mPreserveWindow) {
a.mWindowAdded = true;
r.mPreserveWindow = false;
// Normally the ViewRoot sets up callbacks with the Activity
// in addView->ViewRootImpl#setView. If we are instead reusing
// the decor view we have to notify the view root that the
// callbacks may have changed.
ViewRootImpl impl = decor.getViewRootImpl();
if (impl != null) {
impl.notifyChildRebuilt();
}
}
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
wm.addView(decor, l);
} else {
// The activity will get a callback for this {@link LayoutParams} change
// earlier. However, at that time the decor will not be set (this is set
// in this method), so no action will be taken. This call ensures the
// callback occurs with the decor set.
a.onWindowAttributesChanged(l);
}
}
......
}
,注意注释 // in addView->ViewRootImpl#setView. If we are instead reusing
最终调用ViewRootImpl.setView()
;
2.2 ViewRootImpl.setView()
/**
* We have one child
*/
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
......
requestLayout();
......
}
进入requestLayout()
方法。
2.3 ViewRootImpl.requestLayout()
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
2.4 ViewRootImpl.performTraversals()
@Override
public void performTraversals() {
......
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
......
performLayout(lp, mWidth, mHeight);
......
performDraw();
......
}
核心方法:View的工作主要流程就在此方法中。
归纳上方的流程为下图:
在这里插入图片描述-
measure 测量,决定了测量后的宽高
-
layout 布局,确定四个定点坐标以及View的实际宽高
-
draw 绘制,决定View的显示
依次调用performMeasure、performLayout、performDraw方法,完成顶级View的measure、layout、draw;而在measure、layout、draw又会调用oMeasure、onLayout、onDraw对其子元素重复递归上过程,直到view不为ViewGroup时。
3.View的measure过程
- View的measure
具体流程:
在这里插入图片描述一系列的方法会设置View宽高的测量值。其中getSuggestedMinimumWidth()
会根据是存在背景来返回最小宽度。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
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;
}
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
从getDefault方法中可得结论:
直接继承View的自定义控件需重写onMeasure方法并设置wrap_content时的自身大小,否则使用wrap_content就相当于使用match_content。
- ViewGroup的measure
而ViewGroup需要完成本身的measure过程外,还要遍历所有子元素的measure方法,递归执行。本身未重写onMeasure()方法,提供measureChildren()方法遍历测量View树。具体继承自它的类重写onMeasure()对子View进行测量。
首先要确定DecorView的MeasureSpec用来传递给子View进行生成MeasureSpec。DecorView通过getRootMeasureSpec
确定本身测量模式以及大小。其中windowSize就是屏幕的宽高。
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
其次,通过measureChildren(int widthMeasureSpec, int heightMeasureSpec)
遍历测量所有的View树。
// 遍历测量View树
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
// 根据LayoutParams 以及 父View传递过来的MeasureSpec 创建自身的测量模式/规格
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
// 自身布局参数
final LayoutParams lp = child.getLayoutParams();
// 获取当前所需测量child的宽度的测量模式
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
// 获取当前所需测量child的高度的测量模式
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
// 最终调用measure,判断是View还是ViewGroup
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
其中最重要的源码部分如下:此部分即可以归纳为上面引入的图片。
// spec 父Spec padding值 childDimension为布局文件中设置的参数
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
// 父specMode
int specMode = MeasureSpec.getMode(spec);
// 父specSize
int specSize = MeasureSpec.getSize(spec);
// 父实际可用的空间
int size = Math.max(0, specSize - padding);
// 最终size
int resultSize = 0;
// 最终模式
int resultMode = 0;
// 结合父测量模式以及自身LayoutParams来创建child的测量模式
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
的宽高是固定值
,无论父MeasureSpec为何值,View的测量模式都为EXACTLY
,且specSize都为实际值
。 -
如果
View
的宽高是wrap_content
,无论父MeasureSpec为何值(EXACTLY/AT_MOST
),View的测量模式都为AT_MOST
,且specSize都不笨办法超过父实际的size
。 -
如果
View
的宽高是match_parent
,父测量模式为EXACTLY
,子View的测量模式也为EXACTLY
;父测量模式为AT_MOST
,子View的测量模式也为AT_MOST
;
到此整个View的Measure过程就结束了,测量完成后可通过getMeasuredWidth/Height()获取到View的测量宽高。整个流程的流程图如下(省略了好多细节,大体上走了一遍流程):
在这里插入图片描述4.View的layout过程
Layout的作用是确定子元素的位置,layout方法确定本身所在位置,onLayout确定所有子元素的位置。跟踪ViewRootImpl中的performLayout方法。
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
......
// host 即为DecorView
final View host = mView;
if (host == null) {
return;
}
......
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
......
}
4.1 View中的layout方法
首先初始化LTRB,其中l,t,r,b分别为View的四个顶点的位置。接着调用onLayout确定子元素的位置。onLayout与onMeasure相似,具体的实现在具体的View中重写即可。onLayout中遍历调用setChildFrame(),内部调用的是 child.layout(left, top, left + width, top + height);通过递归完成View树的布局过程。
/**
* <p>Derived classes should not override this method.
* Derived classes with children should override
* onLayout. In that method, they should
* call layout on each of their children.</p>
*
* @param l Left position, relative to parent
* @param t Top position, relative to parent
* @param r Right position, relative to parent
* @param b Bottom position, relative to parent
*/
@SuppressWarnings({"unchecked"})
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;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); // 设置四个顶点的坐标。
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
// 调用onLayout确定所有子元素的位置
onLayout(changed, l, t, r, b);
if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
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);
}
}
}
......
......
}
把LinearLayout中onLayout的具体实现做分析:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
// 具体的实现方法
void layoutVertical(int left, int top, int right, int bottom) {
......
final int count = getVirtualChildCount();
......
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
// 测量宽高
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
......
// 为子元素指定对应的位置
// 设置的就是测量的宽高
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
......
}
}
}
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
5.View的draw过程
Draw过程相对比较简单,即将View绘制到屏幕上。从RootViewImpl中跟踪。观察源码:
private void performDraw() {
if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) {
return;
} else if (mView == null) {
return;
}
......
boolean canUseAsync = draw(fullRedrawNeeded);
......
}
private boolean draw(boolean fullRedrawNeeded) {
......
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
scalingRequired, dirty, surfaceInsets)) {
return false;
}
......
return useAsyncReport;
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
......
mView.draw(canvas);
......
}
接着会调用到View的draw方法,其源码如下:
@CallSuper
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* 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
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
// 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
final Paint p = scrollabilityCache.paint;
final Matrix matrix = scrollabilityCache.matrix;
final Shader fade = scrollabilityCache.shader;
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
}
注释已经解释的很清楚了,主要过程遵循如下几步:
-
绘制背景 drawBackground(canvas);
-
绘制内容 onDraw(canvas);
-
绘制children dispatchDraw(canvas);
-
绘制装饰 onDrawForeground(canvas);
View绘制过程的传递是通过dispatchDraw()来实现的,它会遍历调用所有子元素的draw方法。
View的工作原理就学习到这里了 !!! 太难了~!慢慢学习!!
感谢:
https://blog.csdn.net/xfhy_/article/details/90270630#comments
参考:《Android 开发艺术探索》
网友评论