你需要知道的Android View的佈局

guojun_fire發表於2019-01-12

上一篇我們分析Android View的測量。我們先回顧一下,View的測量,在ViewRootImpl#performTraverals方法下,先進行對DecorView根佈局測量獲取MeasureSpec,然後開始執行測量performMeasure(),通過View#measure找到對應View的核心onMeasure(),如果是ViewGroup,先遞迴子View,將父View的MeasureSpec和子View的LayoutParams作為引數而進行測量,然後逐層返回,不斷儲存ViewGroup的測量寬高。

好了,我們短短回顧後,回到ViewRootImpl#performTraverals方法:

private void performTraversals() {
            ...

        if (!mStopped) {
            int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);  
            int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);       
            }
        } 

        if (didLayout) {
            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();
                }

                performDraw();
            }
        } 
        ...
}複製程式碼

原始碼非常清晰,繼續我們的分析performLayout()。Let`s go!

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
        int desiredWindowHeight) {
    mLayoutRequested = false;
    mScrollMayChange = true;
    mInLayout = true;

    final View host = mView;
    if (DEBUG_ORIENTATION || DEBUG_LAYOUT) {
        Log.v(TAG, "Laying out " + host + " to (" +
                host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")");
    }

    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
    try {
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); 
        ...
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
    mInLayout = false;
}複製程式碼

原始碼挺清晰易懂,我們著重看到host.layout(),host在上面mView賦值,那就是說host是指向DecorView物件的,方法所帶的引數分別是0,0,host.getMeasuredWidth(),host.getMeasuredHeight(),分別代表著View的左上右下四個位置。之前發分析所知DecorView是FrameLayout子類,FrameLayout是ViewGroup子類,而我們在ViewGroup#layout方法中看到是用final修飾的,那就是說host.layout呼叫的就是ViewGroup#layout,我們看一下該方法的原始碼:

    @Override
    public final void layout(int l, int t, int r, int b) {
        if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
            if (mTransition != null) {
                mTransition.layoutChange(this);
            }
            super.layout(l, t, r, b);
        } else {
            // record the fact that we noop`d it; request layout when transition finishes
            mLayoutCalledWhileSuppressed = true;
        }
    }複製程式碼

我們首先利用變數的命名推測,再結合原始碼的註釋來分析,看一下mTransition物件,我們看到是LayoutTransition類的物件,註釋寫著用於處理ViewGroup增加和刪除子檢視的動畫效果,那就是layout方法一開始可能是判斷一些引數來處理動畫的過渡效果的,不影響整體的程式碼邏輯,我們可以直接看super.layout(l, t, r, b);,那就是說呼叫的是View#layout方法,並將左上右下四個引數傳遞過去。

public class View implements ···{
    ···
    public void layout(int l, int t, int r, int b) {
        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的位置是否發生過變化,看有沒必要進行重新layout
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

            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);
                }
            }
        }

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    }
}複製程式碼

一開始的判斷,我們從他們全域性變數的註釋來理解,說的大概是在測量方法被跳過時,需要在layout()前再次呼叫measure()測量方法。接著是isLayoutModeOptical(),這裡面的註釋是這個ViewGroup的佈局是否在視角範圍裡,setOpticalFrame()裡面的實現方法經過一些判斷計算,同樣呼叫回setFrame(l, t, r, b)方法。

    protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;
        ···
        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;

            // Remember our drawn bit
            int drawn = mPrivateFlags & PFLAG_DRAWN;

            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

            // Invalidate our old position
            invalidate(sizeChanged);

            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
            ···
        return changed;
    }複製程式碼

這方法開始我們可以跳過,主要是對mLeft 、mTop、mRight、mBottom賦值,我們稍微看一下方法註釋中對left,top,right,bottom解析是各位置的點,且是相對於父佈局的,那就是說現在賦值後可以確定了View自己在父佈局的位置了。另外我們在類方法中查詢getLeft()等其他三個點,看看他們返回值對用mLeft等對應值的,這個點我們後面再說,我們繼續往下分析。

在setFrame()之後我們終於可以看到onLayout(),點進去檢視View#onLayout方法:

public class View implements···{
    ···
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }
    ···
}

public abstract class ViewGroup extends View implements··
    ···
    @Override
    protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
    ···複製程式碼

從上面原始碼我們看到View#onLayout與ViewGroup#onLayout都是實現了一個空方法。但是ViewGroup是一個抽象方法,那就是說繼承ViewGroup的子類必須重寫onLayout()方法。因為上篇我們分析View的測量同樣是不同的ViewGroup都有不同的onMeasure(),既然測量都不同了,onLayout()佈局方法就肯定不同了,我們按照上篇的邏輯,依舊對FrameLayout(DecorView)的onLayout來分析:

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }

    void layoutChildren(int left, int top, int right, int bottom,
                                  boolean forceLeftGravity) {
        final int count = getChildCount();
        // parentLeft由父容器的padding和Foreground決定
        final int parentLeft = getPaddingLeftWithForeground();
        final int parentRight = right - left - getPaddingRightWithForeground();

        final int parentTop = getPaddingTopWithForeground();
        final int parentBottom = bottom - top - getPaddingBottomWithForeground();

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            // 不為GONE
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                 //獲取子View的測量寬高
                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();

                int childLeft;
                int childTop;

                int gravity = lp.gravity;
                if (gravity == -1) {
                    gravity = DEFAULT_CHILD_GRAVITY;
                }

                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
                // 當子View設定水平方向layout_gravity屬性
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    // 居中的計算方式
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                        lp.leftMargin - lp.rightMargin;
                        break;
                    // 右側的計算方式
                    case Gravity.RIGHT:
                        if (!forceLeftGravity) {
                            childLeft = parentRight - width - lp.rightMargin;
                            break;
                        }
                    // 左側的計算方式
                    case Gravity.LEFT:
                    default:
                        childLeft = parentLeft + lp.leftMargin;
                }
                // 當子View設定豎直方向layout_gravity屬性
                switch (verticalGravity) {
                    case Gravity.TOP:
                        childTop = parentTop + lp.topMargin;
                        break;
                    case Gravity.CENTER_VERTICAL:
                        childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                        lp.topMargin - lp.bottomMargin;
                        break;
                    case Gravity.BOTTOM:
                        childTop = parentBottom - height - lp.bottomMargin;
                        break;
                    default:
                        childTop = parentTop + lp.topMargin;
                }
                 //對子元素進行佈局
                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }複製程式碼

FrameLayout#onLayout方法直接呼叫layoutChildren方法,裡面的實現方法雖然有點長,但是比較好理解,無非加點空間想象力上去就無壓力了。

我們梳理一下:首先是得到父佈局的左上右下的Padding值,然後遍歷子佈局,通過子View的layout_gravity屬性、子View的LayoutParams屬性、父佈局的Padding值來確定子View的左上右下引數,然後呼叫child.layout方法,把佈局流程從父容器傳遞到子元素。

上面我們已經分析過View#layout方法,是一個空方法,主要作用是我們使用的子View重寫該方法,例如TextView、CustomView自定義View等等。不同的View不同的佈局方式。大家有興趣可以看看他們的實現過程。

View#getWidth()、View#getMeasureWidth()

我們在分析View#setFrame()分析到這個問題,我們在今篇View的佈局可知,在View#setFrame()執行裡對
mLeft、mRight、mTop、mBottom,從命名方式帶m,我們可以知道這是一個全域性變數,在View的佈局時賦值的。

而View#getMeasureWidth()就要回到我們上一篇View的測量,在View#onMeasure方法中會呼叫View#setMeasuredDimension方法,在這方式的實現子View設定自身寬高的,這方法裡有View#setMeasuredDimensionRaw方法,我們看一下它的原始碼:

    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }複製程式碼

簡單來說就是對mMeasuredWidth與mMeasuredHeight賦值,所以在View#getMeasureWidth方法裡返回的值,,是我們進行測量後的值mMeasuredWidth。

他們的值基本情況下是一致的,那麼不一致時什麼時候呢?看回我們本篇中的FrameLayout#onLayout,最後是不是呼叫了childView#layout方法,FrameLayout我們不可修改,但是在我們CustomView自定義View,重寫onLayout的時候是可以按照我們的特殊要求修改的,例如修改為:childView.layout(0,0,100,100);那麼View#getWidth()、View#getMeasureWidth()返回的值就會不一致,有興趣的同學可以自己去驗證一下。

所以他們的值在不特殊修改的情況下返回時一樣的,但是他們的意義是完全不同的,一個在測量過程、一個在佈局過程。大家要稍微留意。

View的佈局流程就已經全部分析完了。我們總結一下:佈局流程相對簡單一些,上一篇View的測量,我們可以得到View的寬和高,ViewGroup的layout佈局,呼叫layout方法,確定在父佈局的位置,在onLayout()方法中遍歷其子View,呼叫子View的layout方法並根據子View大小、View的LayoutParams值、父View對子 View位置的限制作為引數傳入,完成佈局;而View的測量利用測量出來的寬和高來計算出子View相對於父View的位置引數,完成佈局。在下篇,我們將會講述最後一步,View的繪製。

相關文章