View的绘制流程之三:View的绘制流程源码分析

1、回顾

由上一篇笔记 《View的绘制流程之二:View的绘制入口源码分析》我们知道了 View 的绘制入口方法;在 ViewRootImpl 中的 performTraversals() 方法,在这个方法中会分别调用 performMeasure() 、 performLayout() 、performDraw() 方法进行测量、摆放、绘制;现在我们就开始对这个流程进行分析


2、performTraversals()源码分析

看到 ViewRootImpl 中的 performTraversals() 方法,这个方法贼长
★ ViewRootImpl # performTraversals()

private void performTraversals() {
    // 1、缓存View,因为在这之后会经常使用
    final View host = mView;
    ....
    // 标记正在遍历View进行绘制
    mIsInTraversal = true;
    ....
    // 2、获取 Window的LayoutParams,mWindowAttributes是一个WindowManager.LayoutParams
    // 宽高的测量模式默认都是 MATCH_PARENT
    WindowManager.LayoutParams lp = mWindowAttributes;
    ....
    Rect frame = mWinFrame;
    // 3、如果是View是刚添加进 ViewRootImpl中,那么在创建 ViewRootImpl对象的时候就会初始化 mFirst为true
    if (mFirst) {
        ....
        // 4、开始分发该 View正在添加到Window这个事件
        host.dispatchAttachedToWindow(mAttachInfo, 0);
        mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
        dispatchApplyInsets(host);
    } ...
    // 5、如果View的可见性改变就会走下面这个逻辑
    if (viewVisibilityChanged) {
        mAttachInfo.mWindowVisibility = viewVisibility;
        host.dispatchWindowVisibilityChanged(viewVisibility);
        if (viewVisibility != View.VISIBLE || mNewSurfaceNeeded) {
            destroyHardwareResources();
        }
        if (viewVisibility == View.GONE) {
            mHasHadWindowFocus = false;
        }
    }
    ....
    if (mFirst || windowShouldResize || insetsChanged ||
            viewVisibilityChanged || params != null) {
        ....    
        // 6、获取窗体宽高
        if (mWidth != frame.width() || mHeight != frame.height()) {
            mWidth = frame.width();
            mHeight = frame.height();
        }

        if (!mStopped || mReportNextDraw) {
            boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
                    (relayoutResult & WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
            if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
                    || mHeight != host.getMeasuredHeight() || contentInsetsChanged) {
                // 7、根据窗体的宽度mWidth以及窗体LayoutParams的宽度获取 View宽度测量规格
                // 由上面可知Window的宽高测量模式都是 MATCH_PARENT
                int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
                // 8、开始 measure View
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                ....
            }
        }
    } ....

    final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
    boolean triggerGlobalLayoutListener = didLayout
            || mAttachInfo.mRecomputeGlobalAttributes;
    if (didLayout) {
        // 9、开始摆放 View
        performLayout(lp, desiredWindowWidth, desiredWindowHeight);
        ....
    }
    ....
    if (!cancelDraw && !newSurface) {
        if (!skipDraw || mReportNextDraw) {
            if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                for (int i = 0; i < mPendingTransitions.size(); ++i) {
                    mPendingTransitions.get(i).startChangingAnimations();
                }
                mPendingTransitions.clear();
            }
            // 10、开始绘制 View
            performDraw();
        }
    } else {
        // 11、当View设置为VISIBLE后就会调用 scheduleTraversals()方法重新调度View的绘制任务
        if (viewVisibility == View.VISIBLE) {
            scheduleTraversals();
        } ....
    }
    // 重置绘制标记位为false  
    mIsInTraversal = false;
}

分析: performTraversals() 方法的大致流程如下:
第 1 步:缓存 View,因为在这之后会经常使用

第 2 步:获取 Window 的 LayoutParams ,mWindowAttributes 是一个 WindowManager.LayoutParams,宽高的测量规格模式都是 MATCH_PARENT

第 3 步:根据 mFirst判断该 ViewRootImpl 是否第一次调用 performTraversals() 方法进行 View 的绘制,mFirst只会在 ViewRootImpl的构造方法中标记位 true,也只会在 performTraversals()方法中标记为 false

第 4 步:调用 DecorView 的 dispatchAttachedToWindow() 方法,DecorView 会将这个事件分发给子View,最终会调用 DecorView 和其子View 的 onAttachedToWindow() 方法(如果子View 是 ViewGroup ,那么就会继续分发下去),通过 这个方法我们可以在 View 添加到 Window 后进行一些设置(注意:此时这个View 还没有进行测量、摆放、绘制)

第 5 步:判断一下 View 的可见性是否发生过变化,如果有就会做一些操作

第 6 步:获取 Window 窗体的宽高

第 7 步:根据窗体的宽度以及窗体的 LayoutParams 的宽度模式获取 View 的宽度测量规格,由上面的代码可知,窗体的宽高的 LayoutParams 中的模式的是 MATCH_PARENT

第 9 步:调用 performMeasure() 方法开始测量子View

第 10 步:调用 performLayout() 方法开始摆放子View

第 11 步:调用 performDraw() 方法开始绘制子View

第 12 步:当 View 设置了 VISIBLE 后就会调用 scheduleTraversals() 方法重新调度 View 的绘制任务

▲ 下面对几个比较重要的点进行分析,至于View绘制流程的三个主要方法会在下一个大节进行分析
 
Point 2:
获取 Window 的 LayoutParams ,mWindowAttributes 是一个 WindowManager.LayoutParams,宽高的测量规格模式都是 MATCH_PARENT,源码如下:
★ ViewRootImpl

// 2、获取 Window的LayoutParams,mWindowAttributes是一个WindowManager.LayoutParams
// 宽高的测量模式默认都是 MATCH_PARENT
WindowManager.LayoutParams lp = mWindowAttributes;
final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();

我们看到 WindowManager.LayoutParams 的构造方法
★ WindowManager.LayoutParams # 构造方法

public LayoutParams() {
    super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    type = TYPE_APPLICATION;
    format = PixelFormat.OPAQUE;
}

这里直接使用了传入了 LayoutParams.MATCH_PARENT , WindowManager.LayoutParams 继承于 ViewGroup.LayoutParams,所以说宽度和高度都是 MATH_PARENT

Point 4:
调用 DecorView 的 dispatchAttachedToWindow() 方法,DecorView 会将这个事件分发给子View,最终会调用 DecorView 和其子View 的 onAttachedToWindow() 方法(如果子View 是 ViewGroup ,那么就会继续分发下去),通过 这个方法我们可以在 View 添加到 Window 后进行一些设置(注意:此时这个View 还没有进行测量、摆放、绘制),源码如下:
★ ViewRootImpl

// 4、开始分发该 View正在添加到Window这个事件
host.dispatchAttachedToWindow(mAttachInfo, 0);

 
这里的 host就是我们的 DecorView , dispatchAttachedToWindow() 是 View 类中的方法, ViewGroup 对该方法进行了重写,但是 DecorView 以及他的父类 FrameLayout 都没有重写 dispatchAttachedToWindow()
 
首先,我们看到 ViewGroup 的 dispatchAttachedToWindow() 方法
★ ViewGroup # dispatchAttachedToWindow()

@Override
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    mGroupFlags |= FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
    super.dispatchAttachedToWindow(info, visibility);
    mGroupFlags &= ~FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
    final int count = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < count; i++) {
        final View child = children[i];
        child.dispatchAttachedToWindow(info,
                combineVisibility(visibility, child.getVisibility()));
    }
    ....
}

在该方法中首先 super 了父类 View 的方法,然后遍历所有的子View ,把事件分发给所有的子View
 

接着,所有的子View 都会遍历调用该方法,那么看到 View 的 dispatchAttachedToWindow() 方法
★ View # dispatchAttachedToWindow()

void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    ....
    // 记录当前View添加到 Window的次数
    mWindowAttachCount++;
    ....
    // 调用View的 onAttachedToWindow()方法告知子View
    onAttachedToWindow();
    ....
    int vis = info.mWindowVisibility;
    if (vis != GONE) {
        onWindowVisibilityChanged(vis);
    }
    onVisibilityChanged(this, visibility);
    ....
}

首先会记录该 View 添加到 Window 的次数,然后就会调用 onAttachedToWindow() 方法告知子View
 

最后,我们看到 View 的 onAttachedToWindow() 方法中
★ View # onAttachedToWindow()

@CallSuper
protected void onAttachedToWindow() {
    if ((mPrivateFlags & PFLAG_REQUEST_TRANSPARENT_REGIONS) != 0) {
        mParent.requestTransparentRegion(this);
    }
    mPrivateFlags3 &= ~PFLAG3_IS_LAID_OUT;
    jumpDrawablesToCurrentState();
    resetSubtreeAccessibilityStateChanged();
    // rebuild, since Outline not maintained while View is detached
    rebuildOutline();
    if (isFocused()) {
        InputMethodManager imm = InputMethodManager.peekInstance();
        if (imm != null) {
            imm.focusIn(this);
        }
    }
}

这个方法没有做什么特别的处理,因为一般都是在我们自定义View 的时候重写这个方法,当 View 添加到 Window 后就会调用该方法,我们可以在这个方法中进行一些设置,但是要记住这个View 还没有进行测量、摆放、绘制
 

小结:
当 DecorView 添加到 Window 后,进行测量、绘制前会调用 dispatchAttachedToWindow() 方法分发事件给子View ,告知子View 其已经被添加到了 Window ;如果子View 要在添加到 Window 后,进行测量、绘制前进行一些处理,但由于 dispatchAttachedToWindow() 方法是包内方法,不能重写,所以我们一般是重写 onAttachedToWindow() 方法,在这个方法中进行处理

Point 7:
获取 Window 窗体的宽高,源码如下:
★ ViewRootImpl

// 7、根据窗体的宽度mWidth以及窗体LayoutParams的宽度获取 View宽度测量规格
// 由上面可知Window的宽高测量模式都是 MATCH_PARENT
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
// 根据窗体的高度mHeight以及窗体LayoutParams的高度获取 View高度测量规格
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

这里会调用 getRootMeasureSpec() 方法对宽度和高度进行测量,由于测量的流程基本差不多,所以这里以宽度的测量流程进行分析
 
看到 getRootMeasureSpec() 方法,由于 Window 的 LayoutParams 的宽高都是 MATCH_PARENT,所以会返回一个大小为 Window 窗口的大小,模式为 MeasureSpec.EXACTLY 的一个 MeasureSpec
★ ViewRootImpl # getRootMeasureSpec()

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
        // 由于 Window的默认宽高测量模式都是MATCH_PARENT,所以会走这块逻辑
        case ViewGroup.LayoutParams.MATCH_PARENT:
            // 窗口不能调整大小,强制跟布局为窗口大小
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // 窗口可以调整大小,为跟布局设置最大值
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // 窗口会有一个确认的值,强制根布局为这个值大小
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
    }
    return measureSpec;
}



三、View绘制的三大方法分析

关于View绘制的三大方法的详细可以阅读文章 《View的绘制流程之四:View绘制的三个方法分析》



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值