Android開發之漫漫長途 Ⅴ——Activity的顯示之ViewRootImpl的PreMeasure、WindowLayout、EndMeasure、Layout、Draw

該文章是一個系列文章,是本人在Android開發的漫漫長途上的一點感想和記錄,我會盡可能按照先易後難的順序進行編寫該系列。該系列引用了《Android開發藝術探索》以及《深刻理解Android 卷Ⅰ,Ⅱ,Ⅲ》中的相關知識,另外也借鑑了其餘的優質博客,在此向各位大神表示感謝,膜拜!!!另外,本系列文章知識可能須要有必定Android開發基礎和項目經驗的同窗才能更好理解,也就是說該系列文章面向的是Android中高級開發工程師。android


第五篇了,,接着上一篇說ide


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


咱們來看ViewRootImpl的構造函數工具

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

注:oop

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

咱們再來看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 {

                }

                ...

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

                }

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

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

				...

                view.assignParent(this);

	            ...
            }
        }
    }

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

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

scheduleTraversals();函數聲明以下this

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

其中mTraversalRunnable的定義是這樣的spa

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

doTraversal()函數聲明以下;操作系統

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的「心跳」。咱們就來看一下這個方法把。 先上源碼:(注:源碼很長,具體的分析在下方)

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()方法將會沿着控件樹依次獲得回調。 預測量和測量原理 <h4>(1)預測量參數的候選(對應第1階段①②③)</h4> 預測量也是一次完整的測量過程,它與最終測量的區別僅在於參數不一樣而已。實際的測量工做是在View或其子類的onMeasure()方法中完成,而且其測量結果須要受限於來自其父控件的指示。這個指示由onMeasure()方法中的兩個參數進行傳達:widthSpec和heightSpec。它們是被稱爲MeasureSpec的複合整型變量,用於指導控件對自身進行測量。她又兩個份量,結構如圖 這裏寫圖片描述 由①、②、③可知預測量時的SPEC_SIZE按照以下原則進行取值: - 第一次「遍歷」時,使用可用的最大尺寸做爲SPEC_SIZE的候選 - 此窗口是一個懸浮窗口時,即LayoutParams.width/height其中之一被指定爲WRAP_CONTENT時,使用可用的最大尺寸做爲SPEC_SIZE的候選 - 其餘狀況下,使用窗口最新尺寸做爲SPEC_SIZE的候選 <h4>(2)測量協商(對應第1階段④)</h4> 在第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;

}

<h4>(3)肯定是否須要改變窗口尺寸(對應第1階段⑤)</h4> 前文屢次設置了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,該變量前文中已有詳細描述。

在上述條件知足的條件下,如下條件知足其一即觸發布局窗口階段 ①測量結果與ViewRootImpl中所保存的當前尺寸有差別

②懸浮窗口的測量結果與窗口的最新尺寸有差別

**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)**作了兩件事。

<h4>① 進行控件樹佈局</h4> 調用了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方法是由父控件調用,所以控件的位置尺寸的最終決定權在父控件手中,測量結果僅僅是一個參考。
  • 通常來講,子控件的測量結果影響父控件的測量結果,所以測量過程是後根遍歷。而父控件的佈局結果影響子控件的佈局結果。因此佈局過程是先根遍歷

<h4> ② 設置透明區域</h4> 佈局階段的另外一個工做是計算並設置窗口的透明區域。這一功能主要是爲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


此致,敬禮

相關文章
相關標籤/搜索