Android GUI之View佈局

傑瑞教育發表於2015-08-17

在清楚了View繪製機制中的第一步測量之後,我們繼續來了解分析View繪製的第二個過程,那就是佈局定位。繼續跟蹤分析原始碼,根據之前的流程分析我們知道View的繪製是從RootViewImpl的performTraversals方法開始的,在此方法中依次呼叫了performMeasure、performLayout、performDraw等方法進行測量、佈局、繪製,那麼下面我們就看看則方performLayout中都做了哪些事情,該方法的關鍵原始碼如下:

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        mLayoutRequested = false;
        mScrollMayChange = true;
        mInLayout = true;
        final View host = mView;
          ……
        try {
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
……
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
        mInLayout = false;
    }

從中看出,最關鍵的程式碼就是呼叫了host.layout方法,那麼大家還記不記得host是個什麼東東呢?對了,正是我們之前說的根檢視DecorView。那麼我們就回到DecorView看看它在layout方法中到底做了什麼事情。令人失望的是,我們在DecorView中並沒有發現該方法,不要急,根據該類的繼承體系,我們最終追蹤到layout方法在View中。

Android GUI之View佈局

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

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

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {}

該方法中的4個引數代表了當前的View與父View之間4個方向上的距離,同時從說明中可以看出,此方法不應該被子類重寫,如果需要重新佈局,可以在子類中重寫的方法是onLayout,此方法在View中是個空方法,什麼都沒有寫。可實際上layout的方法在View中並沒有被標識為final,這就意味是可以被重寫的。

繼續檢視ViewGoup中的相關程式碼,果然layout被重寫了並新增了final標識,同時onLayout被標識為抽象方法,所以繼承了ViewGroup的類是,是不能重寫layout方法的,並且要實現onLayout方法。從程式碼可以看出,雖然ViewGroup重寫了layout,實際本質上還是呼叫了View的layout,然後通過呼叫onLayout方法最終完成佈局定位的。

       @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;
        }
    }
    @Override
    protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);

在DecorView中,並沒有發現onLayout方法,所以它使用的肯定是其父類FrameLayout中的,找到FrameLayout的原始碼,可以檢視到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();

        final int parentLeft = getPaddingLeftWithForeground();
        final int parentRight = right - left - getPaddingRightWithForeground();

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

        mForegroundBoundsChanged = true;

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();

                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;

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

                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中最終呼叫了layoutChildren方法,在該方法中根據測量結果和一些佈局屬性對容器中每一個View都呼叫了layout方法進行了佈局。根據以上的程式碼分析,我們可以得出View佈局定位的流程圖如下。

Android GUI之View佈局

相關文章