View繪製原理 —— 畫在哪?

這是Android視圖繪製系列文章的第二篇,系列文章目錄以下:算法

  1. View繪製原理——畫多大?
  2. View繪製原理——畫在哪?
  3. View繪製原理——畫什麼?
  4. 讀源碼,懂原理,有什麼用?寫業務代碼又用不到?—— 自定義換行容器控件

若是想直接看結論能夠移步到第三篇末尾。bash

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

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

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

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

這一篇將從源碼的角度分析「定位(layout)」。佈局

如何描述位置

位置都是相對的,好比「我在你的右邊」、「你在廣場的西邊」。爲了代表位置,老是須要一個參照物。View的定位也須要一個參照物,這個參照物是View的父控件。能夠在View的成員變量中找到以下四個描述位置的參數:post

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經過上下左右四條線圍城的矩形來肯定相對於父控件的位置以及自身的大小。 那這裏所說的大小和上一篇中測量出的大小有什麼關係呢?留個懸念,先看一下上下左右這四個變量在哪裏被賦值。ui

肯定相對位置

全局搜索後,找到下面這個函數:this

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;
        ...
    }
}
複製代碼

沿着調用鏈繼續往上查找:spa

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()以設置孩子相對於本身的位置。code

不一樣的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()的調用,它表示着視圖矩形區域的大小以及相對於父控件的位置已經肯定。
相關文章
相關標籤/搜索