一文搞定面试 | Android View绘制
绘制的起源
以两张图为开头,让大家对ViewRootImpl的来源和关联性有个大致的认知,因为绘制的主体发起者将是ViewRootImpl
绘制流程
这里以一个极简的demo,输出stack trace以此观测绘制的流程
这是常见的绘制流程图,虽然省略了部分细节,但整体环节的抽象描述恰如其分。通常,在自定义控件时,ViewGroup
的职责为存储View,需要重写onMeasure
、onLayout
,而View
的职责重在自身的绘制,主要需要重写onDraw
。注意是主要,具体实现场景,需要根据其需要做的事情和每个环节的能力决定,详见下面的分述章节
MeasureSpec
MeasureSpec
其实就是个Int,其内部又是老生常谈的位运算。分为高2位Mode
(因为就3种模式,2位足矣),和低30位Size
(最大表示到1048575),对3种模式的释义也附在代码注释里了
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
// ……
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
// MODE_MASK取反即低30位全1
return (measureSpec & ~MODE_MASK);
}
private static final int MODE_SHIFT = 30;
// 0x3 即 二进制 11,左移30位,即取到高2位
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
@IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
@Retention(RetentionPolicy.SOURCE)
public @interface MeasureSpecMode {}
/**
* Measure specification mode: The parent has not imposed any constraint
* on the child. It can be whatever size it wants.
* child想多大,就多大。这和下面的EXACTLY在一定程度上是一致的,只不过当前child尚还不知道自己会有多大
* 类似的场景,比如TextView,在动态setText时,bounds会变化,但在WRAP_CONTENT时如果超出parent会被截断,就是第三种
* 这时,想让TextView的size为自身实际大小的话,就需要用到这种,类似于Paint.measureText
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
/**
* Measure specification mode: 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.
* 精确值如20dp,再比如matchParent,因为parent size已知,其实就是EXACTLY
*/
public static final int EXACTLY = 1 << MODE_SHIFT;
/**
* Measure specification mode: The child can be as large as it wants up
* to the specified size.
* 就是常见的wrapContent,限制最大尺寸
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
LayoutParams
addView
或new View()
时通常都会使用到LayoutParams
,那为什么它会和MeasureSpec产生了关联呢?因为在测量环节生成MeasureSpec.makeMeasureSpec
时通常都会传入lp
选用了源码中getRootMeasureSpec
的英文注释,教科书般解释了LayoutParams.xml
中设置的与实际的MeasureSpec
的对应关系
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width)
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// 强制最大,即尺寸固定时为 mode:EXACTLY,size:为强制的parent size
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// 自适应,即限制最大尺寸时为 mode:AT_MOST,size:限制的最大尺寸
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// 不限制,同为固定尺寸,只不过这里size为设置的尺寸值
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
Measure 测量大小
先看View里measure
,实际通过setMeasuredDimensionRaw
,对mMeasuredWidth\mMeasuredHeight
进行赋值,之后(比如layout
环节,或手动调用measure测量后)getMeasuredWidth
就能取到值了。同时,这里需要注意forceLayout
和needsLayout
两个判断,force取值于mPrivateFlags
,与requestLayout的发起有关,后面灵魂发问里会细讲。而needs就很顾名思义了,就判断一波尺寸是否发生变化,发生变化即认为需要。其中还需要关注下另一个标记位PFLAG_LAYOUT_REQUIRED
// View.java
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// ……
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
// requestLayout()会设置这个标记位,下文可在灵魂发问的1中细看,如果为true,那就比然发起measure
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
// Optimize layout by avoiding an extra EXACTLY pass when the view is
// already measured as the correct size. In API 23 and below, this
// extra pass is required to make LinearLayout re-distribute weight.
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);
final boolean needsLayout = specChanged
&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
if (forceLayout || needsLayout) {
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
// 关注下这里的调用链onMeasure -> setMeasuredDimension -> setMeasuredDimensionRaw
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
// 这里!!很重要,是layout的标记位,也就是说measure会影响layout行为
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
// ……
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
以FrameLayout
为例,来看看ViewGroup,是如何调用到view.measure的吧。先是遍历child进行measureChildWithMargins
》》》child.measure
,再对自身setMeasuredDimension
。其中getChildMeasureSpec
及其传入的参数是最终影响child测量的关键,当然自定义ViewGroup时也可以参照调用getChildMeasureSpec
发起对child的测量
跑马灯一文中,在setView后,如果需要重新测量,就会调用view.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED))
去获取例如TextView重新setText之后的尺寸,后来发现动画设置的并无异常,但child view自身的尺寸被parent 限制了。经debug发现,手动measure之后,其自发又进行了自上而下的measure,而此时传递下来的SpecMode
是parent FrameLayout
的EXACTLY
,而由于自身默认LayoutParams
为WRAP_CONTENT
,导致其最终走向AT_MOST with Parent size
// ViewGroup.java
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 them 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);
}
下面这幅图就是上面源码的总结,其最终会被转化为对应的MeasureSpec
,不建议硬背,应该在实际使用时或遇到问题时,反复去翻看源码巩固记忆。千万要注意,这是getChildMeasureSpec
由parent测量child时对child的影响,和child自行发起measure
是不同的
讲完上述源码,讲一下开发中的实际应用
以width为例,常用的取值方案有如下几种:
- view.post 通过保证在attach后回调,获取到测量完成的值
- addOnGlobalLayoutListener\addOnPreDrawListener,实际上也依赖于attch,是全局viewtree的观测
- onLayout\addOnLayoutChangeListener,这个在下面layout会讲,属于就是在后置环节去取
- 手动调用view.measure(),并且手动生成MeasureSpec.makeMeasureSpec。这个就属于比较万能的了,可以根据自己想要的限制去取到需要的尺寸
Layout 摆放位置
先看View里layout
,实际通过setFrame
,对成员属性mLeft……mBottom
进行赋值,且如getWidth\Height
也源于此。PFLAG_LAYOUT_REQUIRED
再次出现,上文measure提到过,这里就是印证了
// View.java
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;
}
// …
// setOpticalFrame里也最终调用了setFrame,最终确定bounds的位置,同时会根据返回值决定是否要刷新调用onLayout、onLayoutChange
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
// 注意看这是或语句,如果尺寸改变changed或者PFLAG_LAYOUT_REQUIRED被标记,都会调用onLayout
// 而上文中起到measure完成后,会设置PFLAG_LAYOUT_REQUIRED,也就是measure后必然跟随layout
// 当然也不能完全这么说,performLayout的前置条件就是layoutRequested,即requestLayout设置的
// 单纯的measure不会主动发起layout
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
// ……
// 这里还有个OnLayoutChangeListeners的遍历
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
// ……
}
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,这一点与灵魂发问1中有关联
invalidate(sizeChanged);
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
mPrivateFlags |= PFLAG_HAS_BOUNDS;
if (sizeChanged) {
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
// ……
}
return changed;
}
通常取用width时有两种,getMeasuredWidth
和getWidth
。getWidth
在layout完成之后即onDraw环节可以使用,可以取到,值来源于bounds的左右边界差值
public final int getWidth() {
return mRight - mLeft;
}
因为View.layout由ViewGroup调用,这里举例LinearLayout
源码看一下,在具体自定义ViewGroup时可参考应该如何摆放child。其中,以Vertical
为例,childTop
控制行位置纵向排列,setChildFrame
里最终调用了child.layout
进行摆放。整体还是比较简单的,建议可以尝试写个流式布局FlexBoxLayout练练手。
// LinearLayout.java
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) {
// ……一些Gravity的处理
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
// 这里取用的是MeasuredWidth,需要注意
// 像自行发起的measure,对实际自身不会产生影响,因为在像TextView.setText
// resize后会发起requestLayout,并重新measure,所以需要通过layoutParam干预后续的onMeasure
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
// ……一些child gravity处理
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
childTop += lp.topMargin;
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
// 这里
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
Draw Canvas绘制
老规矩,先看View.draw
,分为如下6步,其中2、5一般不执行。当然每个环节重要的在于其前后环节的意义,其自身意义并不一定那么死板,比如ViewGroup在super.dispatchDraw其实意义等同于override onDraw
- 绘制背景,我们通常设置的background,最终会被转化为Drawable对象,然后调用其draw方法,这也是GradientDrawable的原理
- 保存 Canvas 图层为后续淡出做准备(可选)
- 绘制 View 的内容,对于View自身就可以在这个环节进行对应的绘制了
- 绘制子 View (dispatchDraw),对于ViewGroup来说,完成了自身便开始分发绘制child view
- 绘制淡出边缘并恢复 Canvas 图层(可选)
- 绘制装饰(比如 foreground 和 scrollbar)
// View.java
public void draw(Canvas canvas) {
// Step 1, draw the background, if needed
int saveCount;
// 这里不支持override,但会调用backgroud的Drawable.draw()
drawBackground(canvas);
// skip step 2 & 5 if possible (common case)
// 一般情况下第 2 步和第 5 步是不执行的。
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
// RecyclerView的ItemDecoration就是这个环节进行分发绘制的
onDraw(canvas);
// Step 4, draw the children
// 当然也有很多ViewGroup不在onDraw里进行自己的绘制,而选择在super.dispatchDraw之前进行绘制一些辅助内容
// 比如ListView的divline,SmartRefreshLayout的header\footer可以进行一些缩放&位置处理
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// we're done...
return;
}
对于Canvas的绘制,其中还有很多妙招和玩法,想要精通还得多多练习。这边奉上 抛物线
大佬的HenCoder
系列,下面是第1节
灵魂发问
1. invalidate和requestLayout的区别
从发起环节来看,requestLayout
设置了PFLAG_FORCE_LAYOUT
(上文measure和layout环节提过了,没注意的快去回顾一下,同时与PFLAG_FORCE_LAYOUT
关联的是PFLAG_LAYOUT_REQUIRED
)和PFLAG_INVALIDATED
两个标志位,而invalidate仅设置了PFLAG_INVALIDATED
一个标志位,共同点是parent.requestLayout
,p.invalidateChild(View中) -> parent.invalidateChildInParent(ViewGroup中)
均向上进行传递
// View.java
public void requestLayout() {
if (mMeasureCache != null) mMeasureCache.clear();
// ……
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
if (mParent != null && !mParent.isLayoutRequested()) {
// 这里看仔细条件,如果Parent不在requestLayout过程中,就向上传递,如果还在上一次的layout中,那就不会继续了
mParent.requestLayout();
}
// ……
}
// invalidateCache是invalidate调用时传入用于标记局部刷新还是全部刷新,默认true为全部刷新
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
// ……
// 一些非法状态判断,比如不可见、mCurrentAnimation
if (skipInvalidate()) {
return;
}
// 脏区域标记
mPrivateFlags |= PFLAG_DIRTY;
if (invalidateCache) {
// invalidateCache是硬件加速中用于重建View的display list的标记
// 这个可能解释的不太恰当,我这里理解因为仅在ThreadedRenderer取用了该标记位
mPrivateFlags |= PFLAG_INVALIDATED;
// 清除PFLAG_DRAWING_CACHE_VALID
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
}
// Propagate the damage rectangle to the parent view.
// 传递需要重绘的区域给parent
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);
}
// ……
}
PFLAG_INVALIDATED
会被标记在view.mRecreateDisplayList
,对于draw在分发过程中是否触发draw有决定作用。
可能本文对该PFLAG_INVALIDATED标记的认知理解不太正确,如果有了解的大佬欢迎指点
// ThreadedRenderer.java
view.mRecreateDisplayList = (view.mPrivateFlags & View.PFLAG_INVALIDATED) == View.PFLAG_INVALIDATED;
// ViewGroup.java
public RenderNode updateDisplayListIfDirty() {
// ……
if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0
|| !renderNode.hasDisplayList()
|| (mRecreateDisplayList)) {
// mRecreateDisplayList为true只是其中一个条件,再结合下一个判断,那mRecreateDisplayList为true就是draw的必要条件了
// PFLAG_DRAWING_CACHE_VALID的清除在这是也起作用了
if (renderNode.hasDisplayList()
&& !mRecreateDisplayList) {
// mRecreateDisplayList为false的话就return了
mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
dispatchGetDisplayList();
return renderNode; // no work needed
}
// ……
try {
if (layerType == LAYER_TYPE_SOFTWARE) {
// ……
} else {
// ……
draw(canvas);
// ……
}
} finally {
// ……
}
} else {
mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
}
return renderNode;
}
对于invalidate来说,有点特殊,稍作展开,区别在硬件加速和非硬件加速(即软件加速),对于这两种的区别,这边不作展开,仅记录结论。硬件加速重在向上传递需要重绘的view
,而软件加速重在向上传递需要重绘的dirtyRect
区域,对于整个分发流程,调用的方法也会不一致(这一点在文章开头的堆栈就说明了)。对于绘制流程来说,如果dirty
不为空,那就需要进行绘制,而硬件绘制仅对target view
标定,可以减少不必要的绘制
// xml中硬件加速配置
<application
android:hardwareAccelerated="true" />
// ViewGroup.java
public final void invalidateChild(View child, final Rect dirty) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null && attachInfo.mHardwareAccelerated) {
// HW accelerated fast path
onDescendantInvalidated(child, child);
return;
}
ViewParent parent = this;
if (attachInfo != null) {
// ……
do {
View view = null;
if (parent instanceof View) {
view = (View) parent;
}
// ……
parent = parent.invalidateChildInParent(location, dirty);
// ……
} while (parent != null);
}
}
ViewGroup.invalidateChildInParent
主要完成了dirty区域的兼并计算,一直传递到ViewRootImpl.invalidateChildInParent
,而其中不管是invalidate
还是invalidateRectOnScreen
,所以无论invalidate
和requestLayout
最终均调用了ViewRootImpl.scheduleTraversals
,然后开始了文章开头的三板斧流程
// ViewRootImpl.java
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
// 这里是提过的线程校验
checkThread();
// 这个影响performLayout发起
mLayoutRequested = true;
scheduleTraversals();
}
}
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
checkThread();
// ……
if (dirty == null) {
invalidate();
return null;
} else if (dirty.isEmpty() && !mIsAnimating) {
return null;
}
// ……
invalidateRectOnScreen(dirty);
return null;
}
最后兜兜转转,我们还是回来了
// ViewRootImpl.java
private void performTraversals() {
//mLayoutRequested 在requestLayout时赋值为true
boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
if (layoutRequested) {
//measure 过程
windowSizeMayChange |= measureHierarchy(host, lp, res,
desiredWindowWidth, desiredWindowHeight);
}
...
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
if (didLayout) {
//layout 过程
performLayout(lp, mWidth, mHeight);
}
...
}
总结:
其实requestLayout
和invalidate
均会发起scheduleTraversals
之后的三大流程,但对于是否分发深入下去,其中的标记位起到了决定性作用。因此
requestLayout
必然会进行measure、layout、draw(会根据size是否change在layout时发起invalidate,如果不确定是否change且需要重绘,可以再手动调用invalidate())
invalidate
仅会进行draw,当然如果在期间环节,某个view.layoutParams发生变化
,导致measure时判断needsLayout
为true,那也会进行measure、layout
2. 为什么Activity.onResume时还取不到控件高度
3. 为什么view.post可以获取到控件高度
post即把action放在view的等待队列mRunQueue
中,在performTraversals>>view.dispatchAttachedToWindow
会取出所有的handler action
推送到ViewRootHandler,由于performTraversals自身就是个handler Runnable
,所以view.post内容一定会等到performTraversals完成后才会等到消息队列执行。如果对Handler的消息队列不熟的,快去我专栏复习!!
// ViewRootImpl.java
private void performTraversals() {
// ……
host.dispatchAttachedToWindow(mAttachInfo, 0);
// ……
getRunQueue().executeActions(mAttachInfo.mHandler);
// ……
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
// ……
performLayout(lp, mWidth, mHeight);
// ……
performDraw();
}
// View.java
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
// ……
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
}
// View.java
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().post(action);
return true;
}
4. view.setLayoutParams如何产生效果
看到requestLayout
相信大家就应该明白了,再结合measure流程中measureChild通常会用layoutParams,就一目了然了
// View.java
public void setLayoutParams(ViewGroup.LayoutParams params) {
if (params == null) {
throw new NullPointerException("Layout parameters cannot be null");
}
mLayoutParams = params;
resolveLayoutParams();
if (mParent instanceof ViewGroup) {
((ViewGroup) mParent).onSetLayoutParams(this, params);
}
requestLayout();
}
5. 应用切到后台,测量、绘制等操作还会执行吗
当activity STOP时,会根据如下调用链设置ViewRootImpl.mStopped
为true,在performTraversals
时,会拦截measure、layout、draw,虽然draw这边可能有些不清不楚,但盲猜一波是拦住了,因为setWindowStopped
直接对renderer
进行了stop设置和destroy处理
// Actvity.java
final void performStop(boolean preserveWindow, String reason) {
// ……
if (!preserveWindow && mToken != null && mParent == null) {
WindowManagerGlobal.getInstance().setStoppedState(mToken, true);
}
// ……
}
// WindowManagerGlobal.java
public void setStoppedState(IBinder token, boolean stopped) {
// ……
ViewRootImpl root = mRoots.get(i);
root.setWindowStopped(stopped);
// ……
}
// ViewRootImpl.java
private void performTraversals() {
// ……
if (!mStopped || wasReportNextDraw) {
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
// ……
final boolean didLayout = layoutRequested && (!mStopped || wasReportNextDraw);
if (didLayout) {
performLayout(lp, mWidth, mHeight);
}
// ……
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
if (!cancelDraw) {
// ……
performDraw();
} else {
if (isViewVisible) {
// Try again
scheduleTraversals();
} else {
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).endChangingAnimations();
}
mPendingTransitions.clear();
}
// We may never draw since it's not visible. Report back that we're finished
// drawing.
if (!wasReportNextDraw && mReportNextDraw) {
mReportNextDraw = false;
pendingDrawFinished();
}
}
}
}
6. 暂无,欢迎补充
关于View的绘制方面的面试题一般需讲述整体流程,和部分细节源码,同时也会关注项目中的实践。当然文章开头提到的ViewRootImpl也是中高级的高频切入点之一。对于本文来说,建议面试者亲自手写一个自定义ViewGroup如流式布局,一个自定义View如柱状图,以此加深理解为好,还是强烈安利HenCoder
。
其他参考资料
转载自:https://juejin.cn/post/7229842136286920762