View繪製——畫在哪?

Taylor發表於2019-02-26

這是Android檢視繪製系列文章的第二篇,系列文章目錄如下:

  1. View繪製——畫多大?
  2. View繪製——畫在哪?
  3. View繪製——怎麼畫?

如果想直接看結論可以移步到第三篇末尾。

View繪製就好比畫畫,先拋開Android概念,如果要畫一張圖,首先會想到哪幾個基本問題:

  • 畫多大?
  • 畫在哪?
  • 怎麼畫?

Android繪製系統也是按照這個思路對View進行繪製,上面這些問題的答案分別藏在:

  • 測量(measure)
  • 定位(layout)
  • 繪製(draw)

這一篇將從原始碼的角度分析“定位(layout)”。

如何描述位置

位置都是相對的,比如“我在你的右邊”、“你在廣場的西邊”。為了表明位置,總是需要一個參照物。View的定位也需要一個參照物,這個參照物是View的父控制元件。可以在View的成員變數中找到如下四個描述位置的引數:

public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
    /**
     * The distance in pixels from the left edge of this view’s parent
     * to the left edge of this view.
     * view左邊相對於父親左邊的距離
     */
    protected int mLeft;
    
    /**
     * The distance in pixels from the left edge of this view‘s parent
     * to the right edge of this view.
     * view右邊相對於父親左邊的距離
     */
    protected int mRight;
    
    /**
     * The distance in pixels from the top edge of this view’s parent
     * to the top edge of this view.
     * view上邊相對於父親上邊的距離
     */
    protected int mTop;
    
    /**
     * The distance in pixels from the top edge of this view‘s parent
     * to the bottom edge of this view.
     * view底邊相對於父親上邊的距離
     */
    protected int mBottom;
    ...
}
複製程式碼

View通過上下左右四條線圍城的矩形來確定相對於父控制元件的位置以及自身的大小。 那這裡所說的大小和上一篇中測量出的大小有什麼關係呢?留個懸念,先看一下上下左右這四個變數在哪裡被賦值。

確定相對位置

全域性搜尋後,找到下面這個函式:

public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
    /**
     * Assign a size and position to this view.
     * 賦予當前view尺寸和位置
     *
     * This is called from layout.
     * 這個函式在layout中被呼叫
     *
     * @param left Left position, relative to parent
     * @param top Top position, relative to parent
     * @param right Right position, relative to parent
     * @param bottom Bottom position, relative to parent
     * @return true if the new size and position are different than the previous ones
     */
    protected boolean setFrame(int left, int top, int right, int bottom) {
        ...
        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
        ...
    }
}
複製程式碼

沿著呼叫鏈繼續往上查詢:

public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
    /**
     * Assign a size and position to a view and all of its
     * descendants
     * 將尺寸和位置賦予當前view和所有它的孩子
     *
     * <p>This is the second phase of the layout mechanism.
     * (The first is measuring). In this phase, each parent calls
     * layout on all of its children to position them.
     * This is typically done using the child measurements
     * that were stored in the measure pass().</p>
     *
     * <p>Derived classes should not override this method.
     * Derived classes with children should override
     * onLayout. In that method, they should
     * call layout on each of their children.</p>
     * 子類不應該過載這個方法,而應該過載onLayout(),並且在其中區域性所有孩子
     *
     * @param l Left position, relative to parent
     * @param t Top position, relative to parent
     * @param r Right position, relative to parent
     * @param b Bottom position, relative to parent
     */
    public void layout(int l, int t, int r, int b) {
        ...
        //為View上下左右四條線賦值
        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);
            ...
        }
    }
    ...
    /**
     * Called from layout when this view should
     * assign a size and position to each of its children.
     * 當需要賦予所有孩子尺寸和位置的時候,這個函式在layout中被呼叫
     *
     * Derived classes with children should override
     * this method and call layout on each of
     * their children.
     * 帶有孩子的子類應該過載這個方法並呼叫每個孩子的layout()
     * @param changed This is a new size or position for this view
     * @param left Left position, relative to parent
     * @param top Top position, relative to parent
     * @param right Right position, relative to parent
     * @param bottom Bottom position, relative to parent
     */
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }
}
複製程式碼

結合呼叫鏈和程式碼註釋,可以得出結論:孩子的定位是由父控制元件發起的,父控制元件會在ViewGroup.onLayout()中遍歷所有的孩子並呼叫它們的View.layout()以設定孩子相對於自己的位置。

不同的ViewGroup有不同的方式來佈局孩子,以FrameLayout為例:

public class FrameLayout extends ViewGroup {

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

        //遍歷所有孩子
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            //排除不可見孩子
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();

                //獲得孩子在measure過程中確定的寬高
                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;
                }
                //呼叫孩子的layout(),確定孩子相對父控制元件位置
                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }
}
複製程式碼

FrameLayout所有的孩子都是相對於它的左上角進行定位,並且在定位孩子右邊和下邊的時候直接加上了在measure過程中得到的寬和高。

測量尺寸和實際尺寸的關係

FrameLayout遍歷孩子並觸發它們定位的過程中,會用到上一篇測量的結果(通過getMeasuredWidth()getMeasuredHeight()),並最終通過layout()影響mRightmBottom的值。對比一下getWidth()getMeasuredWidth()

public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
    public final int getWidth() {
        //控制元件右邊和左邊差值
        return mRight - mLeft;
    }
    
    /**
     * Like {@link #getMeasuredWidthAndState()}, but only returns the
     * raw width component (that is the result is masked by
     * 獲得MeasureSpec的尺寸部分
     * {@link #MEASURED_SIZE_MASK}).
     *
     * @return The raw measured width of this view.
     */
    public final int getMeasuredWidth() {
        return mMeasuredWidth & MEASURED_SIZE_MASK;
    }
}
複製程式碼
  • getMeasuredWidth()是measure過程的產物,它是測量尺寸。getWidth()是layout過程的產物,它是佈局尺寸。它們的值可能不相等。
  • 測量尺寸只是layout過程中可能用到的關於控制元件大小的參考值,不同的ViewGroup會有不同的layout演算法,也就有不同的使用參考值的方法,控制元件最終展示尺寸由layout過程決定(以佈局尺寸為準)。

總結

  1. 控制元件位置和最終展示的尺寸是通過上(mTop)、下(mBottom)、左(mLeft)、右(mRight)四條線圍城的矩形來描述的。
  2. 控制元件定位就是確定自己相對於父控制元件的位置,子控制元件總是相對於父控制元件定位,當根佈局的位置確定後,螢幕上所有控制元件的位置都確定了。
  3. 控制元件定位是由父控制元件發起的,父控制元件完成自己定位之後會呼叫onLayout(),在其中遍歷所有孩子並呼叫它們的layout()方法以確定子控制元件相對於自己的位置。
  4. 整個定位過程的終點是View.setFrame()的呼叫,它表示著檢視矩形區域的大小以及相對於父控制元件的位置已經確定。

相關文章