Android View绘制1 performTraversals流程

一 概述

我们在Android窗口管理5 理解ViewRootImpl中已经简单介绍过 performTraversals 方法,接下来我们展开进行深入分析。

我们知道整个 Activity 显示出来之前需要经过 View 的绘制,首先,我们先来看一下 Activity 中 Window 的几个区域:
在这里插入图片描述
在这里插入图片描述
Overscan(过扫描区)

overscan 是电视机特有的概念,上图中黄色的区域就是 overscan 区域,指的是电视机四周一圈的黑色区域,成为overscan (过扫描) 区域,这个区域也是显示屏的一部分,但通常不能显示。如果窗口的某些内容画在这个区域里,它在某些电视上就会看不到。为了避免这种情况发生,通常要求 UI 不要画在屏幕的边角上,而是预留一定的空间。因为 Overscan 的区域大小随着电视不同而不同,它一般由终端用户通过 UI 指定,(比如说 GoogleTV 里就有确定 Overscan 大小的应用,更多关于 Overscan 的内容涉及到早起电视机的显示缺陷问题,对这方法面比较感兴趣的读者可以自行搜索)。

OverscanScreen,Screen
OverscanScreen 是包含 Overscan 区域的屏幕大小,而 Screen 则为去除 Overscan 区域后的屏幕区域,OverscanScreen > Screen。

Restricted and Unrestricted
有些区域是被系统保留的,比如说手机屏幕上方的状态栏和下方的导航栏,根据是否包括这些预留的区域,Android 把区域分为 Unrestricted Area 和 Resctrited Aread,前者包括这部分预留区域,后者则不包含,Unrestricted area > Rectricted area。

RestrictedOverScanScreen
包括 overscan 区,不包含导航栏、因此这个区域上面到屏幕的顶部,下面就到导航栏的顶部。
  
RestrictedScreen
这个区域不包含 overscan 区域不包含导航栏。
  
UnRestrictedScreen
不包含屏幕的 overscan 区域,包含状态栏和导航栏。
  
stableFullScreen
包含状态栏、输入法、不包含导航栏。
  
Decor区域
不包含状态栏、不包含导航栏、包含输入法区域。
  
Curren
不包含状态栏、不包含导航栏、不包含输入法区域。
  
Insects
insets “边衬” 的定义如上图所示,其本质就是一块屏幕区域的边衬。图二中的示意:

假如 content 区域周围有一圈类似于状态栏的东西,此时整个界面就是 content 内容区+一圈状态栏,那么这一圈状态栏就叫做“内容区边衬 (content insects) ";同理我们还可以得到”可见区边衬(visible insects)“。

在大多数情况下,Activity 窗口的内容区域和可见区域的大小是一致的,而状态栏和输入法窗口所占用的区域又称为屏幕装饰区 (decorations)。WMS 服务实际上就是需要根据屏幕以及可能出现的状态栏和输入法窗口的大小来计算出 Activity 窗口的整体大小及其过扫描区域边衬和可见区域边衬的大小。

PhoneWindowManager 一共定义了 10 种区域,剩下的 system 区域、Stable 区域、Content 区域的范围和 Decor 区域相同。这些区域在不同场合含义不同,但是值是相同的。

需要注意的是,overscan 区域和智能手机屏幕上的"大黑边"不是一会事,前者是软件层面的问题,后者是硬件层面问题,这里不做过多解释。

理解了上述的区域划分之后,我们正式开始本文的分析。此篇文章我们重点分析 performTraversals() 方法,因为这个方法是三大流程的开端,在它其中做了很多的事情。

二 ViewRootImpl.performTraversals

ViewRootImpl 类中的 performTraversals() 方法,这个方法大约有 800 行代码,我们分段来分析,这里我们尽量省略了一些不相关的代码。

阶段一:计算窗口期望尺寸

    private void performTraversals() {
        //mView就是DecorView根布局,记录ViewRootImpl管理
        //的View树的根节点,通过setView方法传进来的
        final View host = mView;    
        //mAdded指DecorView是否被成功加入到window中,在setView()中被赋值为true
        if (host == null || !mAdded)    
            return;

        mIsInTraversal = true;      //是否正在遍历
        mWillDrawSoon = true;       //是否马上绘制View
        boolean windowSizeMayChange = false; //视图的大小可能改变
        boolean surfaceChanged = false;     //界面改变
        WindowManager.LayoutParams lp = mWindowAttributes;

        int desiredWindowWidth;//顶层视图DecorView窗口的期望宽高
        int desiredWindowHeight;
        //DecorView视图是否可见
        final int viewVisibility = getHostVisibility();
        //视图可见性改变
        final boolean viewVisibilityChanged = !mFirst
                && (mViewVisibility != viewVisibility ||
                mNewSurfaceNeeded
                // Also check for possible double visibility update,
                // which will make current viewVisibility value 
                // equal to mViewVisibility and we may miss it.
                || mAppVisibilityChanged);
        ......
        //用来保存窗口宽度和高度,来自于全局变量mWinFrame
        //这个mWinFrame保存了窗口最新尺寸
        Rect frame = mWinFrame; 
        //在构造方法中mFirst已经设置为true,表示是否是第一次
        //被请求traversals,在后面的代码中被赋值false
        if (mFirst) {
            mFullRedrawNeeded = true; //是否需要全部重绘
            mLayoutRequested = true; //是否要求重新Layout界面
            
            final Configuration config =
                mContext.getResources().getConfiguration();
            // 初始化期望窗口长宽
            if (shouldUseDisplaySize(lp)) {//如果窗口的类型是状态栏,输入法等
                // NOTE -- system code, won't try to do compat mode.
                Point size = new Point();
                //设置窗口尺寸为屏幕真实尺寸,不剪去任何装饰的尺寸
                mDisplay.getRealSize(size);
                desiredWindowWidth = size.x;
                desiredWindowHeight = size.y;
            } else {
                desiredWindowWidth = mWinFrame.width();
                desiredWindowHeight = mWinFrame.height();
            }
            ......
        } else {
            //除了第一次被请求执行traversals以外,它的当前宽度
            //和高度就等于成员变量mWinFrame/frame中的宽度和高度
            desiredWindowWidth = frame.width();
            desiredWindowHeight = frame.height();
           
            if (desiredWindowWidth != mWidth ||
                desiredWindowHeight != mHeight) {
                //mWidth此时代表的是上一次执行该方法的时候的frame.width()值
                //如果此时两值不相等,那就说明视图改变需要重新测量绘制了。
                mFullRedrawNeeded = true;   //需要重新绘制标志位
                mLayoutRequested = true;    //要求重新Layout标志位
                windowSizeMayChange = true; //Window的尺寸可能改变
            }
        }

这段代码中主要交代了 Activity 当前的 Window 的宽高是如何得出的。

本段代码结尾的地方,关于 desiredWindowWidth 和 mWidth 的比较,我们需要介绍一下:首先这个 mWidth 和 mHeight 也是用来描述 Activity 窗口当前宽度和高度的,与 desiredWindowWidth 和 desiredWindowHeight 的区别是它们的值是由应用程序进程上一次主动请求 WMS 服务计算得到的,并且会一直保持不变到应用程序进程下一次再次请求 WMS 服务来重新计算为止 (这个在下文的代码中也有所体现)。

如果 Activity 窗口不是第一次被请求执行 traversals,并且 Activity 窗口上一次请求 WMS 服务计算得到的宽度 mWidth 和高度 mHeight 不等于 Activity 窗口的当前宽度 desiredWindowWidth 和当前高度 desiredWindowHeight,那么就说明 Activity 窗口的大小发生了变化,这时候变量 windowSizeMayChange 的值就会被标记为 true,以便接下来可以对 Activity 窗口的大小变化进行处理。
  
阶段二:再计算窗口期望尺寸并开始测量流程

        ......
        boolean insetsChanged = false;

        boolean layoutRequested = mLayoutRequested &&
            (!mStopped || mReportNextDraw);
        if (layoutRequested) {
            final Resources res = mView.getContext().getResources();
            if (mFirst) {
                // make sure touch mode code executes by setting cached value
                // to opposite of the added touch mode.
                mAttachInfo.mInTouchMode = !mAddedTouchMode;
                ensureTouchModeLocally(mAddedTouchMode);
            } else {
                /**
                 * 判断一下几个insects的值和上一次相比有没有什么变化,不同的话就改变insetsChanged
                 * mOverscanInsets 记录屏幕中的 overscan 区域               见贴图中相应描述区域
                 * mContentInsets  记录了屏幕中的控件在布局时必须预留的空间   见贴图中相应描述区域
                 * mStableInsets   记录了Stable区域,比mContentInsets区域大  见贴图中相应描述区域
                 * mVisibleInsets  记录了被遮挡的区域,如正在进行输入的TextView等不被遮挡,这样
                 *                 VisibleInsets的变化并不会导致重新布局,
                 *                 所以这里仅仅是将VisibleInsets保存到mAttachInfo中,以便绘制时使用
                 */
                if (!mPendingOverscanInsets.equals(mAttachInfo.mOverscanInsets)) {
                    insetsChanged = true;
                }
                if (!mPendingContentInsets.equals(mAttachInfo.mContentInsets)) {
                    insetsChanged = true;
                }
                if (!mPendingStableInsets.equals(mAttachInfo.mStableInsets)) {
                    insetsChanged = true;
                }
                if (!mPendingVisibleInsets.equals(mAttachInfo.mVisibleInsets)) {
                    mAttachInfo.mVisibleInsets.set(mPendingVisibleInsets);
                    if (DEBUG_LAYOUT) Log.v(TAG, "Visible insets changing to: "
                            + mAttachInfo.mVisibleInsets);
                }
                if (!mPendingOutsets.equals(mAttachInfo.mOutsets)) {
                    insetsChanged = true;
                }
                if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
                        || lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
                    windowSizeMayChange = true;

                    if (shouldUseDisplaySize(lp)) {
                        // NOTE -- system code, won't try to do compat mode.
                        Point size = new Point();
                        mDisplay.getRealSize(size);
                        desiredWindowWidth = size.x;
                        desiredWindowHeight = size.y;
                    } else {
                        Configuration config = res.getConfiguration();
                        desiredWindowWidth = dipToPx(config.screenWidthDp);
                        desiredWindowHeight = dipToPx(config.screenHeightDp);
                    }
                }
            }

            // Ask host how big it wants to be
            windowSizeMayChange |= measureHierarchy(host, lp, res,
                    desiredWindowWidth, desiredWindowHeight);
        }

首先我们来看看这段代码中出现的变量 mAttachInfo 实际上是 View 类中的静态内部类 AttachInfo 类的对象。

View.java

    /**
     * A set of information given to a view when it is attached to its parent
     * window.
     */
    final static class AttachInfo {
        /**
         * For windows that are full-screen but using insets to layout inside
         * of the screen areas, these are the current insets to appear inside
         * the overscan area of the display.
         */
        //下面三个变量的注释基本类似,就不贴了
        final Rect mOverscanInsets = new Rect();
        final Rect mContentInsets = new Rect();
        final Rect mVisibleInsets = new Rect();
        final Rect mStableInsets = new Rect();
        ......
    }

可以看到这个是 View 类的一个内部类 AttachInfo 类的对象,从注释可以看出该类的主要作用就是储存一组当 View attach 到它的父 Window 的时候 Activity 各种属性的信息。

我们再来看上一段代码中所涉及到的该类的其它几个变量:

mOverscanInsets,mContentInsets,mStableInsets,mVisibleInsets,根据注释我们可以对 mOverscanInsets 所表示的意义这样理解:这几个变量都是"对于全屏状态下的 Window,但是利用”边衬“来布局屏幕内部,这些是当前出现在屏幕上’过扫描区域’内部的’边衬’的变量(这里 ”insets“ 翻译为 “边衬比较恰当”),关于 overscan 和 insects 区域的划分,文章开头的时候已经做了说明,因此不难理解 mOverscanInsets 这个变量的意义。

    /**
     * @hide Compute the insets that should be consumed by this view and the ones
     * that should propagate to those under it.
     */
    protected boolean computeFitSystemWindows(Rect inoutInsets, Rect outLocalInsets) {
        if ((mViewFlags & OPTIONAL_FITS_SYSTEM_WINDOWS) == 0
                || mAttachInfo == null
                || ((mAttachInfo.mSystemUiVisibility & SYSTEM_UI_LAYOUT_FLAGS) == 0
                        && !mAttachInfo.mOverscanRequested)) {
            outLocalInsets.set(inoutInsets);
            inoutInsets.set(0, 0, 0, 0);
            return true;
        } else {
            // The application wants to take care of fitting system window for
            // the content...  however we still need to take care of any overscan here.
            final Rect overscan = mAttachInfo.mOverscanInsets;
            outLocalInsets.set(overscan);
            inoutInsets.left -= overscan.left;
            inoutInsets.top -= overscan.top;
            inoutInsets.right -= overscan.right;
            inoutInsets.bottom -= overscan.bottom;
            return false;
        }
    }

并且我们可以看到这句无奈的注释:

// The application wants to take care of fitting system window for
// the content...  however we still need to take care of any overscan here.

我们再看 mPendingOverscanInsets,mPendingContentInsets,mPendingStableInsets,mPendingVisibleInsets 四个变量,它们和上面四个变量标示的意义是一样的,只不过它们是由WindowManagerService 服务主动请求 Activity 窗口设置的,但是尚未生效。说的更通俗一点,这个 mOverscanInsets 是上一次执行 performTraversals() 函数时保存在 mAttachInfo 中的值,而 mPendingOverscanInsets 是这一次请求该函数时还没生效的值,两个值一比较,如果不相等,说明 insets 的值发生了改变 (insetsChanged = true)。

回到第二段代码中,我们注意到那段代码中的 if 代码块,如果 Activity 窗口是第一次被测量,布局和绘制,那么 mAttachInfo.mInTouchMode = !mAddedTouchMode;这里的 mInTouchMode 是 View 内部类 AttachInfo 类的成员变量,作用是 “指示 View 所处的 Window 当前是否处于触摸模式”。

而紧接着 ensureTouchModeLocally(mAddedTouchMode);则是 “确保这个 Window 的触摸模式已经被设置”,并且在这个方法的注释中有对传入参数 mAddedTouchMode 的解释:“我们是否想处于触摸模式”,这个方法的返回值是——进入触摸模式,ture;离开触摸模式,flase。但由于在上一段代码中这个方法的返回值并没有进行局部变量赋值等进一步处理,所以可以暂时先不管这个方法。总之,如果 Activity 窗口是第一次被测量,布局和绘制这种情况下,只是对于是否处于触摸模式情况的一个判断,除了改变了 AttachInfo 实体类中相关信息以外,并没有做进一步处理。

接着是 else 中的情况,也就是 Activity 窗口不是第一次被测量,布局和绘制,即 mFrist 成员变量中的值为 false。此时先判断一下几个 insects 的值和上一次相比有没有什么变化,不想等的话就将 insetsChanged 标志位变为 ture。

接着判断 Activity 的窗口情况,如果 Activity 窗口的宽度被设置为 WRAP_CONTENT 或者高度被设置为 WRAP_CONTENT,那么就意味着 Activity 窗口的大小要等于内容区域的大小,但是由于 Activity 窗口的大小是需要覆盖整个屏幕的,因此,这时候系统就会将 Activity 窗口的当前宽度 desiredWindowWidth 和当前高度 desiredWindowHeight 设置为屏幕的宽度和高度。也就是说,如果我们将 Activity 窗口的宽度和高度设置为 WRAP_CONTENT,实际上这个设置是无效的,它最终的宽度和高度还是会等于屏幕的宽度和高度。这种情况也意味着 Acitivity 窗口的大小发生了变化 (从设置的 WRAP_CONTENT 变为屏幕大小),因此将 windowSizeMayChange 设为 true。紧接着是一个窗口类型的判断,如果窗口的类型是有状态栏的,那么 Activity 窗口的宽度和高度就是屏幕的宽高除了状态栏,否则就是整个屏幕的大小。

最后一句代码:windowSizeMayChange |= measureHierarchy(host, lp, res,desiredWindowWidth, desiredWindowHeight);,measureHierarchy() 这个方法中实际上进行了 measure() 测量过程,只不过这个测量过程不属于三大流程,而是为了确定 Window 的大小而打的辅助,只不过最终返回的仍然是 widow 的 size 是否改变的 boolean 值。

阶段三:

    ......
    if (layoutRequested) {
        // Clear this now, so that if anything requests a layout in the
        // rest of this function we will catch it and re-run a full
        // layout pass.                     --注释写的很清楚了吧?
        mLayoutRequested = false;
    }

    boolean windowShouldResize = layoutRequested && windowSizeMayChange
        && ((mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight())
            || (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT &&
                    frame.width() < desiredWindowWidth && frame.width() != mWidth)
            || (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT &&
                    frame.height() < desiredWindowHeight && frame.height() != mHeight));
    windowShouldResize |= mDragResizing && mResizeMode == RESIZE_MODE_FREEFORM;
    
   // If the activity was just relaunched, it might have unfrozen the task bounds (while
    // relaunching), so we need to force a call into window manager to pick up the latest
    // bounds.
    windowShouldResize |= mActivityRelaunched;
        
    // Determine whether to compute insets.
    // If there are no inset listeners remaining then we may still need to compute
    // insets in case the old insets were non-empty and must be reset.
    final boolean computesInternalInsets =
            mAttachInfo.mTreeObserver.hasComputeInternalInsetsListeners()
            || mAttachInfo.mHasNonEmptyGivenInternalInsets;

第一个 if 语段注释已经写的很清楚了,我们主要看后面两段,这两段代码做了以下两件事情:

1.检查是否需要处理 Activity 窗口的大小变化事件,如果同时满足下面三个条件,就需要处理,将 windowShouldResize 标志位变为 true。

  • layoutRequested 等于 true,说明程序已经发起 (正在执行) 了一次测量,布局,绘制流程
  • windowSizeMayChange 等于 true,说明前面已经检测到了 Activity 窗口的变化
  • 上一段代码的最后一句中,我们说过已经做了一次 measure() 工作,如果测量出来的宽度和高度和 Activity 窗口的当前宽度 mWidth 和高度 mHeight 一样,那么即使条件1和条件2能满足,那么也是可以认为是 Activity 窗口的大小是没有发生变化的。

换句话说,只有当测量出来的大小和当前大小不一致时,才认为 Activity 窗口大小发生了变化。另一方面,如果测量出来的大小和当前大小一致,但是 Activity 窗口的大小被要求设置成 WRAP_CONTENT,即设置成和屏幕的宽度 desiredWindowWidth 和高度 desiredWindowHeight 一致,但是 WindowManagerService 服务请求 Activity 窗口设置的宽度 frame.width() 和高度 frame.height() 与它们不一致,而且与 Activity 窗口上一次请求 WindowManagerService 服务计算的宽度 mWidth 和高度 mHeight 也不一致,那么也是认为 Activity 窗口大小发生了变化。

2.检查 Activity 窗口是否需要指定有额外的内容边衬区域和可见边衬区域。如果有的话,那么变量 attachInfo 所指向的一个 AttachInfo 对象的成员变量 mTreeObserver 所描述的一个 TreeObserver 对象的成员函数 hasComputeInternalInsetsListerner 的返回值 ComputeInternalInsets 就会等于 true。Activity 窗口指定额外的内容边衬区域和可见边衬区域是为了放置一些额外的东西。
(该段摘自老罗的文章,解释的相当清楚没有什么多说得)

阶段四:

    if (mFirst || windowShouldResize || insetsChanged ||
                viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
            mForceNextWindowRelayout = false;

            if (isViewVisible) {
                // If this window is giving internal insets to the window
                // manager, and it is being added or changing its visibility,
                // then we want to first give the window manager "fake"
                // insets to cause it to effectively ignore the content of
                // the window during layout.  This avoids it briefly causing
                // other windows to resize/move based on the raw frame of the
                // window, waiting until we can finish laying out this window
                // and get back to the window manager with the ultimately
                // computed insets.
                insetsPending = computesInternalInsets && (mFirst || viewVisibilityChanged);
            }
            ......

这段代码以及接下来的两段代码都是在满足下面的条件之一的情况下执行的 (都在一个 if() 里边):

  • Activity 窗口是第一次执行测量、布局和绘制操作,即 ViewRootImpl 类的成员变量 mFirst 的值等于 true
  • 前面得到的变量 windowShouldResize 的值等于 true,即 Activity 窗口的大小的确是发生了变化
  • 前面得到的变量 insetsChanged 的值等于 true,即 Activity 窗口的内容区域边衬发生了变化
  • Activity 窗口的可见性发生了变化,即变量 viewVisibilityChanged 的值等于 true
  • Activity 窗口的属性发生了变化,即变量 params 指向了一个 WindowManager.LayoutParams 对象
  • mForceNextWindowRelayout 为 true

在满足上述条件之一,并且 Activity 窗口处于可见状态,那么就需要检查接下来请求 WindowManagerService 服务计算大小时,是否要告诉 WindowManagerService 服务它指定了额外的内容区域边衬和可见区域边衬,但是这些额外的内容区域边衬和可见区域边衬又还有确定。这种情况发生在 Activity 窗口第一次执行测量、布局和绘制操作或者由不可见变化可见时。因此,当前面得到的变量 computesInternalInsets 等于 true 时,即 Activity 窗口指定了额外的内容区域边衬和可见区域边衬,那么就需要检查 mFirst 或者变量 viewVisibilityChanged 的值是否等于 true。如果这些条件能满足,那么变量 insetsPending 的值就会等于 true,表示 Activity 窗口有额外的过扫描区域边衬和可见区域边衬等待指定。

阶段五:

    try {
        ......
        relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
        ......
        final boolean overscanInsetsChanged = !mPendingOverscanInsets.equals(
                        mAttachInfo.mOverscanInsets);
        contentInsetsChanged = !mPendingContentInsets.equals(
                mAttachInfo.mContentInsets);
        final boolean visibleInsetsChanged = !mPendingVisibleInsets.equals(
                mAttachInfo.mVisibleInsets);
        final boolean stableInsetsChanged = !mPendingStableInsets.equals(
                mAttachInfo.mStableInsets);
        final boolean outsetsChanged = !mPendingOutsets.equals(mAttachInfo.mOutsets);
        if (contentInsetsChanged) {
            ......
            mAttachInfo.mContentInsets.set(mPendingContentInsets);
         }
     } catch (RemoteException e) {
     }
     ......
     mAttachInfo.mWindowLeft = frame.left;
     mAttachInfo.mWindowTop = frame.top;

     // !!FIXME!! This next section handles the case where we did not get the
     // window size we asked for. We should avoid this by getting a maximum size from
     // the window session beforehand.
     if (mWidth != frame.width() || mHeight != frame.height()) {
         mWidth = frame.width();//mWidth和mHeight也是用来描述Activity窗口当前宽度和高度的
         mHeight = frame.height(); //与desiredWindowWidth和desiredWindowHeight不同,它们的值是
         //由应用程序进程上一次主动请求WMS服务计算得到的,并且会一直保持不变到应用程序进
         //程下一次再请求WindowManagerService服务来重新计算为止
     }

这段代码主要就是调用 relayoutWindow() 来请求 WindowManagerService 服务计算 Activity 窗口的大小以及过扫描区域边衬大小和可见区域边衬大小。计算完毕之后,Activity 窗口的大小就会保存在成员变量 mWinFrame 中,而 Activity 窗口的内容区域边衬大小和可见区域边衬大小分别保存在 ViewRootImpl 类的成员变量 mPendingOverscanInsets 和 mPendingVisibleInsets 中。

如果这次计算得到的 Activity 窗口的内容区域边衬大小 mPendingContentInsets 和可见区域边衬大小 mPendingVisibleInsets 与上一次计算得到的不一致,即 mAttachInfo 所指向的一个 AttachInfo 对象的成员变量 mOverscanInsets 和 mVisibleInsets 所描述的大小不一致,那么变量 overscanInsetsChanged 和 visibleInsetsChanged 的值就会等于 true,表示 Activity 窗口的内容过扫描边衬大小和可见区域边衬大小发生了变化。(虽然这段代码中一共提到了四个变量,但是上文我们说过,只有 mOverscanInsets 和 mVisibleInsets 是用到的)。

由于变量 frame 和 mWinFrame 引用的是同一个 Rect 对象,因此,这时候变量 frame 描述的也是 Activity 窗口请求 WMS 服务计算之后得到的大小。这段代码分别将计算得到的 Activity 窗口的左上角坐标保存在变量 attachInfo 所指向的一个 AttachInfo 对象的成员变量 mWindowLeft 和 mWindowTop 中,并且将计算得到的 Activity 窗口的宽度和高度保存在 mWidth 和 mHeight 中。
  
阶段六:

    ......
    //mStopped的注释:Set to true if the owner of this window is in the stopped state.
    //如果此窗口的所有者(Activity)处于停止状态,则为ture.
    if (!mStopped || mReportNextDraw) {//mReportNextDraw Window上报下一次绘制.
        boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
                (relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
        if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
                || mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
                updatedConfiguration) {
            //mWidth != host.getMeasuredWidth() 表示frame的宽不等于初始DecorView宽.
            //getMeasuredWidth()方法可以获取View测量后的宽高,host上面说过为DecorView根布局.
            //获得view宽高的测量规格,lp.width和lp.height表示DecorView根布局宽和高
            int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
            int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

            // Ask host how big it wants to be
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);//开始执行测量操作
            //getMeasuredWidth()所获得View的宽高绝大部分情况下等于View最终的宽高
            int width = host.getMeasuredWidth();
            int height = host.getMeasuredHeight();
            boolean measureAgain = false;

            //lp.horizontalWeight表示将多少额外空间水平地(在水平方向上)分配给与这些LayoutParam
            //关联的视图。如果视图不应被拉伸,请指定0。否则,将在所有权重大于0的视图中分配额外的像素。
            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;
        }
    }

这段代码用来检查是否需要重新测量 Activity 窗口的大小。如果满足以下条件之一,那么就需要重新测量:

  • Activity 窗口的触摸模式发生了变化,并且由此引发了 Activity 窗口当前获得焦点的控件发生了变化,即变量 focusChangedDueToTouchMode 的值等于 true。这个检查是通过调用 ensureTouchModeLocally 来实现的
  • Activity 窗口前面测量出来的宽度和高度不等于 WMS 服务计算出来的宽度 mWidth 和高度 mHeight
  • Activity 窗口的内容区域边衬大小和可见区域边衬大小发生了变化,即前面得到的变量 contentInsetsChanged 的值等于 true。

重新计算一次之后,如果 Activity 窗口的属性 lp 表明需要对测量出来的宽度 width 和高度 height 进行扩展,即变量 lp 的成员变量 horizontalWeight 和 verticalWeight 的值大于 0.0,那么就需要对 Activity 窗口的顶层视图 host 的最大可用空间进行扩展后再进行一次测量工作。

阶段七:

    } else {
        maybeHandleWindowMove(frame);       
    }



private void maybeHandleWindowMove(Rect frame) {
    final boolean windowMoved = mAttachInfo.mWindowLeft != frame.left
            || mAttachInfo.mWindowTop != frame.top;
    if (windowMoved) {//如果窗口移动
        if (mTranslator != null) {
            mTranslator.translateRectInScreenToAppWinFrame(frame);
        }
        mAttachInfo.mWindowLeft = frame.left;
        mAttachInfo.mWindowTop = frame.top;
    }
    if (windowMoved || mAttachInfo.mNeedsUpdateLightCenter) {
        // Update the light position for the new offsets.
        if (mAttachInfo.mThreadedRenderer != null) {
            mAttachInfo.mThreadedRenderer.setLightCenter(mAttachInfo);
        }
        mAttachInfo.mNeedsUpdateLightCenter = false;
    }
}

首先,这里的 else 接的是 “阶段四” 中的那个 if,是的,这个 if 就是这么长。其次,这段代码实际上就是判断 Window 有没有移动,如果发生移动则会调用 translateRectInScreenToAppWinFrame() 执行移动动画。

这个 mWindowLeft 和 mWindowTop 代表的分别是 View 所 attach 的 Window 的左坐标和顶坐标,回到上一个方法中,我们可以看到,其实在这个方法中,有这么两句:

    mAttachInfo.mWindowLeft = frame.left;
    mAttachInfo.mWindowTop = frame.top;

也就是说,给 windowMoved 赋值的那句代码,实际上就是将上一次调用改方法时 Window 的顶坐标和左坐标与这一次相比,从而得出 Window 是否 Moved。

阶段八:

        ......
        final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
        if (didLayout) {//layoutRequested测量要求标志位
            performLayout(lp, mWidth, mHeight); //开始执行Layout操作
            // By this point all views have been sized and positioned
            // We can compute the transparent area
            ......//省略计算透明区域的一大块代码boolean skipDraw = false;

        if (mFirst) {
        ......
        } else if (mWindowsAnimating) { //表示窗口动画正在进行中。
            //mRemainingFrameCount:How many frames the app is still allowed to draw when a window animation is happening
            if (mRemainingFrameCount <= 0) {
                skipDraw = true;    //跳过绘制,剩下允许的帧数为0了。
            }
            mRemainingFrameCount--;
        }

        mFirst = false;
        ......
        if (!cancelDraw && !newSurface) {   //既没有取消绘制,也没有创建新的平面。
            if (!skipDraw || mReportNextDraw) {     //没有跳过绘制或者已经上报进行下一次绘制
                ......
                performDraw();  //开始执行绘制操作  ③
            }
        } else {
            if (viewVisibility == View.VISIBLE) {
                // Try again
                scheduleTraversals();   //还记得这个方法是干什么的吧?就是重新来过的意思!不记得的话去看上一篇文章。
            } else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                for (int i = 0; i < mPendingTransitions.size(); ++i) {
                    mPendingTransitions.get(i).endChangingAnimations();
                }
                mPendingTransitions.clear();
            }
        }

        mIsInTraversal = false;
    }

在这段代码中,Activity的Window测量过程已经进行完毕,当满足layoutRequested && (!mStopped || mReportNextDraw);条件时,将进行布局(layout)过程。可以看到“第六段代码”和本段代码中,出现了View绘制的三大流程。

关于 View 绘制的三大流程以及 relayoutWindow() 中涉及到的 WindowManagerService 服务计算 Activity 窗口的大小的过程我们将在后续分析。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值