View的测量布局绘制过程

7/13/2021 AndroidView源码

在上一篇文章View的显示过程末尾,重点提到了ViewRootImpl的四个方法:

private void performTraversals() {
    ...
    //协商测量
    measureHierarchy
    ...
    //测量
    performMeasure();
    ...
    //布局
    performLayout();
    ...
    //绘制
    performDraw();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

本章就重点分析此四法。

# 前置分析(下面代码位于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();
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

接下来看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; //音量调整框
}
1
2
3
4
5

方法很easy,就是判断window类型是否是:状态栏/输入框/音量调整框。

接下来看mDisplay.getRealSize(Point)

public void getRealSize(Point outSize) {
    synchronized (this) {
        updateDisplayInfoLocked();
        //这里直接使用了逻辑尺寸,什么是逻辑尺寸,看下面
        outSize.x = mDisplayInfo.logicalWidth;
        outSize.y = mDisplayInfo.logicalHeight;
    }
}
1
2
3
4
5
6
7
8

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;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

简单意思就是:这玩意可能比物理尺寸小点!这个是系统使用的,我们做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);
}
1
2
3
4
5
6
7
8
9
10
11

# 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

小结:

  • 1 协商测量的条件:只有wrap_content才执行,因为match_parent和具体值都是exactly,都是具体值,肯定是用户指定的,不能改变用户意图,所以只有wrap_content才使用
  • 2 协商测量的目的:使得dialog在大屏幕上显示美观
  • 3 协商测量的过程:
      1. 首先看期望值是否大于默认值,大于才进行协商,小于则不需要协商,直接使用期望值;
      1. 协商时先尝试使用系统默认的较小尺寸(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;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

重点: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);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

接下来看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());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

接下来看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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

小结:

  • 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.performLayout() -> 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);
    
    //...省略其他代码
}
1
2
3
4
5
6
7
8
9

接下来看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);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

接下来看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);
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

来看下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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

接着看View的onLayout:

//空实现
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
1
2
3

ViewGroup的onLayout:

//复写了此方法,声明为抽象的
@Override
protected abstract void onLayout(boolean changed,int l, int t, int r, int b);
1
2
3

小结:

  • 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();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

接着看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);
    }

    //...省略部分代码
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

上述代码省略了大量的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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84

接下来直接看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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

这里面涉及比较多的坐标转换,如果不熟悉的话,可以看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;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

这里的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();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

接着来看下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;

    //...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

接着看drawChild:

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    //又掉了view.draw(),注意!!这里是三个参数的draw,跟上面的不一样
    return child.draw(canvas, this, drawingTime);
}
1
2
3
4

接下来看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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159

小结:

  • 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有更新,都会自底向上回溯到根节点,并且沿路添加标记,然后再从根节点向下回溯,检测标记来更新并清除标记。

Last Updated: 1/30/2022, 3:38:46 PM