在上一篇文章View的显示过程末尾,重点提到了ViewRootImpl的四个方法:
private void performTraversals() {
...
//协商测量
measureHierarchy
...
//测量
performMeasure();
...
//布局
performLayout();
...
//绘制
performDraw();
}
本章就重点分析此四法
前置分析(下面代码位于measureHierarchy之前)
performTraversals(){
//这个View就是DecorView
final View host = mView;
//本轮期望宽度
int desiredWindowWidth;
//本轮期望高度
int desiredWindowHeight;
//window大小是否改变,直接影响是否执行performMeasure();
boolean windowSizeMayChange = false;
if (mFirst) { //如果是第一次测量,需要走全部流程
//是否需要完全重新绘制,如果需要,那么后续绘制会全部绘制,否则只绘制需要绘制的局部
mFullRedrawNeeded = true;
//是否需要重新布局,如果需要重新布局,那么就会触发协商测量
mLayoutRequested = true;
final Configuration config = mContext.getResources().getConfiguration();
//判断是否是系统窗口:包括从状态栏下拉、键盘弹出、音量调整(下面有这个方法)
if (shouldUseDisplaySize(lp)) {
Point size = new Point();
mDisplay.getRealSize(size);//如果是系统窗口,则使用逻辑尺寸(下面有这个方法)
desiredWindowWidth = size.x;
desiredWindowHeight = size.y;
} else {
//不是系统窗口,直接使用窗口的大小,也就是物理尺寸
desiredWindowWidth = mWinFrame.width();
desiredWindowHeight = mWinFrame.height();
}
//...省略部分代码
} else {
//如果不是第一次,就尝试使用上一次的尺寸
desiredWindowWidth = frame.width();
desiredWindowHeight = frame.height();
//如果本次期望尺寸和上次尺寸不一样,则需要重新测量
if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
//所以这里重置几个标记
mFullRedrawNeeded = true;
mLayoutRequested = true;
windowSizeMayChange = true;
}
}
//这里有个切入代码1!!!
...
//协商测量
measureHierarchy
...
//测量
performMeaure();
...
//布局
performLayout();
...
//绘制
performDraw();
}
接下来看shouldUseDisplaySize(WindowManager.LayoutParams)
private static boolean shouldUseDisplaySize(final WindowManager.LayoutParams lp) {
return lp.type == TYPE_STATUS_BAR_PANEL //状态栏
|| lp.type == TYPE_INPUT_METHOD //输入框
|| lp.type == TYPE_VOLUME_OVERLAY; //音量调整框
}
方法很easy,就是判断window类型是否是:状态栏/输入框/音量调整框。 接下来看mDisplay.getRealSize(Point)
public void getRealSize(Point outSize) {
synchronized (this) {
updateDisplayInfoLocked();
//这里直接使用了逻辑尺寸,什么是逻辑尺寸,看下面
outSize.x = mDisplayInfo.logicalWidth;
outSize.y = mDisplayInfo.logicalHeight;
}
}
Display中关于逻辑尺寸的说明:
/** * The logical width of the display, in pixels. * Represents the usable size of the display which may be smaller than the * physical size when the system is emulating a smaller display. */
@UnsupportedAppUsage
public int logicalWidth;
/** * The logical height of the display, in pixels. * Represents the usable size of the display which may be smaller than the * physical size when the system is emulating a smaller display. */
@UnsupportedAppUsage
public int logicalHeight;
简单意思就是:这玩意可能比物理尺寸小点!这个是系统使用的,我们做App的不需要关心,知道就行.
小结:
- 1 如果是第一次测量,那么先判断是否是系统的特定窗口,如果是,则使用可能小一点的逻辑尺寸,否则直接使用窗口尺寸,同时设置需要完整绘制和重新布局的标记
- 2 如果不是第一次测量,则先取上一次的尺寸来看是否满足本次期望尺寸,如果不满足,就重置需要完整绘制和重新布局的标记,并且重置需要测量的标记
measureHierarchy() 协商测量过程
1 先看协商测量的入口,下面代码位于上面的切入代码1处:
//这里有三个标记,mLayoutRequested上面已经分析过了,mStopped表示窗口是否停止,第三个参数暂时不用管
boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
if (layoutRequested) {
final Resources res = mView.getContext().getResources();
//...省略部分代码
//进行协商测量,这里的入参依次是:DecorView,WidowManager.Layoutparams,resource,期望宽度,期望高度
//返回值是window是否改变,上面也提到过,直接影响是否执行performMeasure()过程
windowSizeMayChange |= measureHierarchy(host, lp, res,
desiredWindowWidth, desiredWindowHeight);
}
2 measureHierarchy()流程,牛逼的思想
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;
//这个标记表明:测量结果是否满足
boolean goodMeasure = false;
//只有在宽度是wrap_content的情况下才进行协商测量,原因看下面注释
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
// On large screens, we don't want to allow dialogs to just
// stretch to fill the entire width of the screen to display
// one line of text. First try doing the layout at a smaller
// size to see if it will fit.
// 这段注释说明了为什么要进行协商测量
// 大体意思就是:在大屏幕上展示一个对话框的时候,不想让对话框的宽度填满屏幕,尝试给一个较小的尺寸来展示,这样美观些
final DisplayMetrics packageMetrics = res.getDisplayMetrics();
//获取系统内置的默认尺寸,这个320dp
res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);
int baseSize = 0;
//进行单位转换,并把这个尺寸保存在baseSize
if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
baseSize = (int) mTmpValue.getDimension(packageMetrics);
}
// 如果baseSize==0,则不存在协商的条件,直接跳过
// 如果desiredWindowWidth<baseSize,则不需要协商,也直接跳过
if (baseSize != 0 && desiredWindowWidth > baseSize) {
//这里将MeasureSize拼接为MeasureSpec,也就是在高两位加上了测量模式(下面有代码展示)
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
//进行一次measure,我们之前说过,measure之后会保存一些标记(这里加个标记: measure标记1!!!)
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
//measure之后来看是否满足
if ((host.getMeasuredWidthAndState() & View.MEASURED_STATE_TOO_SMALL) == 0) {
//没有这个标记,说明不比期望尺寸小,说明满足期望尺寸
goodMeasure = true;
} else {
//不满足,继续搞,需要大一点,那么就取默认尺寸和期望尺寸的平均数(相加除以2)
baseSize = (baseSize + desiredWindowWidth) / 2;
//再组合一下尺寸,这里只搞了宽度,高度已经不需要了
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
//在measure一次
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
//再判断一下是否满足
if ((host.getMeasuredWidthAndState() & View.MEASURED_STATE_TOO_SMALL) == 0) {
if (DEBUG_DIALOG) Log.v(mTag, "Good!");
//满足测量
goodMeasure = true;
}//这里没有else,因为即使不满足,也没法处理了,不能再大了,干脆直接跳过,用期望尺寸去搞
}
}
}
//协商后还不满足 或者 根本就没参加协商过程(期望尺寸直接小于默认尺寸,goodMeasure还是false的)
if (!goodMeasure) {
//直接取期望值放进去
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
//measure一下
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
//检测尺寸是否变化
if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
//如果尺寸变化了,则更新这个标记
windowSizeMayChange = true;
}
}
//返回
return windowSizeMayChange;
}
小结:
- 1 协商测量的条件:只有wrap_content才执行,因为match_parent和具体值都是exactly,都是具体值,肯定是用户指定的,不能改变用户意图,所以只有wrap_content才使用
- 2 协商测量的目的:使得dialog在大屏幕上显示美观
- 3 协商测量的过程:
-
- 首先看期望值是否大于默认值,大于才进行协商,小于则不需要协商,直接使用期望值;
-
- 协商时先尝试使用系统默认的较小尺寸(320dp)来看是否满足,满足则使用;否则取期望值和默认值的和的一半(肯定大于320dp)来看是否满足,满足则使用,不满足则直接使用期望值
-
- 4 我们发现,协商测量可能多次执行performMeasure(),所以一个View在显示过程中被measure()多次不需要惊讶,原因就在于此
performMeasure() 正式测量过程
先来看入口
//...省略关于windowSizeMayChange的判断,满足这个条件是进入下面代码的条件之一,条件太jb复杂了,故省略
if (!mStopped || mReportNextDraw) {
boolean focusChangedDueToTouchMode = ensureTouchModeLocally((relayoutResult & WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()|| mHeight != host.getMeasuredHeight() || contentInsetsChanged ||updatedConfiguration) {
//拼装MeasureSpec,这里直接取出窗口尺寸
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
//执行测量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
//获取测量后的尺寸
int width = host.getMeasuredWidth();
int height = host.getMeasuredHeight();
boolean measureAgain = false;
//是否设置权重,设置了的话,就需要再次测量
if (lp.horizontalWeight > 0.0f) {
width += (int) ((mWidth - width) * lp.horizontalWeight);
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,MeasureSpec.EXACTLY);
measureAgain = true;
}
if (lp.verticalWeight > 0.0f) {
height += (int) ((mHeight - height) * lp.verticalWeight);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,MeasureSpec.EXACTLY);
measureAgain = true;
}
//需要重新测量
if (measureAgain) {
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
//这里刷新了标记,需要重新布局
layoutRequested = true;
}
}
重点:performMeasure(int,int)
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
//调用了mView.measure(int,int);
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
View#Measure(int,int)
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
//这一块表示是否打开了光学边界,对于开发者来说no egg use,不需要看的
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int oWidth = insets.left + insets.right;
int oHeight = insets.top + insets.bottom;
widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
}
//用一个long的高32位表示宽度,低32位表示高度,来缓存尺寸
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
//创建缓存
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
//是否要强制更新布局,当你调用了View.requestLayout(),就会添加这个PFLAG_FORCE_LAYOUT标记
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
//尺寸是否改变(注意:这里是MeasureSpec这个包含测量模式复合值,而不是MeasureSize这个只包含尺寸的数字)
final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec;
//是否是精准模式(match_parent或精确尺寸,比如100dp)
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) {
//去掉这个标记,这个标记表示已经正确的设置了尺寸,现在重新测量,肯定先去掉
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
//处理一下Rtl(从右往左排版)的情况
resolveRtlPropertiesIfNeeded();
//取出测量缓存,这个key就是上面用64位long存储宽高的那个玩意儿
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
//若果没有缓存 或者 无视缓存
if (cacheIndex < 0 || sIgnoreMeasureCache) {
//直接调用onMeasure()
onMeasure(widthMeasureSpec, heightMeasureSpec);
//去掉这个标记,这个标记表示,在布局之前是否需要触发一下onMeasure()
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
//有缓存,就取出缓存
long value = mMeasureCache.valueAt(cacheIndex);
//直接设置为宽高,宽为高32位,高为低32位,这里直接强转为int,正好只取低32位
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
//因为没有走onMeasure(),所以需要添加这个标记
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
//如果没有PFLAG_MEASURED_DIMENSION_SET这个标记,这里直接报错,这个标记是在setMeasuredDimension()里面添加的,就是检测有没有设置测量宽高的
//所以重写onMeasure(),一定要调用这两个方法,除非有人xjb写也没法
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException("View with id " + getId() + ": " + getClass().getName() + "#onMeasure() did not set the" + " measured dimension by calling" + " setMeasuredDimension()"); } //添加这个标记,表示需要重新布局 mPrivateFlags |= PFLAG_LAYOUT_REQUIRED; } //将本次测量的结果保存起来,作为下次的旧值来用 mOldWidthMeasureSpec = widthMeasureSpec; mOldHeightMeasureSpec = heightMeasureSpec; //将测量结果缓存起来 mMeasureCache.put(key, ((long) mMeasuredWidth) << 32|(long) mMeasuredHeight & 0xffffffffL); }
接下来看onMeasure(int,int)
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);
//如果是UNSPECIFIED,就返回最小宽度,否则返回测量宽度
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
protected int getSuggestedMinimumWidth() {
//这个逻辑很简单,有背景就取背景和minWidth的最大值,没有背景就取minWidth
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
接下来看setMeasuredDimension(int,int)
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
//处理光学边界(跳过)
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
//直接看这个
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
//保存了宽高,现在可以通过getMeasuredWidth/Height()获取了
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
//添加了这个标记,还记得刚刚那个xjb写的crash吗,就是检测这个的
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
小结:
- 1 performMeasure(int,int)直接调用了measure(int,int);
- 2 measure(int,int)会先判断是否需要进行measure(int,int);如果尺寸变了或者手动调用了requestLayout()才需要进行measure(),measure()前会先清除PFLAG_MEASURED_DIMENSION_SET标记;
- 3 如果测量值有缓存,则直接使用缓存值取进行设置宽高,同时设置PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT标记, 否则调用onMeasure(int,int);
- 4 onMeasure(int,int)内部会调用setMeasureDimension(int,int),同时会根据是否有背景以及minWidth/minHeight的值来给出一个建议的大小;
- 5 setMeasureDimension(int,int)最后还是调用了setMeasureDimension(int,int)来设置宽高,并且又添加了PFLAG_MEASURED_DIMENSION_SET这个标记,表示已经设置了宽高
- 6 measure(int,int)末尾会对PFLAG_MEASURED_DIMENSION_SET这个标记进行检测,如果有人重写了onMeasure(int,int)但是没有调用setMeasureDimension(int,int)就不会又这个标记,就会报错
- 7 measure(int,int)末尾还会把本次测量结果保存起来作为下次的旧值使用,并且还会缓存起来
- 8 调用链: ViewRootImpl.performTraversals() -> ViewRootImpl.performMeasure() -> View.measure() -> View.onMeasure() -> View.setMeasureDimension() -> View.setMeasureDimensionRaw();
performLayout() 布局过程
先来看入口
//是否需要布局, layoutRequested在上一阶段末尾已经为true了,其他两个参数也分析过了
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
boolean triggerGlobalLayoutListener = didLayout || mAttachInfo.mRecomputeGlobalAttributes;
if (didLayout) {
//调用performLayout()
performLayout(lp, mWidth, mHeight);
//...省略其他代码
}
接下来看performLayout:
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {
//...省略其他代码
final View host = mView;
if (host == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
try {
//直接调用了host.layout();
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
//...省略其他代码
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
接下来看View#layout():
public void layout(int l, int t, int r, int b) {
// 检测这个标记,如果有,就需要走一下onMeasure()
// 还记得这个标记吗,在View的measure()里面,有缓存的时候是直接设置宽高并同时添加这个标记的,而没有调用onMeasure()
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
//onMeasure()后就去掉
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
//上一次的位置信息
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
// 通过setFrame()来设置左上右下,并返回是否需要改变(下面有代码,可以先看)
// setOpticalFrame()是处理光学边界的,内部也调用了setFrame()
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
//如果位置改变了 或者 有PFLAG_LAYOUT_REQUIRED标记,还记得这个标记吗,在measure()后面添加的
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//触发onLayout()
onLayout(changed, l, t, r, b);
//...
//去掉这个标记,表示此View已经布局过了
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
//回调onLayoutChange(),这里我们可以拿到我们的真实位置和mWidth/mHeight了
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);
}
}
}
}
来看下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;
//...省略其他代码
}
return changed;
}
接着看View的onLayout:
//空实现
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
ViewGroup的onLayout:
//复写了此方法,声明为抽象的
@Override
protected abstract void onLayout(boolean changed,int l, int t, int r, int b);
小结:
- 1 performLayout()直接调用View.layout()
- 2 View.layout()会检测PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT这个标记来决定是否调用onMeasure()
- 3 然后会先保存旧的位置,再通过setFrame()设置新位置,并清空PFLAG_LAYOUT_REQUIRED标记
- 4 如果设置位置后发现有改变,就触发onLayout(),并回调onLayoutChange()
- 5 调用链: ViewRootImpl.performTraversals() -> ViewRootImpl.performLayout() -> View.layout(){setFrame(l,t,r,b)}-> View.onLayout()
performDraw() 绘制过程
先看入口
// 条件很简单,预绘制 或者 可见
// 预绘制表示: 在绘制过程中又来了绘制请求,比如不断滑动
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
if (!cancelDraw) {
//执行动画
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).startChangingAnimations();
}
mPendingTransitions.clear();
}
//重点!
performDraw();
} else {
//不需要绘制,要么是在滑动,要么是不可见
if (isViewVisible) {
//这里表示预绘制,也就是在滑动,那么就不断触发performTraversals()从而不断进行布局绘制测量
scheduleTraversals();
} else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
//跑到这里就表示不可见,直接清除动画
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).endChangingAnimations();
}
mPendingTransitions.clear();
}
}
接着看performDraw():
private void performDraw() {
//如果熄屏了,就跳过
if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) {
return;
} else if (mView == null) {
return;
}
//是否需要完全绘制,如果需要,就是绘制这个canvas,否则只绘制局部
final boolean fullRedrawNeeded = mFullRedrawNeeded || mReportNextDraw;
mFullRedrawNeeded = false;
mIsDrawing = true;
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");
//...省略Async逻辑
try {
//执行绘制
boolean canUseAsync = draw(fullRedrawNeeded);
//..省略部分代码
} finally {
mIsDrawing = false;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
//...省略部分代码
}
上述代码省略了大量的Async逻辑,关于Async的逻辑可以看另一篇Handler源码分析之二 异步消息的处理 接着看draw():
private boolean draw(boolean fullRedrawNeeded) {
Surface surface = mSurface;
//检测surface
if (!surface.isValid()) {
return false;
}
//展示fps
if (DEBUG_FPS) {
trackFPS();
}
//...省略滑动逻辑
//获取缩放比例
final float appScale = mAttachInfo.mApplicationScale;
//是否需要缩放
final boolean scalingRequired = mAttachInfo.mScalingRequired;
//绘制区域
final Rect dirty = mDirty;
//如果需要完整绘制,则直接将dirty全部清空
if (fullRedrawNeeded) {
dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
}
//回调onDraw(),不是View的onDraw(),是Listener的
mAttachInfo.mTreeObserver.dispatchOnDraw();
//根据是否在进行动画计算垂直滚动距离
boolean animating = mScroller != null && mScroller.computeScrollOffset();
final int curScrollY;
if (animating) {
curScrollY = mScroller.getCurrY();
} else {
curScrollY = mScrollY;
}
//计算偏移量
int xOffset = -mCanvasOffsetX;
int yOffset = -mCanvasOffsetY + curScrollY;
final WindowManager.LayoutParams params = mWindowAttributes;
//如果有insets,需要减去
final Rect surfaceInsets = params != null ? params.surfaceInsets : null;
if (surfaceInsets != null) {
xOffset -= surfaceInsets.left;
yOffset -= surfaceInsets.top;
dirty.offset(surfaceInsets.left, surfaceInsets.right);
}
//记录绘制时间
mAttachInfo.mDrawingTime = mChoreographer.getFrameTimeNanos() / TimeUtils.NANOS_PER_MS;
boolean useAsyncReport = false;
if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
//如果开启了硬件加速,则使用硬件加速
if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
//...省略部分代码
useAsyncReport = true;
//使用硬件绘制
mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
} else {
//这里表示软件绘制
//...省略部分代码
//直接调用drawSoftware()
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
scalingRequired, dirty, surfaceInsets)) {
return false;
}
}
}
//...省略部分代码
if (animating) {
mFullRedrawNeeded = true;
scheduleTraversals();
}
return useAsyncReport;
}
接下来直接看drawSoftware():
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
//画布
final Canvas canvas;
//处理偏移量
int dirtyXOffset = xoff;
int dirtyYOffset = yoff;
if (surfaceInsets != null) {
dirtyXOffset += surfaceInsets.left;
dirtyYOffset += surfaceInsets.top;
}
try {
//先减去边距
dirty.offset(-dirtyXOffset, -dirtyYOffset);
//再获取画布
canvas = mSurface.lockCanvas(dirty);
canvas.setDensity(mDensity);
} catch (Surface.OutOfResourcesException e) {
handleOutOfResourcesException(e);
return false;
} catch (IllegalArgumentException e) {
Log.e(mTag, "Could not lock surface", e);
mLayoutRequested = true;
return false;
} finally {
//最后再加上边距
dirty.offset(dirtyXOffset, dirtyYOffset);
}
try {
//如果canvas不是实心的,也就是带有透明度的,则需要清除,否则透明下的内容会被看到
//如果有偏移,则也需要清除,否则在偏移区域外的内容会被看到
if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
}
//清空dirty
dirty.setEmpty();
mIsAnimating = false;
//添加PFLAG_DRAWN标记,表示已经绘制
mView.mPrivateFlags |= View.PFLAG_DRAWN;
//根据偏移量将坐标系切换到View的坐标系
canvas.translate(-xoff, -yoff);
//调用View的draw()
mView.draw(canvas);
//无障碍功能逻辑
drawAccessibilityFocusedDrawableIfNeeded(canvas);
} finally {
try {
surface.unlockCanvasAndPost(canvas);
} catch (IllegalArgumentException e) {
mLayoutRequested = true;
return false;
}
}
return true;
}
这里面涉及比较多的坐标转换,如果不熟悉的话,可以看Android View基础 接着看View.draw():
public void draw(Canvas canvas) {
//此时的坐标系已经处理过滑动偏移量了,此时是View内容的坐标系
final int privateFlags = mPrivateFlags;
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;
//画背景
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条件的
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
onDraw(canvas); //回调onDraw()
// Step 4, draw the children
dispatchDraw(canvas); //dispatchDraw()
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);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
}
这里的onDraw(canvas)我们就不看了,不同的view有不同的实现,我们来看下drawBackground(canvas):
private void drawBackground(Canvas canvas) {
//背景就是个drawable
final Drawable background = mBackground;
if (background == null) {
return;
}
//设置背景边界,我们知道drawable如果不设置边界的话,绘制出来是没有效果的
setBackgroundBounds();
//...省略硬件加速逻辑
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
//如果没有滑动,直接绘制
background.draw(canvas);
} else {
//否则,先切换到原来位置,也就是没有滑动过的坐标系
canvas.translate(scrollX, scrollY);
//绘制背景
background.draw(canvas);
//再切换回来,也就是滑动过的坐标系,以便后续绘制
canvas.translate(-scrollX, -scrollY);
//上述逻辑说明:View背景的绘制是无视滑动的,也解释了为什么在滑动的过程中,View的背景不动?
}
}
//设置边界逻辑
void setBackgroundBounds() {
if (mBackgroundSizeChanged && mBackground != null) {
//直接设置为了当前view的大小
mBackground.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
rebuildOutline();
}
}
接着来看下dispatchDraw(canvas),直接看ViewGroup的即可。 ViewGroup#dispatchDraw(canvas):
protected void dispatchDraw(Canvas canvas) {
//此时的坐标系已经处理过偏移量了,此时是View内容的坐标系
boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
int flags = mGroupFlags;
//...省略动画逻辑代码
int clipSaveCount = 0;
//处理clipToPadding标签,如果为false,表示允许绘制到父布局的padding区域
final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
if (clipToPadding) {
//如果clipToPadding=true,则先保存当前图层
clipSaveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
//然后变换canvas
canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
mScrollX + mRight - mLeft - mPaddingRight,
mScrollY + mBottom - mTop - mPaddingBottom);
}
// We will draw our child's animation, let's reset the flag
mPrivateFlags &= ~PFLAG_DRAW_ANIMATION;
mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;
boolean more = false;
final long drawingTime = getDrawingTime();
//...
//获取child的存储列表
final ArrayList<View> preorderedList = usingRenderNodeProperties ? null : buildOrderedChildList();
//是否是用户指定了child的顺序
final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled();
//遍历绘制
for (int i = 0; i < childrenCount; i++) {
//...
//依次获取child
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
//如果child可见 或者 在执行动画,就绘制
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
//...省略部分代码
//绘制完毕,还原canvas
if (clipToPadding) {
canvas.restoreToCount(clipSaveCount);
}
// mGroupFlags might have been updated by drawChild()
flags = mGroupFlags;
//...
}
接着看drawChild:
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
//又掉了view.draw(),注意!!这里是三个参数的draw,跟上面的不一样
return child.draw(canvas, this, drawingTime);
}
接下来看view.draw(canvas,viewGroup,drawingTime):
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
//此时的坐标系是父View的坐标系
// 是否开启硬件加速,我们假设不开启,也就是false
final boolean hardwareAcceleratedCanvas = canvas.isHardwareAccelerated();
// 这个值是false
boolean drawingWithRenderNode = mAttachInfo != null
&& mAttachInfo.mHardwareAccelerated
&& hardwareAcceleratedCanvas;
boolean more = false;
final int parentFlags = parent.mGroupFlags;
Transformation transformToApply = null;
//是否需要缩放
final boolean scalingRequired = mAttachInfo != null && mAttachInfo.mScalingRequired;
//...省略动画逻辑
//标记为已经绘制
mPrivateFlags |= PFLAG_DRAWN;
//...
//绘图缓存
Bitmap cache = null;
// 获取绘制方式
int layerType = getLayerType();
// 如果是软件绘制(这里直接假定是)
if (layerType == LAYER_TYPE_SOFTWARE || !drawingWithRenderNode) {
if (layerType != LAYER_TYPE_NONE) {
layerType = LAYER_TYPE_SOFTWARE;
//构建缓存
buildDrawingCache(true);
}
//获取缓存
cache = getDrawingCache(true);
}
//滑动偏移量处理
int sx = 0;
int sy = 0;
if (!drawingWithRenderNode) {
computeScroll();
sx = mScrollX;
sy = mScrollY;
}
//是否使用缓存绘制
final boolean drawingWithDrawingCache = cache != null && !drawingWithRenderNode;
//是否有偏移
final boolean offsetForScroll = cache == null && !drawingWithRenderNode;
//保存当前图层
int restoreTo = -1;
if (!drawingWithRenderNode || transformToApply != null) {
restoreTo = canvas.save();
}
//计算偏移
if (offsetForScroll) {
canvas.translate(mLeft - sx, mTop - sy);
//上面这一行表示将坐标系切换到自己内容区的坐标系,可以拆分为:
//1 canvas.translate(mLeft,mTop): 将坐标系从父View的坐标系切换到自己的坐标系
//2 canvas.translate(-sx,-sy): 将坐标系从自己的坐标系切换到自己内容区的坐标系
} else {
if (!drawingWithRenderNode) {
//没有偏移,只需要切换到自己的坐标系(同时也是内容区的坐标系)即可
canvas.translate(mLeft, mTop);
}
//缩放处理
if (scalingRequired) {
if (drawingWithRenderNode) {
restoreTo = canvas.save();
}
final float scale = 1.0f / mAttachInfo.mApplicationScale;
canvas.scale(scale, scale);
}
}
float alpha = drawingWithRenderNode ? 1 : (getAlpha() * getTransitionAlpha());
//省略矩阵变换处理
if (!drawingWithRenderNode) {
//处理clipChildren标签
if ((parentFlags & ViewGroup.FLAG_CLIP_CHILDREN) != 0 && cache == null) {
if (offsetForScroll) {
//如果有偏移,则需要加上偏移量
canvas.clipRect(sx, sy, sx + getWidth(), sy + getHeight());
} else {
if (!scalingRequired || cache == null) {
//没有cache,裁剪整个区域
canvas.clipRect(0, 0, getWidth(), getHeight());
} else {
//否则取cache
canvas.clipRect(0, 0, cache.getWidth(), cache.getHeight());
}
}
}
if (mClipBounds != null) {
canvas.clipRect(mClipBounds);
}
}
//是否使用缓存绘制
if (!drawingWithDrawingCache) {
//不使用缓存
if (drawingWithRenderNode) {
//...
} else {
//如果有PFLAG_SKIP_DRAW标签,表示不需要绘制背景,直接dispatchDraw()即可,ViewGroup默认带有此标签
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
dispatchDraw(canvas);
} else {
//没有PFLAG_DIRTY_MASK,乖乖的走draw(canvas)
draw(canvas);
}
}
} else if (cache != null) {
//使用缓存绘制,且有缓存
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
if (layerType == LAYER_TYPE_NONE || mLayerPaint == null) {
//如果没有layerType,就取父View的
Paint cachePaint = parent.mCachePaint;
if (cachePaint == null) {
cachePaint = new Paint();
cachePaint.setDither(false);
parent.mCachePaint = cachePaint;
}
cachePaint.setAlpha((int) (alpha * 255));
//直接绘制到Bitmap即可
canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
} else {
int layerPaintAlpha = mLayerPaint.getAlpha();
if (alpha < 1) {
mLayerPaint.setAlpha((int) (alpha * layerPaintAlpha));
}
//直接绘制到Bitmap
canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint);
if (alpha < 1) {
mLayerPaint.setAlpha(layerPaintAlpha);
}
}
}
//还原
if (restoreTo >= 0) {
canvas.restoreToCount(restoreTo);
}
//..省略硬件加速代码
mRecreateDisplayList = false;
return more;
}
小结:
- 1 performDraw()直接调用了draw(boolean),在这里处理了动画,无障碍功能等,并根据是否开启硬件加速进行软硬绘制
- 2 软件绘制部分计算了边距,滚动处理,并根据偏移量切换到View内容的坐标系,然后调用view.draw()
- 3 在view.draw()内部,依次绘制背景,onDraw(),子元素(dispatchDraw()),前景等元素
- 4 绘制背景时会先将坐标系切换到View的坐标系,然后绘制背景,完事再切换回View内容的坐标系
- 5 dispatchDraw()内部回遍历调用drawChild(),drawChild()内部又调用了三个参数的view.draw(canvas,viewGroup,time)
- 6 三个参数的draw内部,又做了矩阵变换,坐标系变换,动画处理,绘图缓存等,最后又调用个一个参数的draw(),如此循环,知道全部绘制完毕.
- 7 调用链:ViewRootImpl.performTraversals() -> ViewRootImpl.performDraw() -> ViewRootImpl.draw(canvas) -> ViewRootImpl.drawSoftware() -> View.draw(){onDraw()} -> ViewGroup.dispatchDraw() -> ViewGroup.drawChild() -> View.draw(canvas,viewGroup,time) -> view.draw(canvas) -> …
总结
View的整体测量、布局、绘制流程大概就完事了,可以把View理解为一个多叉树,每次测量布局绘制都是从树根开始向下分发,是个深度遍历的过程。而每次View有更新,都会自底向上回溯到根节点,并且沿路添加标记,然后再从根节点向下回溯,检测标记来更新并清除标记。
今天的文章View的测量布局绘制过程分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/14115.html