天天看點

源碼解析Android中View的layout布局過程

Android中的Veiw從記憶體中到呈現在UI界面上需要依次經曆三個階段:量算 -> 布局 -> 繪圖,關于View的量算、布局、繪圖的總體機制可參見博文 《 Android中View的布局及繪圖機制》。量算是布局的基礎,如果想了解量算的細節,可參見博文《源碼解析Android中View的measure量算過程》。本文将從源碼角度解析View的布局layout過程,本文會詳細介紹View布局過程中的關鍵方法,并對源碼加上了注釋以進行說明。

對View進行布局的目的是計算出View的尺寸以及在其父控件中的位置,具體來說就是計算出View的四條邊界分别到其父控件左邊界、上邊界的距離,即計算View的left、top、right、bottom的值。

layout

layout()方法是View布局的入口,其源碼如下所示:

public void layout(int l, int t, int r, int b) {
        //成員變量mPrivateFlags3中的一些比特位存儲着和layout相關的資訊
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != ) {
            //如果在mPrivateFlags3的低位位元組的第4位(從最右向左數第4位)的值為1,
            //那麼就表示在layout布局前需要先對View進行量算,
            //這種情況下就會執行View的onMeasure方法對View進行量算
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            //量算完成後就會将mPrivateFlags3低位位元組的第4位重置為0,
            //移除掉标簽PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        //如果isLayoutModeOptical()傳回true,那麼就會執行setOpticalFrame()方法,
        //否則會執行setFrame()方法。并且setOpticalFrame()内部會調用setFrame(),
        //是以無論如何都會執行setFrame()方法。
        //setFrame()方法會将View新的left、top、right、bottom存儲到View的成員變量中
        //并且傳回一個boolean值,如果傳回true表示View的位置或尺寸發生了變化,
        //否則表示未發生變化
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);


        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            //如果View的布局發生了變化,或者mPrivateFlags有需要LAYOUT的标簽PFLAG_LAYOUT_REQUIRED,
            //那麼就會執行以下代碼
            //首先會觸發onLayout方法的執行,View中預設的onLayout方法是個空方法
            //不過繼承自ViewGroup的類都需要實作onLayout方法,進而在onLayout方法中依次循環子View,
            //并調用子View的layout方法
            onLayout(changed, l, t, r, b);
            //在執行完onLayout方法之後,從mPrivateFlags中移除标簽PFLAG_LAYOUT_REQUIRED
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

            //我們可以通過View的addOnLayoutChangeListener(View.OnLayoutChangeListener listener)方法
            //向View中添加多個Layout發生變化的事件監聽器
            //這些事件監聽器都存儲在mListenerInfo.mOnLayoutChangeListeners這個ArrayList中
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                //首先對mOnLayoutChangeListeners中的事件監聽器進行拷貝
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = ; i < numListeners; ++i) {
                    //周遊注冊的事件監聽器,依次調用其onLayoutChange方法,這樣Layout事件監聽器就得到了響應
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }

        //從mPrivateFlags中移除強制Layout的标簽PFLAG_FORCE_LAYOUT
        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        //向mPrivateFlags3中加入Layout完成的标簽PFLAG3_IS_LAID_OUT
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    }
           
  • 在layout()方法内部剛開始執行的時候,首先會根據mPrivateFlags3變量是否具有标志位PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT判斷是否需要執行View的onMeasure()方法。如果具有标志位PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT,則執行onMeasure()方法,進而對View進行量算,量算的結果會儲存到View的成員變量中。量算完成後就會将mPrivateFlags3低位位元組的第4位重置為0,移除掉标簽PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT。
  • 如果isLayoutModeOptical()傳回true,那麼就會執行setOpticalFrame()方法,否則會執行setFrame()方法。并且setOpticalFrame()内部會調用setFrame(),是以無論如何都會執行setFrame()方法。setFrame()方法會将View新的left、top、right、bottom存儲到View的成員變量中,并且傳回一個boolean值,如果傳回true表示View的位置或尺寸發生了變化,否則表示未發生變化。後面會對setFrame()方法詳細介紹。
  • 如果View的布局發生了變化,或者mPrivateFlags有需要LAYOUT的标簽PFLAG_LAYOUT_REQUIRED,就會觸發onLayout方法的執行,View中預設的onLayout方法是個空方法。不過繼承自ViewGroup的類都需要實作onLayout方法,進而在onLayout方法中依次循環子View,并調用子View的layout方法。在執行完onLayout方法之後,從mPrivateFlags中移除标簽PFLAG_LAYOUT_REQUIRED。然後會周遊注冊的Layout Change事件監聽器,依次調用其onLayoutChange方法,這樣Layout事件監聽器就得到了響應。
  • 最後,從mPrivateFlags中移除強制Layout的标簽PFLAG_FORCE_LAYOUT,向mPrivateFlags3中加入Layout完成的标簽PFLAG3_IS_LAID_OUT。

setFrame

setFrame()方法是具體用來完成給View配置設定尺寸以及位置工作的,在layout()方法中會調用setFrame()方法。其源碼如下所示:

protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;

        if (DBG) {
            Log.d("View", this + " View.setFrame(" + left + "," + top + ","
                    + right + "," + bottom + ")");
        }

        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            //将新舊left、right、top、bottom進行對比,隻要不完全相對就說明View的布局發生了變化,
            //則将changed變量設定為true
            changed = true;

            //先儲存一下mPrivateFlags中的PFLAG_DRAWN标簽資訊
            int drawn = mPrivateFlags & PFLAG_DRAWN;

            //分别計算View的新舊尺寸
            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            //比較View的新舊尺寸是否相同,如果尺寸發生了變化,那麼sizeChanged的值為true
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

            // Invalidate our old position
            invalidate(sizeChanged);

            //将新的left、top、right、bottom存儲到View的成員變量中
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            //mRenderNode.setLeftTopRightBottom()方法會調用RenderNode中原生方法的nSetLeftTopRightBottom()方法,
            //該方法會根據left、top、right、bottom更新用于渲染的顯示清單
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

            //向mPrivateFlags中增加标簽PFLAG_HAS_BOUNDS,表示目前View具有了明确的邊界範圍
            mPrivateFlags |= PFLAG_HAS_BOUNDS;


            if (sizeChanged) {
                //如果View的尺寸和之前相比發生了變化,那麼就執行sizeChange()方法,
                //該方法中又會調用onSizeChanged()方法,并将View的新舊尺寸傳遞進去
                sizeChange(newWidth, newHeight, oldWidth, oldHeight);
            }

            if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
                //有可能在調用setFrame方法之前,invalidate方法就被調用了,
                //這會導緻mPrivateFlags移除了PFLAG_DRAWN标簽。
                //如果目前View處于可見狀态就将mPrivateFlags強制添加PFLAG_DRAWN狀态位,
                //這樣會確定下面的invalidate()方法會執行到其父控件級别。
                mPrivateFlags |= PFLAG_DRAWN;
                invalidate(sizeChanged);
                //invalidateParentCaches()方法會移除其父控件的PFLAG_INVALIDATED标簽,
                //這樣其父控件就會重建用于渲染的顯示清單
                invalidateParentCaches();
            }

            // 重新恢複mPrivateFlags中原有的PFLAG_DRAWN标簽資訊
            mPrivateFlags |= drawn;

            mBackgroundSizeChanged = true;
            if (mForegroundInfo != null) {
                mForegroundInfo.mBoundsChanged = true;
            }

            notifySubtreeAccessibilityStateChangedIfNeeded();
        }
        return changed;
    }
           
  • 在該方法中,會将新舊left、right、top、bottom進行對比,隻要不完全相同就說明View的布局發生了變化,則将changed變量設定為true。然後比較View的新舊尺寸是否相同,如果尺寸發生了變化,并将其儲存到變量sizeChanged中。如果尺寸發生了變化,那麼sizeChanged的值為true。
  • 然後将新的left、top、right、bottom存儲到View的成員變量中儲存下來。并執行mRenderNode.setLeftTopRightBottom()方法會,其會調用RenderNode中原生方法的nSetLeftTopRightBottom()方法,該方法會根據left、top、right、bottom更新用于渲染的顯示清單。
  • 如果View的尺寸和之前相比發生了變化,那麼就執行sizeChange()方法,該方法中又會調用onSizeChanged()方法,并将View的新舊尺寸傳遞進去。
  • 如果View處于可見狀态,那麼會調用invalidate和invalidateParentCaches方法。invalidateParentCaches()方法會移除其父控件的PFLAG_INVALIDATED标簽,這樣其父控件就會重建用于渲染的顯示清單。

sizeChange

sizeChange方法會在View的尺寸發生變化時調用,在setFrame()方法中就可能會調用sizeChange()方法。當然,在View的setLeft()、setTop()、setRight()、setBottom()等其他改變View尺寸的方法中也會調用sizeChange()方法,其源碼如下所示:

private void sizeChange(int newWidth, int newHeight, int oldWidth, int oldHeight) {
        //将View的新舊尺寸傳遞給onSizeChanged()方法
        onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);
        if (mOverlay != null) {
            mOverlay.getOverlayView().setRight(newWidth);
            mOverlay.getOverlayView().setBottom(newHeight);
        }
        rebuildOutline();
    }
           

在該方法中其主要将View的新舊尺寸傳遞給onSizeChanged()方法使其執行。

onSizeChanged

onSizeChanged()方法是個空方法,代碼如下所示:

protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    }
           

該方法會在View的尺寸發生變化時,通過sizeChange()方法的執行而被調用。當View第一次加入到View樹中時,該方法也會被調用,隻不過傳入的舊尺寸oldWidth和oldHeight都是0。

總結

layout方法總的調用過程主線如下所示:

layout() -> onMeasure() -> setFrame() -> sizeChange() -> onSizeChanged() -> onLayout() ->周遊執行OnLayoutChangeListener.onLayoutChange()

希望本文對大家了解View的layout布局過程有所幫助!

相關閱讀:

《我的Android博文整理彙總》

《 Android中View的布局及繪圖機制》

《源碼解析Android中View的measure量算過程》