Activity的顯示之ViewRootImpl詳解

該文章是一個系列文章,是本人在Android開發的漫漫長途上的一點感想和記錄,若是能給各位看官帶來一絲啓發或者幫助,那真是極好的。android


前言

終於到了咱們的豬腳ViewRootImpl出場的時候了。ViewRootImpl類比較複雜,若是要把這個類所有解釋清楚那須要不少章節,而且該類涉及了許多其餘知識,如Android進程間通訊的Binder了,還有其餘許多本文以及前文沒有講到的概念。因此咱們只分析其中的一部分。ide

ViewRootImpl的構造函數

public ViewRootImpl(Context context, Display display) {
  ...
  /**① 從WindowManagerGlobal 中獲取一個IWindowSession的實例。
    *它是ViewRootImpl和WindowManagerService(如下簡稱WMS)進行通訊的代理
    */
  mWindowSession = WindowManagerGlobal.getWindowSession();
  //② FallbackEventHandler是一個處理未經任何人消費的輸入事件的場所。
  mFallbackEventHandler = new PhoneFallbackEventHandler(context);
 ...
}

注:函數

  1. 關於IWindowSession

    它是一個Binder對象,真正的實現類是Session,也就是說下文setView方法中關於它的操做實際上是一次IPC過程。關於IPC(進程間通訊)的方式,以及Android操做系統中最主要的IPC方式Binder會在之後的文章中介紹。工具

  2. 關於FallbackEventHandler

    關於FallbackEventHandler具體我會在下一章介紹。oop

ViewRootImpl的setView函數

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        if (mView == null) {
            //保存了控件的根
            mView = view;
            ...

            mFallbackEventHandler.setView(view);

            ...
            /**
            *① 在添加窗口以前,先經過requestLayout方法在主線程上安排一次「遍歷」。
            * 所謂「遍歷」是指ViewRootImpl中的核心方法performTraversal()。
            * 這個方法實現對控件樹進行測量、佈局、向WMS申請修改窗口屬性以及重繪的全部工做。
            */

            requestLayout();

            /**
            * ② 初始化mInputChanel。InputChannel是窗口接收來自InputDispatcher的輸入事件的管道
            */
            if ((mWindowAttributes.inputFeatures
                    & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
                mInputChannel = new InputChannel();
            }
                ...

            try {
            /**
            *上文剛講過mWindowSession是個Binder類,它的實現類是Session,
            *將經過IPC遠程調用(即調用另外一個進程中的)Session的addToDisplay方法把窗口添加進WMS中。
            *完成這個操做後,mWindow已經被添加到指定對象中並且mInputChannel(若是不爲空)已經準備好接收事件
            */
                res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                        getHostVisibility(), mDisplay.getDisplayId(),
                        mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                        mAttachInfo.mOutsets, mInputChannel);
            } catch (RemoteException e) {

            } finally {

            }

            ...
            
            // 錯誤處理。窗口添加失敗的緣由一般是是權限問題、重複添加或者token無效
            if (res < WindowManagerGlobal.ADD_OKAY) {
            }

            ...
            // ③ 若是mInputChannel不爲空,則建立mInputEventReceiver用於接收輸入事件。
            if (mInputChannel != null) {

                mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
                        Looper.myLooper());
            }

            ...

            view.assignParent(this);

            ...
        }
    }
}

接着咱們來一個個分析,先來最重要的,也是本章的最主要內容,另外兩個將會在下一章分析。佈局

requestLayout()

@Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

scheduleTraversals();函數聲明以下post

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

其中mTraversalRunnable的定義是這樣的this

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

doTraversal()函數聲明以下;spa

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }

        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}

注:如下文章屢次摘抄於張大偉老師的《深刻理解Android卷Ⅲ》,請支持原創,讀者也可去看張大偉老師的這本書籍
終於看到了咱們的豬腳performTraversals();,ViewRootImpl中接收的各類變化,如來自WMS的窗口屬性變化、來自控件樹的尺寸變化以及重繪請求等都引起performTraversals();的調用,並在其中完成處理。View類及其子類的onMeasure()、onLayout()、onDraw()等回調也都是在該方法執行的過程當中直接或間接的引起。該函數可謂是是ViewRootImpl的「心跳」。咱們就來看一下這個方法把。操作系統

performTraversals()

先上源碼:(注:源碼很長,具體的分析在下方)

private void performTraversals() {
    final View host = mView;
    /**
        第1階段 預測量
    */
    boolean windowSizeMayChange = false;
    boolean newSurface = false;
    boolean surfaceChanged = false;
    WindowManager.LayoutParams lp = mWindowAttributes;
    ......
    //聲明本階段的豬腳,這兩個變量將是mView的SPEC_SIZE份量的候選
    int desiredWindowWidth;
    int desiredWindowHeight;
   ......
    Rect frame = mWinFrame;
   ......
    if (mFirst) {
        mFullRedrawNeeded = true;
        mLayoutRequested = true;

        final Configuration config = mContext.getResources().getConfiguration();
        if (shouldUseDisplaySize(lp)) {
           //爲狀態欄設置desiredWindowWidth/height 其取值是屏幕尺寸
            Point size = new Point();
            mDisplay.getRealSize(size);
            desiredWindowWidth = size.x;
            desiredWindowHeight = size.y;
        } else {
        // ① 第1次「遍歷」的測量,採用了應用可使用的最大尺寸做爲SPEC_SIZE的候選
            desiredWindowWidth = dipToPx(config.screenWidthDp);
            desiredWindowHeight = dipToPx(config.screenHeightDp);
        }
        ......
    } else {
        // ② 在非第1次遍歷的狀況下,會採用窗口的最新尺寸做爲SPEC_SIZE的候選
        desiredWindowWidth = frame.width();
        desiredWindowHeight = frame.height();
        //若是窗口的最新尺寸與ViewRootImpl中的現有尺寸不一樣,說明WMS單方面改變了窗口的尺寸,將致使一下三個結果
        if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
        //須要完整的重繪以適應新的窗口尺寸
            mFullRedrawNeeded = true;
        //須要對控件樹從新佈局
            mLayoutRequested = true;
        //控件樹可能拒絕接受新的窗口尺寸,可能須要窗口在佈局階段嘗試設置新的窗口尺寸,,只是嘗試
            windowSizeMayChange = true;
        }
    }
    ......
    boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
    if (layoutRequested) {

        final Resources res = mView.getContext().getResources();

        if (mFirst) {
          ......
        } else {
            ......
            /**
            *檢查WMS是否單方面改變了一些參數,標記下來,而後做爲後文是否進行控件佈局的條件之一
            *若是窗口的width或height被指定爲WRAP_CONTENT時。表示該窗口爲懸浮窗口。
            */
            if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
                    || lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
                //懸浮窗口的尺寸取決於測量結果。所以有可能向WMS申請改變窗口的尺寸
                windowSizeMayChange = true;

                if (shouldUseDisplaySize(lp)) {
                   //同樣的設置狀態欄的desiredWindowWidth/height
                    Point size = new Point();
                    mDisplay.getRealSize(size);
                    desiredWindowWidth = size.x;
                    desiredWindowHeight = size.y;
                } else {
                // ③ 設置懸浮窗口的SPEC_SIZE的候選爲應用可使用的最大尺寸
                    Configuration config = res.getConfiguration();
                    desiredWindowWidth = dipToPx(config.screenWidthDp);
                    desiredWindowHeight = dipToPx(config.screenHeightDp);
                }
            }
        }

        // ④ 進行測量
        windowSizeMayChange |= measureHierarchy(host, lp, res,
                desiredWindowWidth, desiredWindowHeight);
       
    }
    ......
    
   
    if (layoutRequested) {
       
        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));
    
    ......
    
    /**
         第1階段 預測量到這裏結束
    */

    /**
         第2階段 窗口布局階段從這裏開始
    */
  if (/*進入窗口布局的幾個條件*/) {
        ......
         boolean hadSurface = mSurface.isValid();
         ......
          try {
              relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
              }catch(...){
              ......
              }finally{
              ......
              }
    /**
         第2階段 窗口布局階段到這裏結束。關於窗口布局的部分涉及太多,咱們不具體分析源碼,後文會有總結
    */
    /**
         第3階段 最終測量階段從這裏開始
    */
        if (!mStopped || mReportNextDraw) {
            ......
                int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
                //① 能夠看到與與測量中調用的performMeasure
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
             
                int width = host.getMeasuredWidth();
                int height = host.getMeasuredHeight();
                boolean measureAgain = false;
                //② 判斷LayoutParams.horizontalWeight和lp.verticalWeight ,以做爲是否再次測量的依據
                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) {
                    if (DEBUG_LAYOUT) Log.v(mTag,
                            "And hey let's measure once more: width=" + width
                            + " height=" + height);
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                }

                layoutRequested = true;
            }
        }
    } else {
      
    }
    /**
         第3階段 最終測量階段到這裏結束
    */
    /**
         第4階段 控件佈局階段從這裏開始
    */
    //① 佈局階段的判斷條件
    final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
  ......
    if (didLayout) {
        ......
        //② 經過performLayout對控件進行佈局
        performLayout(lp, mWidth, mHeight);

       ......
       //③ 若是有必要,計算窗口的透明區域並把該區域設置給WMS
        if ((host.mPrivateFlags & View.PFLAG_REQUEST_TRANSPARENT_REGIONS) != 0) {
            
            host.getLocationInWindow(mTmpLocation);
            mTransparentRegion.set(mTmpLocation[0], mTmpLocation[1],
                    mTmpLocation[0] + host.mRight - host.mLeft,
                    mTmpLocation[1] + host.mBottom - host.mTop);

            host.gatherTransparentRegion(mTransparentRegion);
            if (mTranslator != null) {
                mTranslator.translateRegionInWindowToScreen(mTransparentRegion);
            }

            if (!mTransparentRegion.equals(mPreviousTransparentRegion)) {
                mPreviousTransparentRegion.set(mTransparentRegion);
                mFullRedrawNeeded = true;
                
                try {
                    mWindowSession.setTransparentRegion(mWindow, mTransparentRegion);
                } catch (RemoteException e) {
                }
            }
        }

     
    /**
         第4階段 控件佈局階段到這裏結束
    */
    /**
         第5階段 繪製階段從這裏開始
    */
    ......
    boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;

    if (!cancelDraw && !newSurface) {
        if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
            for (int i = 0; i < mPendingTransitions.size(); ++i) {
                mPendingTransitions.get(i).startChangingAnimations();
            }
            mPendingTransitions.clear();
        }

        performDraw();
    } else {
        if (isViewVisible) {
            // 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;
}

因爲該方法是Android源代碼中最龐大的方法之一,因此咱們對其進行分階段分析。在源碼中有標註1,2,3,4,5,對每一階段再細分爲①②...,對照上文註釋

1. 預測量階段(PreMeasure)

這是進入performTraversals();的第一個階段。它會對控件樹進行第一次測量。在此階段中將會計算出控件樹爲顯示其內容所需的尺寸,即指望的窗口尺寸。在這個階段中View及其子類的onMeasure()方法將會沿着控件樹依次獲得回調。
預測量和測量原理

(1)預測量參數的候選(對應第1階段①②③)

預測量也是一次完整的測量過程,它與最終測量的區別僅在於參數不一樣而已。實際的測量工做是在View或其子類的onMeasure()方法中完成,而且其測量結果須要受限於來自其父控件的指示。這個指示由onMeasure()方法中的兩個參數進行傳達:widthSpec和heightSpec。它們是被稱爲MeasureSpec的複合整型變量,用於指導控件對自身進行測量。她又兩個份量,結構如圖

這裏寫圖片描述

由①、②、③可知預測量時的SPEC_SIZE按照以下原則進行取值:

  • 第一次「遍歷」時,使用可用的最大尺寸做爲SPEC_SIZE的候選
  • 此窗口是一個懸浮窗口時,即LayoutParams.width/height其中之一被指定爲WRAP_CONTENT時,使用可用的最大尺寸做爲SPEC_SIZE的候選
  • 其餘狀況下,使用窗口最新尺寸做爲SPEC_SIZE的候選

(2)測量協商(對應第1階段④)

在第1階段第④步時,咱們看到了measureHierarchy方法,該方法用於測量整個控件樹。傳入的參數desiredWindowWidth,desiredWindowHeight在前述代碼中作了精心的挑選。控件樹本能夠按照這兩個參數完成測量,可是measureHierarchy有本身的考量,即如何將窗口布局的儘量優雅。measureHierarchy如何作到這一步呢,經過跟控件樹的協商。可是協商只發生在LayoutParams.width被指定爲WRAP_CONTENT時,若是LayoutParams.width被指定爲MATCH_PARENT或者固定數值時。該協商過程不會發生。

咱們來看一下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;//表示側臉是否能知足控件樹充分顯示內容的要求
    if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
       /**
       ① 第一次協商 measureHierarchy使用它指望的寬度限制進行測量,
       */
       final DisplayMetrics packageMetrics = res.getDisplayMetrics();
       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);
       }
       //若是寬度限制不爲0而且傳入的desiredWindowWidth 大於measureHierarchy指望的限制寬度,
       if (baseSize != 0 && desiredWindowWidth > baseSize) {
           childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
           childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
           //② 第一次測量 使用measureHierarchy指望的限制寬度 並獲得狀態
           performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
           //判斷狀態
           if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
               goodMeasure = true;//控件樹對測量結果滿意
           } else {
              //③ 控件樹對測量結果不滿意,進行第二次協商,此次把限制寬度放大爲指望寬度baseSize和最大寬度desiredWindowWidth和的一半
             baseSize = (baseSize+desiredWindowWidth)/2;
             
              childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
              //④ 第2次測量
              performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
             
            if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                  goodMeasure = true;
              }
           }
       }
    }
    //若是兩次協商測量均不能讓控件樹滿意,那麼measureHierarchy再也不對寬度進行限制,使用最大寬度進行測量
     if (!goodMeasure) {
         childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
         childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
         
         performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
         //若是測量獲得的寬度或者高度與ViewRootImpl中的窗口不一致,,那麼以後可能要改變窗口的尺寸了
         if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
             windowSizeMayChange = true;
         }
     }
    
     return windowSizeMayChange;
}

咱們再來看performMeasure方法,performMeasure方法的實現很是簡單,它直接調用了mView.measure的方法

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    if (mView == null) {
        return;
    }
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    try {
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

終於到了View的measure方法,在該方法內部會調用咱們熟悉的onMeasure方法,咱們來看View.measure方法的實現

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    .......//初始化操做
    if (forceLayout || needsLayout) {
          //① 準備工做
         mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
         .......
          //② 對本控價進行測量
         onMeasure(widthMeasureSpec, heightMeasureSpec);
        .......
        //③ 檢查onMeasure的實現是否調用了setMeasuredDimension()
        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()");
         }
        //④ 將PFLAG_LAYOUT_REQUIRED加入mPrivateFlags ,這一操做會對以後的佈局操做放行
         mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
     }
    
     mOldWidthMeasureSpec = widthMeasureSpec;
     mOldHeightMeasureSpec = heightMeasureSpec;

}

(3)肯定是否須要改變窗口尺寸(對應第1階段⑤)

前文屢次設置了windowSizeMayChange 爲true,可是windowSizeMayChange 爲true滿是窗口是否須要改變尺寸的條件之一,咱們來看第1階段⑤對應代碼。

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 的判斷較爲複雜,咱們來總結一下

必要條件:

layoutRequested爲true。表示ViewRootImpl的requestLayout方法被調用過。

在View中也有requestLayout方法。當控件內容發生變化從而須要調整其尺寸時,會調用自身的requestLayout(),而且此方法會沿着控件樹向根部回溯,最終調用到ViewRootImpl的requestLayout,從而引起一次performTraversals()調用。

之因此這是一個必要條件,是由於performTraversals()還有可能由於重繪時調用,當控件僅須要重繪而不須要從新佈局時(例如背景色或者前景色發生變化時)。會經過invalidate()方法回溯到ViewRootImpl,此時不會經過requestLayout觸發performTraversals()調用,而是經過scheduleTraversals()方法進行觸發。這種狀況下不須要進行佈局窗口階段

windowSizeMayChange爲true,該變量前文中已有詳細描述。

在上述條件知足的條件下,如下條件知足其一即觸發布局窗口階段

  1. 測量結果與ViewRootImpl中所保存的當前尺寸有差別
  2. 懸浮窗口的測量結果與窗口的最新尺寸有差別

2. 佈局窗口階段(WindowLayout)

根據預測量的結果,經過IWindowSession.relayout()方法向WMS請求調整窗口的尺寸等屬性,這將引起WMS對窗口從新佈局,並將佈局結果返回給ViewRootImpl.

總結:佈局窗口得以進行的緣由是控件系統有修改窗口屬性的需求,如第一次「遍歷」須要肯定窗口的尺寸以及一塊Surface,預測量結果與窗口當前尺寸不一致須要進行窗口尺寸更改,mView可見性發生變化須要將窗口隱藏或顯示等。

3. 最終測量階段(EndMeasure)

預測量的結果是控件樹所指望的窗口尺寸。然而因爲在WMS中影響佈局的因素不少,WMS不必定會將窗口的準確的佈局爲控件樹所要求的尺寸,而迫於WMS做爲系統服務的強勢地位,控件樹不得不接受WMS的佈局結果。在這個階段中View及其子類的onMeasure()方法將會沿着控件樹依次被回調。最終測量階段直接調用performMeasure而不是measureHierarchy,是由於measureHierarchy有個協商過程,而到了最終測量階段控件樹已經沒有了協商的餘地,不管控件樹樂意與否,他只能被迫接受WMS的佈局結果

4. 佈局控件樹階段(Layout)

將上一步完成的最終測量的結果做爲依據進行佈局。測量肯定的是控件的尺寸,而佈局肯定的是控件的位置。在這個階段中View及其子類的onLayout()方法將會被回調。

整體來講4. 佈局控件樹階段(Layout)作了兩件事。

進行控件樹佈局

調用了performLayout函數,雖然咱們還沒看到該函數,但猜想想必和performMeasure差很少。咱們來看一下

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
        int desiredWindowHeight) {
  ......
    try {
    //同樣是調用View.layout函數
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

     ......

    } finally {
        
    }
    ......

}

咱們再來看View.layout。佈局階段把測量結果轉化爲控件的實際位置與尺寸。而控件的實際位置與尺寸由Veiw的mLeft、mTop、mRight、mBottom 這4個成員變量存儲的座標值。即控件樹的佈局過程就是根據測量結果爲每個控件設置這4個成員變量的過程。其中mLeft、mTop、mRight、mBottom 是相對於父控件的座標值。

public void layout(int l, int t, int r, int b) {
    //若是設置了PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT標誌位,那麼在佈局以前先進行測量,調用onMeasure函數
    if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
        onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }
    //保存原始座標
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;
    //設置新座標
    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    //應該還記得上文View.measure方法中的最後設置了PFLAG_LAYOUT_REQUIRED吧
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
    //調用onLayout方法。若是該Vie是個ViewGroup。onLayout中須要依次調用子控件的layout方法
        onLayout(changed, l, t, r, b);
    ......
    //清除PFLAG_LAYOUT_REQUIRED標記
        mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
    //通知每個對此控件佈局變化有興趣的Listener
        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);
            }
        }
    }
    ......
    }
}

咱們來對比測量和佈局階段以便更好的理解

  • 測量肯定的是控件的尺寸,並在必定程度上肯定了子控件的位置。而佈局是針對測量結果來實施,並最終肯定子控件的位置
  • 測量結果對佈局過程並無約束力。雖然說子控件在onMeasure方法中計算出了本身應有的尺寸,可是因爲layout方法是由父控件調用,所以控件的位置尺寸的最終決定權在父控件手中,測量結果僅僅是一個參考。
  • 通常來講,子控件的測量結果影響父控件的測量結果,所以測量過程是後根遍歷。而父控件的佈局結果影響子控件的佈局結果。因此佈局過程是先根遍歷

設置透明區域

佈局階段的另外一個工做是計算並設置窗口的透明區域。這一功能主要是爲SurfaceView服務。關於SurfaceView的相關知識咱們後文介紹

5. 繪製階段(Draw)

這是performTraversals();的最後階段。肯定控件的尺寸和位置後。便進行對控件樹的繪製。在這個階段中View及其子類的onDraw()方法將會被回調。

咱們在開發Android自定義控件時,每每都須要重寫View.onDraw()方法以繪製內容到一個給定的Canvas中。
 咱們來看一下Canvas。Canvas是一個繪圖工具類,其API提供了一系列繪圖指定供開發者使用。這些指令能夠分爲兩個部分:
  • 繪製指令。這些最經常使用的指令由一系列名爲drawXXX()的方法提供。它們用來實現實際的繪製行爲,例如繪製點、線、圓以及方塊等
  • 輔助指令。這些用於提供輔助功能的指令將會影響後續指令的效果。如變換、裁剪區域等。這些輔助指令不如上面的繪製指令那麼直觀,可是在Android的繪製過程當中大量使用了輔助指令。在這些輔助指令中,最經常使用的莫過於變換指令了。變換指令包括translate(平移座標系),rotate(旋轉座標系),scale(縮放座標系)等,這些指令很大的幫助了控件樹的繪製。其實只要想想咱們在重寫onDraw()函數時從未考慮過控件的位置、旋轉、縮放等狀態。這說明在onDraw()方法執行以前,這些狀態都已經以變換的方式設置到Canvas中了。所以onDraw()方法中Canvas使用的是控件自身的座標系。

本篇總結

本篇文章詳細分析了ViewRootImpl的五大過程,ViewRootImpl比較複雜,尤爲是它的「心跳」performTraversals();。但願讀者能多看幾遍上面的分析。相信你必定會有收穫的


下篇預告

在下一篇文章中咱們將進行實戰項目,也是對咱們前幾篇文章的實際應用。老話說的好,紙上得來終覺淺,絕知此事要躬行。下一篇甚至幾篇咱們就來自定義View


參考博文


此致,敬禮

相關文章
相關標籤/搜索