View的测量布局绘制过程


theme: channing-cyan

在上一篇文章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 == TYPESTATUSBARPANEL //状态栏 || lp.type == TYPEINPUTMETHOD //输入框 || lp.type == TYPEVOLUME_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 协商测量的条件:只有wrapcontent才执行,因为matchparent和具体值都是exactly,都是具体值,肯定是用户指定的,不能改变用户意图,所以只有wrap_content才使用 * 2 协商测量的目的:使得dialog在大屏幕上显示美观 * 3 协商测量的过程: - 1) 首先看期望值是否大于默认值,大于才进行协商,小于则不需要协商,直接使用期望值; - 2) 协商时先尝试使用系统默认的较小尺寸(320dp)来看是否满足,满足则使用;否则取期望值和默认值的和的一半(肯定大于320dp)来看是否满足,满足则使用,不满足则直接使用期望值 * 4 我们发现,协商测量可能多次执行performMeasure(),所以一个View在显示过程中被measure()多次不需要惊讶,原因就在于此

performMeasure() 正式测量过程

先来看入口 ``` //...省略关于windowSizeMayChange的判断,满足这个条件是进入下面代码的条件之一,条件太jb复杂了,故省略 if (!mStopped || mReportNextDraw) { boolean focusChangedDueToTouchMode = ensureTouchModeLocally((relayoutResult & WindowManagerGlobal.RELAYOUTRESINTOUCHMODE) != 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.TRACETAGVIEW, "measure"); try { //调用了mView.measure(int,int); mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); } finally { Trace.traceEnd(Trace.TRACETAGVIEW); } } 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()前会先清除PFLAGMEASUREDDIMENSIONSET标记; * 3 如果测量值有缓存,则直接使用缓存值取进行设置宽高,同时设置PFLAG3MEASURENEEDEDBEFORELAYOUT标记, 否则调用onMeasure(int,int); * 4 onMeasure(int,int)内部会调用setMeasureDimension(int,int),同时会根据是否有背景以及minWidth/minHeight的值来给出一个建议的大小; * 5 setMeasureDimension(int,int)最后还是调用了setMeasureDimension(int,int)来设置宽高,并且又添加了PFLAGMEASUREDDIMENSIONSET这个标记,表示已经设置了宽高 * 6 measure(int,int)末尾会对PFLAGMEASUREDDIMENSION_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 & PFLAG3MEASURENEEDEDBEFORELAYOUT) != 0) { onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec); //onMeasure()后就去掉 mPrivateFlags3 &= ~PFLAG3MEASURENEEDEDBEFORELAYOUT; }

//上一次的位置信息
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()会检测PFLAG3MEASURENEEDEDBEFORELAYOUT这个标记来决定是否调用onMeasure() * 3 然后会先保存旧的位置,再通过setFrame()设置新位置,并清空PFLAGLAYOUTREQUIRED标记 * 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源码分析之二 异步消息的处理](https://juejin.cn/post/6960112470325198862) 接着看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基础](https://juejin.cn/post/6984411341960740878) 接着看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有更新,都会自底向上回溯到根节点,并且沿路添加标记,然后再从根节点向下回溯,检测标记来更新并清除标记。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值