Android常用Layout源碼總結—RelativeLayout

前言

通過學習Android官方Layout的源碼,可以幫助自己更好的理解Android的UI框架系統,瞭解內部便捷的封裝好的API調用,有助於進行佈局優化和自定義view實現等工作。這裏把學習結果通過寫博客進行總結,便於記憶,不至於將來遺忘。

本篇博客中源碼基於Android 8.1

RelativeLayout特點

RelativeLayout是Android開發中最常用的Layout之一,它支持子view間設置相對位置關係,子view可以支持指定兄弟view作爲錨點,基於錨點做相對位置佈局。

要實現這樣的功能,可以推測,RelativeLayout支持子view給LayoutParams設置相對位置規則的屬性,以及在測量和佈局過程中能夠計算出子view間的相對位置和依賴關係。

源碼探究

構造函數

public RelativeLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
    // 初始化自定義屬性
    initFromAttributes(context, attrs, defStyleAttr, defStyleRes);
    // 設置兼容模式標記位
    queryCompatibilityModes(context);
}

RelativeLayout的構造函數主要做兩件事:初始化屬性和設置兼容標記。

初始化屬性

private void initFromAttributes(
        Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    final TypedArray a = context.obtainStyledAttributes(
            attrs, R.styleable.RelativeLayout, defStyleAttr, defStyleRes);
    // 不受mGravity影響的子view ID,默認無忽略。
    mIgnoreGravity = a.getResourceId(R.styleable.RelativeLayout_ignoreGravity, View.NO_ID);
    // 對齊方式,默認Gravity.START | Gravity.TOP。
    mGravity = a.getInt(R.styleable.RelativeLayout_gravity, mGravity);
    a.recycle();
}

看源碼可知RelativeLayout可以設置ignoreGravitygravity屬性,其中gravity屬性會凌駕於子view的相對位置規則屬性,而ignoreGravity可以設置不受gravity屬性影響的子view

舉例說明:

  1. 雖然子view設置了layout_alignParentLeft屬性,但是RelativeLayout設置了gravity屬性
<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="right">

    <View
        android:id="@+id/view"
        android:layout_width="160dp"
        android:layout_height="100dp"
        android:layout_alignParentLeft="true"
        android:background="@android:color/holo_blue_dark"/>
</RelativeLayout>
  1. RelativeLayout設置了ignoreGravity屬性
<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:ignoreGravity="@+id/view"
    android:gravity="right">

    <View
        android:id="@+id/view"
        android:layout_width="160dp"
        android:layout_height="100dp"
        android:layout_alignParentLeft="true"
        android:background="@android:color/holo_blue_dark"/>
</RelativeLayout>

設置兼容標記

private void queryCompatibilityModes(Context context) {
    int version = context.getApplicationInfo().targetSdkVersion;
    // 小等於Android4.2的版本爲true(不考慮低版本的話,就記住該變量值爲false)
    mAllowBrokenMeasureSpecs = version <= Build.VERSION_CODES.JELLY_BEAN_MR1;
    // 大等於Android4.3的版本爲true(不考慮低版本的話,就記住該變量值爲true)
    mMeasureVerticalWithPaddingMargin = version >= Build.VERSION_CODES.JELLY_BEAN_MR2;
}
  • mAllowBrokenMeasureSpecs
    官方註釋說明:

Compatibility hack. Old versions of the platform had problems with MeasureSpec value overflow and RelativeLayout was one source of them. Some apps came to rely on them. 😦

在不低於Android4.3的版本上,RelativeLayout在測量階段生成MeasureSpec時,若傳入的測量模式爲UNSPECIFIED,則生成的MeasureSpec的測量模式可以也置爲UNSPECIFIED。

  • mMeasureVerticalWithPaddingMargin
    官方註釋說明:

Compatibility hack. Old versions of the platform would not take margins and padding into account when generating the height measure spec for children during the horizontal measure pass.

在不低於Android4.3的版本上,RelativeLayout在測量階段爲子view生成高度MeasureSpec時,會計算padding和margin。

LayoutParams

RelativeLayout中定義了靜態內部類LayoutParams繼承自MarginLayoutParams。
其中有一些比較重要的成員變量:

// child設置的相對位置規則的屬性值(容量爲22)
private int[] mRules = new int[VERB_COUNT];
private int[] mInitialRules = new int[VERB_COUNT];

// child的左上右下邊界約束(可理解爲layout中的l、t、r、b)
private int mLeft, mTop, mRight, mBottom;

// layout_alignWithParentIfMissing屬性值
public boolean alignWithParent;
  • mRules說明
    這裏將layout_toLeftOf、layout_alignLeft等屬性稱爲規則,mRules數組用於保存子view設置的相對位置規則屬性的值,不同的下標索引存放對應的屬性,總共有22個位置規則屬性。
    看LayoutParams構造函數中屬性初始化部分:
public LayoutParams(Context c, AttributeSet attrs) {
    // 省略部分
    // ···
    
    final int[] rules = mRules;
    //noinspection MismatchedReadAndWriteOfArray
    final int[] initialRules = mInitialRules;

    final int N = a.getIndexCount();
    // 遍歷TypedArray,依次解析出屬性值。
    for (int i = 0; i < N; i++) {
        int attr = a.getIndex(i);
        // 注意:這裏switch語句塊中省略了很多case,僅列出了部分有代表性的舉例。
        switch (attr) {
            case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignWithParentIfMissing:
                alignWithParent = a.getBoolean(attr, false);
                break;
            case com.android.internal.R.styleable.RelativeLayout_Layout_layout_toLeftOf:
                // 這裏獲取的值爲layout_toLeftOf屬性對應的view ID,將ID保存在rules數組索引0的位置裏。
                rules[LEFT_OF] = a.getResourceId(attr, 0);
                break;
            case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentLeft:
                // 這裏獲取的值爲layout_alignParentLeft屬性對應的布爾值,按true=>-1,false=>0,
                // 轉換成整型保存在rules數組索引9的位置。
                rules[ALIGN_PARENT_LEFT] = a.getBoolean(attr, false) ? TRUE : 0;
                break;
        }
    }
    
    mRulesChanged = true;
    System.arraycopy(rules, LEFT_OF, initialRules, LEFT_OF, VERB_COUNT);

    a.recycle();
}

可以看出,若屬性值爲view的ID,則保存在該屬性對應的數組下標索引位置。若屬性值爲布爾值,則轉換成-1或0後再保存。屬性全部解析完後,mRules的某個索引值爲0,表示子view沒有設置這個屬性值或設置爲false

所有規則屬性

上圖中是所有規則屬性,按順序分別對應到mRules數組的22個元素中。

  • mLeft, mTop, mRight, mBottom
    和layout階段中的l、t、r、b類似,表示左上右下邊界約束,默認值爲VALUE_NOT_SET,VALUE_NOT_SET常量值爲Integer.MIN_VALUE。這四個成員變量值會在measure階段確定,layout階段將直接使用。

假設有view A設置了layout_below屬性,屬性值爲view B的ID。那麼view A的LayoutParams.mTop將被修改爲view B的LayoutParams.mBottom(若有padding、margin,也需要加入計算)。

  • alignWithParent
    layout_alignWithParentIfMissing屬性,爲true時,如果錨點不存在或錨點的可見性爲GONE,則將父級用作錨點。

假設有 A layout_alignRight B,當B設置爲GONE時,A的右邊將靠父佈局RelativeLayout的右邊對齊。

相對位置依賴關係圖

RelativeLayout在測量階段會根據每個child的LayoutParams的mRules生成依賴關係圖,用來表示各child間的位置依賴關係,便於計算尺寸和位置約束。

如圖所示佈局:

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <View
        android:id="@+id/A"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="@android:color/holo_red_light"/>

    <View
        android:id="@+id/B"
        android:layout_width="200dp"
        android:layout_height="100dp"
        android:layout_toRightOf="@+id/A"
        android:background="@android:color/holo_blue_dark"/>

    <View
        android:id="@+id/C"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_below="@+id/A"
        android:background="@android:color/holo_purple"/>

    <View
        android:id="@+id/D"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_below="@+id/B"
        android:layout_alignRight="@+id/B"
        android:background="@android:color/holo_blue_bright"/>
</RelativeLayout>

用依賴關係圖表示爲:

其中D的位置依賴B,B、C的位置依賴A,A無依賴。這裏把依賴當前節點的view稱爲伴隨節點,被當前節點依賴的view稱爲錨點節點,而沒有依賴其他view的錨點節點稱爲根錨點節點

比如此圖例中:A爲根錨點,並且是B、C的錨點,B、C是A伴隨。B是D的錨點,D是B的伴隨。

有了依賴關係圖的概念,在代碼中需要用數據結構來表示它:

節點Node

一個view用一個Node表示。

static class Node {
    /**
     * The view representing this node in the layout.
     */
    View view;

    /**
     * The list of dependents for this node; a dependent is a node
     * that needs this node to be processed first.
     */
    final ArrayMap<Node, DependencyGraph> dependents =
            new ArrayMap<Node, DependencyGraph>();

    /**
     * The list of dependencies for this node.
     */
    final SparseArray<Node> dependencies = new SparseArray<Node>();
    
    // ···
}
  • view:持有view引用
  • dependents:存放依賴我的view,即伴隨節點集合
  • dependencies:存放被我依賴的view,即錨點節點集合

依賴圖DependencyGraph

一個RelativeLayout會生成一個DependencyGraph。

private static class DependencyGraph {
    /**
     * List of all views in the graph.
     */
    private ArrayList<Node> mNodes = new ArrayList<Node>();

    /**
     * List of nodes in the graph. Each node is identified by its
     * view id (see View#getId()).
     */
    private SparseArray<Node> mKeyNodes = new SparseArray<Node>();

    /**
     * Temporary data structure used to build the list of roots
     * for this graph.
     */
    private ArrayDeque<Node> mRoots = new ArrayDeque<Node>();
    
    // ···
}
  • mNodes:RelativeLayout中的所有child節點集合
  • mKeyNodes:RelativeLayout中的所有有view ID的child集合,view ID作爲key
  • mRoots:RelativeLayout中的所有根錨點集合

DependencyGraph中重要方法說明:

  • findRoots
    獲取所有根錨點
// 入參rulesFilter表示一組規則索引,對應LayoutParams.mRules數組中的索引。
// 例如傳入toLeftOf規則,則索引值爲0,對應mRules[0]的位置。
private ArrayDeque<Node> findRoots(int[] rulesFilter) {
    final SparseArray<Node> keyNodes = mKeyNodes;
    final ArrayList<Node> nodes = mNodes;
    final int count = nodes.size();

    // Find roots can be invoked several times, so make sure to clear
    // all dependents and dependencies before running the algorithm
    // 首先遍歷節點,清空舊數據。
    for (int i = 0; i < count; i++) {
        final Node node = nodes.get(i);
        node.dependents.clear();
        node.dependencies.clear();
    }

    // Builds up the dependents and dependencies for each node of the graph
    // 遍歷節點構建依賴關係。
    for (int i = 0; i < count; i++) {
        final Node node = nodes.get(i);

        // 取出節點對應的view的LayoutParams中的mRules(mRules存放的是各規則屬性的值)
        final LayoutParams layoutParams = (LayoutParams) node.view.getLayoutParams();
        final int[] rules = layoutParams.mRules;
        final int rulesCount = rulesFilter.length;

        // Look only the the rules passed in parameter, this way we build only the
        // dependencies for a specific set of rules
        // 遍歷入參的規則索引數組,初始化各節點的錨點集合和伴隨集合。
        for (int j = 0; j < rulesCount; j++) {
            // rulesFilter中的元素值是mRules數組的索引,根據索引從rules中取出對應元素值。
            final int rule = rules[rulesFilter[j]];
            if (rule > 0) {
                // 若rule大於0,表示child有設置對應的規則屬性。例如child設置了layout_toLeftOf屬性,那麼rule值爲設置的view ID。
                // The node this node depends on
                // 根據view ID獲取節點,該節點即爲當前child的錨點。
                final Node dependency = keyNodes.get(rule);
                // Skip unknowns and self dependencies
                if (dependency == null || dependency == node) {
                    continue;
                }
                // Add the current node as a dependent
                // 錨點節點記錄伴隨節點
                dependency.dependents.put(node, this);
                // Add a dependency to the current node
                // 伴隨節點記錄錨點
                node.dependencies.put(rule, dependency);
            }
        }
    }

    // 清空根錨點集合舊數據
    final ArrayDeque<Node> roots = mRoots;
    roots.clear();

    // Finds all the roots in the graph: all nodes with no dependencies
    // 遍歷所有節點,初始化根錨點集合。
    for (int i = 0; i < count; i++) {
        final Node node = nodes.get(i);
        // 若該節點沒有依賴任何別的節點,則添加至根錨點集合。
        if (node.dependencies.size() == 0) roots.addLast(node);
    }

    // 返回根錨點集合
    return roots;
}

該方法中首先清空舊數據。之後遍歷節點,填充節點的dependents和dependencies集合。最後再次遍歷節點,將沒有依賴任何其他節點的節點,添加至根錨點集合。
可以看出在這個方法中,完成了RelativeLayout子節點依賴關係的構建

  • getSortedViews
    將所有節點按照指定相對位置關係規則進行排序
// sorted用於保存排序後的結果,傳入前已根據節點數量創建好數組。
// 入參rulesFilter表示一組規則索引,對應LayoutParams.mRules數組中的索引。
// 例如傳入toLeftOf規則,則索引值爲0,對應mRules[0]的位置。
void getSortedViews(View[] sorted, int... rules) {
    // 獲取所有根錨點,用於從根錨點開始進行遍歷。
    final ArrayDeque<Node> roots = findRoots(rules);
    // 用於記錄遍歷過的節點數和數組索引,用於結尾判斷圖是否存在環。
    int index = 0;

    Node node;
    // 依次取出根錨點集合的尾元素。
    while ((node = roots.pollLast()) != null) {
        final View view = node.view;
        final int key = view.getId();

        // 存儲節點對應的view引用。
        sorted[index++] = view;

        // 取出該節點的伴隨集合
        final ArrayMap<Node, DependencyGraph> dependents = node.dependents;
        final int count = dependents.size();
        // 遍歷伴隨節點
        for (int i = 0; i < count; i++) {
            final Node dependent = dependents.keyAt(i);
            final SparseArray<Node> dependencies = dependent.dependencies;

            // 從伴隨節點的錨點集合中移除當前根錨點自身
            dependencies.remove(key);
            if (dependencies.size() == 0) {
                // 若伴隨節點在移除根錨點後,沒有再依賴其他的節點的話,則將其當作新的根錨點,加入根錨點集合中。
                roots.add(dependent);
            }
        }
    }

    // 在完成上述根錨點集合遍歷後,會將所有節點按照依賴順序存儲在sorted數組中。
    // 若出現沒有存滿,說明出現了節點間循環依賴,RelativeLayout不允許child間循環依賴。
    if (index < sorted.length) {
        throw new IllegalStateException("Circular dependencies cannot exist"
                + " in RelativeLayout");
    }
}

圖例說明排序過程:

圖例

onMeasure測量

RelativeLayout的測量大致可以分成以下幾個階段:

  1. 構建依賴關係圖
  2. 初始化輔助計算變量參數
  3. 計算水平方向測量規格
  4. 計算垂直方向測量規格
  5. 調整wrap_content情況下的寬高
  6. 根據Gravity屬性調整位置
  7. 設置RelativeLayout自身的寬高

構建依賴關係圖

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 判斷是否構建過。當調用requestLayout方法時,會再次將mDirtyHierarchy置爲true。
    if (mDirtyHierarchy) {
        mDirtyHierarchy = false;
        // 將child按照特定規則排序。
        sortChildren();
    }
    
    // ···
}

關鍵邏輯在sortChildren方法中:

private void sortChildren() {
    // 初始化mSortedVerticalChildren和mSortedHorizontalChildren數組,用於存放排序後的child。
    final int count = getChildCount();
    if (mSortedVerticalChildren == null || mSortedVerticalChildren.length != count) {
        mSortedVerticalChildren = new View[count];
    }

    if (mSortedHorizontalChildren == null || mSortedHorizontalChildren.length != count) {
        mSortedHorizontalChildren = new View[count];
    }

    // 清空DependencyGraph
    final DependencyGraph graph = mGraph;
    graph.clear();

    // 往DependencyGraph中添加child,初始化DependencyGraph中數據。
    for (int i = 0; i < count; i++) {
        graph.add(getChildAt(i));
    }

    // 按照特定規則將child進行排序。
    graph.getSortedViews(mSortedVerticalChildren, RULES_VERTICAL);
    graph.getSortedViews(mSortedHorizontalChildren, RULES_HORIZONTAL);
}

該方法中將初始化依賴關係圖DependencyGraph,之後再對child進行排序,將結果保存在mSortedVerticalChildrenmSortedHorizontalChildren數組中。

DependencyGraph初始化邏輯在DependencyGraph.add方法中,通過add方法保存所有child節點:

void add(View view) {
    final int id = view.getId();
    // 從對象緩存池中取一個Node實例,Node將持有這個view引用。
    final Node node = Node.acquire(view);

    // 若view有設置ID,則根據ID保存至mKeyNodes。
    if (id != View.NO_ID) {
        mKeyNodes.put(id, node);
    }

    // mNodes保存所有節點。
    mNodes.add(node);
}

graph.getSortedViews方法前面有介紹過,將按照指定的相對位置規則對child進行排序。這裏傳入了兩組規則RULES_VERTICALRULES_HORIZONTAL

// 垂直相對位置依賴的規則,對應到LayoutParams.mRules數組的索引是[2,3,4,6,8]。
private static final int[] RULES_VERTICAL = {
        ABOVE, BELOW, ALIGN_BASELINE, ALIGN_TOP, ALIGN_BOTTOM
};
// 水平相對位置依賴的規則,對應到LayoutParams.mRules數組的索引是[0,1,5,7,16,17,18,19]。
private static final int[] RULES_HORIZONTAL = {
        LEFT_OF, RIGHT_OF, ALIGN_LEFT, ALIGN_RIGHT, START_OF, END_OF, ALIGN_START, ALIGN_END
};

這裏把child按照垂直和水平兩組相對位置規則分別進行排序,結果分別保存在mSortedVerticalChildren和mSortedHorizontalChildren數組中。

輔助變量參數準備

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // ···
    
    // 測量過程中RelativeLayout的臨時尺寸
    int myWidth = -1;
    int myHeight = -1;

    // RelativeLayout最終確定尺寸
    int width = 0;
    int height = 0;

    // 從RelativeLayout的測量規格中解析出模式和尺寸
    final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    final int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    // Record our dimensions if they are known;
    // myWidth、myHeight記錄EXACTLY或AT_MOST下的半明確的尺寸
    if (widthMode != MeasureSpec.UNSPECIFIED) {
        myWidth = widthSize;
    }

    if (heightMode != MeasureSpec.UNSPECIFIED) {
        myHeight = heightSize;
    }

    // width、height記錄明確的尺寸
    if (widthMode == MeasureSpec.EXACTLY) {
        width = myWidth;
    }

    if (heightMode == MeasureSpec.EXACTLY) {
        height = myHeight;
    }

    View ignore = null;
    // 從mGravity取出水平軸相關對齊方式的比特位
    int gravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
    // 標記是否設置了水平軸相關的對齊方式(START爲默認對齊,若設置了也可以忽略,所以這裏排除START)
    final boolean horizontalGravity = gravity != Gravity.START && gravity != 0;
    // 取出垂直駐歐相關對齊方式的比特位
    gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
    // 標記是否設置了垂直軸相關的對齊方式(TOP爲默認對齊,若設置了也可以忽略,所以這裏排除TOP)
    final boolean verticalGravity = gravity != Gravity.TOP && gravity != 0;

    // 記錄最寬鬆的左上右下四邊界約束。測量過程中會比較最小的左上邊界和最大的右下邊界,故此處默認值設爲MAX_VALUE和MIN_VALUE。
    int left = Integer.MAX_VALUE;
    int top = Integer.MAX_VALUE;
    int right = Integer.MIN_VALUE;
    int bottom = Integer.MIN_VALUE;

    // 標記是否有child設置了layout_alignParentEnd屬性
    boolean offsetHorizontalAxis = false;
    // 標記是否有child設置了layout_alignParentBottom屬性
    boolean offsetVerticalAxis = false;

    if ((horizontalGravity || verticalGravity) && mIgnoreGravity != View.NO_ID) {
        // 獲取忽略RelativeLayout.gravity限制的child
        ignore = findViewById(mIgnoreGravity);
    }

    // 標記RelativeLayout尺寸是否是明確的
    final boolean isWrapContentWidth = widthMode != MeasureSpec.EXACTLY;
    final boolean isWrapContentHeight = heightMode != MeasureSpec.EXACTLY;

    // We need to know our size for doing the correct computation of children positioning in RTL
    // mode but there is no practical way to get it instead of running the code below.
    // So, instead of running the code twice, we just set the width to a "default display width"
    // before the computation and then, as a last pass, we will update their real position with
    // an offset equals to "DEFAULT_WIDTH - width".
    // 獲取佈局方向,RTL或LTR
    final int layoutDirection = getLayoutDirection();
    if (isLayoutRtl() && myWidth == -1) {
        // 若佈局方向是從右至左且寬度未知,則設定一個默認寬度。
        myWidth = DEFAULT_WIDTH;
    }
    
    // ···
}

水平方向依賴約束測量

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // ···

    // 獲取根據水平規則排序的child數組。
    View[] views = mSortedHorizontalChildren;
    int count = views.length;

    for (int i = 0; i < count; i++) {
        View child = views[i];
        if (child.getVisibility() != GONE) {
            LayoutParams params = (LayoutParams) child.getLayoutParams();
            // 獲取LayoutParams.mRules,用於獲取child設置的所有規則屬性值。
            // 方法中還會根據佈局方向調整屬性值,將toStartOf、toEndOf、alignStart、alignEnd、alignParentStart、alignParentEnd的view ID轉到
            // 對應的toLeftOf、toRightOf、alignLeft、alignRight、alignParentLeft、alignParentRight上。
            // 
            int[] rules = params.getRules(layoutDirection);

            // 根據規則調整child的LayoutParams的左右邊界約束。
            applyHorizontalSizeRules(params, myWidth, rules);
            // 分發child測量
            measureChildHorizontal(child, params, myWidth, myHeight);

            // 利用child測量後獲取child的測量寬度,再次調整child的左右邊界約束,並返回是否設置了layout_alignParentEnd屬性。
            if (positionChildHorizontal(child, params, myWidth, isWrapContentWidth)) {
                offsetHorizontalAxis = true;
            }
        }
    }
    
    // ···
}

這個階段會先調整child的左右邊界約束,並進行一次child分發測量。注意,此次測量僅保證寬度測量規格是準確的。

進入applyHorizontalSizeRules方法,看RelativeLayout如何對child進行邊界約束:

private void applyHorizontalSizeRules(LayoutParams childParams, int myWidth, int[] rules) {
    RelativeLayout.LayoutParams anchorParams;

    // VALUE_NOT_SET indicates a "soft requirement" in that direction. For example:
    // left=10, right=VALUE_NOT_SET means the view must start at 10, but can go as far as it
    // wants to the right
    // left=VALUE_NOT_SET, right=10 means the view must end at 10, but can go as far as it
    // wants to the left
    // left=10, right=20 means the left and right ends are both fixed
    childParams.mLeft = VALUE_NOT_SET;
    childParams.mRight = VALUE_NOT_SET;

    // 獲取該child的toLeftOf的那個錨點view的LayoutParams(若有的話)。
    // getRelatedViewParams方法會從DependencyGraph中查找,根據LEFT_OF從rules對
    // 應索引處取得view ID,因爲DependencyGraph中保存了view ID和節點的映射集合,所
    // 以可以很方便的找到。若找到的錨點爲GONE,則再找錨點的錨點。
    anchorParams = getRelatedViewParams(rules, LEFT_OF);
    if (anchorParams != null) {
        // 約束child的右邊界爲錨點的左邊界,再減去margin。
        childParams.mRight = anchorParams.mLeft - (anchorParams.leftMargin +
                childParams.rightMargin);
    } else if (childParams.alignWithParent && rules[LEFT_OF] != 0) {
        // 上面沒找到錨點的原因有可能是錨點都爲GONE,或者child沒有設置toLeftOf規則屬性。
        // 這裏再判斷child是否設置toLeftOf和alignWithParentIfMissing屬性。
        
        // 此時RelativeLayout的寬度不爲UNSPECIFIED的情況,進行右邊界約束,否則需要
        // 等到child測量完成後,再進行約束。
        if (myWidth >= 0) {
            // 約束child的右邊界爲RelativeLayout的右邊界,再減去padding和margin。
            childParams.mRight = myWidth - mPaddingRight - childParams.rightMargin;
        }
    }

    // 獲取child的toRightOf的錨點LayoutParams
    anchorParams = getRelatedViewParams(rules, RIGHT_OF);
    if (anchorParams != null) {
        // 約束child左邊界爲錨點的右邊界,再加上margin。
        childParams.mLeft = anchorParams.mRight + (anchorParams.rightMargin +
                childParams.leftMargin);
    } else if (childParams.alignWithParent && rules[RIGHT_OF] != 0) {
        // 約束child左邊界爲RelativeLayout的左邊界,再加上padding和margin。
        childParams.mLeft = mPaddingLeft + childParams.leftMargin;
    }

    // 獲取child的alignLeft的錨點LayoutParams
    anchorParams = getRelatedViewParams(rules, ALIGN_LEFT);
    if (anchorParams != null) {
        // 約束左邊界爲錨點左邊界
        childParams.mLeft = anchorParams.mLeft + childParams.leftMargin;
    } else if (childParams.alignWithParent && rules[ALIGN_LEFT] != 0) {
        // 約束左邊界爲RelativeLayout左邊界
        childParams.mLeft = mPaddingLeft + childParams.leftMargin;
    }

    // 獲取alignRight的錨點LayoutParams
    anchorParams = getRelatedViewParams(rules, ALIGN_RIGHT);
    if (anchorParams != null) {
        // 約束右邊界爲錨點右邊界
        childParams.mRight = anchorParams.mRight - childParams.rightMargin;
    } else if (childParams.alignWithParent && rules[ALIGN_RIGHT] != 0) {
        if (myWidth >= 0) {
            // 約束右邊界爲RelativeLayout右邊界
            childParams.mRight = myWidth - mPaddingRight - childParams.rightMargin;
        }
    }

    if (0 != rules[ALIGN_PARENT_LEFT]) {
        // 若設置了alignParentLeft規則屬性,則約束左邊界爲RelativeLayout左邊界。
        childParams.mLeft = mPaddingLeft + childParams.leftMargin;
    }

    if (0 != rules[ALIGN_PARENT_RIGHT]) {
        if (myWidth >= 0) {
            // 若設置了alignParentRight,則約束右邊界爲RelativeLayout右邊界。
            childParams.mRight = myWidth - mPaddingRight - childParams.rightMargin;
        }
    }
}

接着進入measureChildHorizontal方法,看分發child測量:

private void measureChildHorizontal(
        View child, LayoutParams params, int myWidth, int myHeight) {
    // 爲child生成寬度測量規格。
    final int childWidthMeasureSpec = getChildMeasureSpec(params.mLeft, params.mRight,
            params.width, params.leftMargin, params.rightMargin, mPaddingLeft, mPaddingRight,
            myWidth);

    final int childHeightMeasureSpec;
    if (myHeight < 0 && !mAllowBrokenMeasureSpecs) {
        // 若高度測量模式爲UNSPECIFIED(不考慮低版本)
        if (params.height >= 0) {
            // 若child的LayoutParams.height設置了明確的像素值,則爲child生成精確的高度測量規格。
            childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                    params.height, MeasureSpec.EXACTLY);
        } else {
            // Negative values in a mySize/myWidth/myWidth value in
            // RelativeLayout measurement is code for, "we got an
            // unspecified mode in the RelativeLayout's measure spec."
            // Carry it forward.
            // 爲child也生成UNSPECIFIED的高度測量規格。
            childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        }
    } else {
        final int maxHeight;
        // 高版本mMeasureVerticalWithPaddingMargin爲true,低版本可忽略不計。
        if (mMeasureVerticalWithPaddingMargin) {
            // 計算child的高度,並確保不小於0。
            maxHeight = Math.max(0, myHeight - mPaddingTop - mPaddingBottom
                    - params.topMargin - params.bottomMargin);
        } else {
            maxHeight = Math.max(0, myHeight);
        }

        final int heightMode;
        if (params.height == LayoutParams.MATCH_PARENT) {
            heightMode = MeasureSpec.EXACTLY;
        } else {
            heightMode = MeasureSpec.AT_MOST;
        }
        // 爲child生成高度測量規格(此時的高度規格不一定準確)。
        childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight, heightMode);
    }

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

該方法中爲child生成了寬度和高度測量規格,之後調用child測量。其中調用了getChildMeasureSpec方法生成寬度測量規格,接下來看這個方法:

/**
 * @param childStart The left or top field of the child's layout params
 * @param childEnd The right or bottom field of the child's layout params
 * @param childSize The child's desired size (the width or height field of
 *        the child's layout params)
 * @param startMargin The left or top margin
 * @param endMargin The right or bottom margin
 * @param startPadding mPaddingLeft or mPaddingTop
 * @param endPadding mPaddingRight or mPaddingBottom
 * @param mySize The width or height of this view (the RelativeLayout)
 * @return MeasureSpec for the child
 */
private int getChildMeasureSpec(int childStart, int childEnd,
        int childSize, int startMargin, int endMargin, int startPadding,
        int endPadding, int mySize) {
    int childSpecMode = 0;
    int childSpecSize = 0;

    // Negative values in a mySize value in RelativeLayout
    // measurement is code for, "we got an unspecified mode in the
    // RelativeLayout's measure spec."
    // RelativeLayout自身測量規格模式爲UNSPECIFIED時,mySize值爲-1。
    final boolean isUnspecified = mySize < 0;
    // mAllowBrokenMeasureSpecs在高版本爲false,低版本忽略不計。
    if (isUnspecified && !mAllowBrokenMeasureSpecs) {
        if (childStart != VALUE_NOT_SET && childEnd != VALUE_NOT_SET) {
            // 若左/上和右/下都設置了約束,則計算區間距離作爲尺寸,測量規格爲精確值。
            // Constraints fixed both edges, so child has an exact size.
            childSpecSize = Math.max(0, childEnd - childStart);
            childSpecMode = MeasureSpec.EXACTLY;
        } else if (childSize >= 0) {
            // 若child的LayoutParams.width/height設置了精確像素值,則測量規格也是精確值。
            // The child specified an exact size.
            childSpecSize = childSize;
            childSpecMode = MeasureSpec.EXACTLY;
        } else {
            // 爲child生成的測量規格模式也沿用UNSPECIFIED。
            // Allow the child to be whatever size it wants.
            childSpecSize = 0;
            childSpecMode = MeasureSpec.UNSPECIFIED;
        }

        // 生成測量規格並返回。
        return MeasureSpec.makeMeasureSpec(childSpecSize, childSpecMode);
    }

    // Figure out start and end bounds.
    int tempStart = childStart;
    int tempEnd = childEnd;

    // If the view did not express a layout constraint for an edge, use
    // view's margins and our padding
    // 若child沒有設置左/上邊界約束,則以RelativeLayout左邊界加上padding和margin作爲左/上邊界。
    if (tempStart == VALUE_NOT_SET) {
        tempStart = startPadding + startMargin;
    }
    // 若child沒有設置右/下邊界約束,則以RelativeLayout右邊界減去padding和margin作爲右/下邊界。
    if (tempEnd == VALUE_NOT_SET) {
        tempEnd = mySize - endPadding - endMargin;
    }

    // Figure out maximum size available to this view
    // 計算區間距離作爲約束下的可用尺寸。
    final int maxAvailable = tempEnd - tempStart;

    if (childStart != VALUE_NOT_SET && childEnd != VALUE_NOT_SET) {
        // 左/上和右/下都設置了約束,則尺寸可以確定(在高版本中,此處的isUnspecified值爲false)。
        // Constraints fixed both edges, so child must be an exact size.
        childSpecMode = isUnspecified ? MeasureSpec.UNSPECIFIED : MeasureSpec.EXACTLY;
        childSpecSize = Math.max(0, maxAvailable);
    } else {
        if (childSize >= 0) {
            // child的LayoutParams設置了精確像素值。
            // Child wanted an exact size. Give as much as possible.
            childSpecMode = MeasureSpec.EXACTLY;

            if (maxAvailable >= 0) {
                // 確保child的尺寸不得超過約束尺寸。
                // We have a maximum size in this dimension.
                childSpecSize = Math.min(maxAvailable, childSize);
            } else {
                // We can grow in this dimension.
                childSpecSize = childSize;
            }
        } else if (childSize == LayoutParams.MATCH_PARENT) {
            // child的LayoutParams爲填充父佈局,以約束尺寸作爲child的尺寸。
            // Child wanted to be as big as possible. Give all available
            // space.
            childSpecMode = isUnspecified ? MeasureSpec.UNSPECIFIED : MeasureSpec.EXACTLY;
            childSpecSize = Math.max(0, maxAvailable);
        } else if (childSize == LayoutParams.WRAP_CONTENT) {
            // child的LayoutParams爲恰好包裹內容。
            // Child wants to wrap content. Use AT_MOST to communicate
            // available space if we know our max size.
            if (maxAvailable >= 0) {
                // We have a maximum size in this dimension.
                childSpecMode = MeasureSpec.AT_MOST;
                // 以約束尺寸作爲最大限制尺寸。
                childSpecSize = maxAvailable;
            } else {
                // We can grow in this dimension. Child can be as big as it
                // wants.
                childSpecMode = MeasureSpec.UNSPECIFIED;
                childSpecSize = 0;
            }
        }
    }

    // 生成測量規格並返回。
    return MeasureSpec.makeMeasureSpec(childSpecSize, childSpecMode);
}

該方法中根據child的LayoutParams的邊界約束和LayoutParams的width/height值和RelativeLayout自身測量規格以及間距值,結合RelativeLayout佈局特性,生成測量規格。

回到onMeasure方法中,在執行完measureChildHorizontal方法後,緊接着執行positionChildHorizontal方法

private boolean positionChildHorizontal(View child, LayoutParams params, int myWidth,
        boolean wrapContent) {

    final int layoutDirection = getLayoutDirection();
    // 獲取轉換過得mRules
    int[] rules = params.getRules(layoutDirection);

    if (params.mLeft == VALUE_NOT_SET && params.mRight != VALUE_NOT_SET) {
        // child右邊界已設置,左邊界未設置,這裏進行設置。
        // Right is fixed, but left varies
        // 經過剛纔的測量,child的寬度可以確定,這裏通過用右邊界減去child寬度求得左邊界。
        params.mLeft = params.mRight - child.getMeasuredWidth();
    } else if (params.mLeft != VALUE_NOT_SET && params.mRight == VALUE_NOT_SET) {
        // Left is fixed, but right varies
        // 通過用左邊界加上child寬度求得右邊界。
        params.mRight = params.mLeft + child.getMeasuredWidth();
    } else if (params.mLeft == VALUE_NOT_SET && params.mRight == VALUE_NOT_SET) {
        // 左右邊界都未設置
        // Both left and right vary
        if (rules[CENTER_IN_PARENT] != 0 || rules[CENTER_HORIZONTAL] != 0) {
            // 該child設置了centerInParent或centerHorizontal屬性,即爲水平居中。
            // 判斷RelativeLayout的測量規格模式是否爲EXACTLY。
            if (!wrapContent) {
                // 測量模式爲EXACTLY,RelativeLayout的寬度爲明確像素值。
                // centerHorizontal方法利用child寬度和RelativeLayout寬度設置child的左右約束。
                centerHorizontal(child, params, myWidth);
            } else {
                // positionAtEdge方法以RelativeLayout左右邊界和child寬度計算設置child的左右約束。
                positionAtEdge(child, params, myWidth);
            }
            return true;
        } else {
            // This is the default case. For RTL we start from the right and for LTR we start
            // from the left. This will give LEFT/TOP for LTR and RIGHT/TOP for RTL.
            positionAtEdge(child, params, myWidth);
        }
    }
    // 這裏的rules已被轉換過,因此START和END相關屬性值都爲0,必定返回false。
    return rules[ALIGN_PARENT_END] != 0;
}

positionChildHorizontal方法作用是補充設置child的左右邊界約束。在前面流程中child的左右約束根據自身設置屬性來設置,存在未設置約束的邊界,因此在child寬度準確測量完後,利用child寬度再設置左右約束。

垂直方向依賴約束測量

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // ···
    
    // 獲取根據垂直規則排序的child數組
    views = mSortedVerticalChildren;
    count = views.length;
    final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;

    for (int i = 0; i < count; i++) {
        final View child = views[i];
        if (child.getVisibility() != GONE) {
            final LayoutParams params = (LayoutParams) child.getLayoutParams();

            // 和applyHorizontalSizeRules方法作用類似,根據child設置的相對位置屬性設置上下邊界約束。
            applyVerticalSizeRules(params, myHeight, child.getBaseline());
            // 分發child測量(此次測量確定了準確的child的高度)。
            measureChild(child, params, myWidth, myHeight);
            // positionChildVertical分發和positionChildHorizontal類似,在
            // child測量後,利用child高度補充設置上下邊界。該方法固定返回false。
            if (positionChildVertical(child, params, myHeight, isWrapContentHeight)) {
                offsetVerticalAxis = true;
            }

            if (isWrapContentWidth) {
                // RelativeLayout的寬度規格模式不爲EXACTLY。
                // 記錄RelativeLayout至少需要的最大寬度。
                if (isLayoutRtl()) {
                    if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
                        width = Math.max(width, myWidth - params.mLeft);
                    } else {
                        width = Math.max(width, myWidth - params.mLeft + params.leftMargin);
                    }
                } else {
                    if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
                        width = Math.max(width, params.mRight);
                    } else {
                        width = Math.max(width, params.mRight + params.rightMargin);
                    }
                }
            }

            if (isWrapContentHeight) {
                // RelativeLayout的寬度規格模式不爲EXACTLY。
                // 記錄RelativeLayout至少需要的最大高度。
                if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
                    height = Math.max(height, params.mBottom);
                } else {
                    height = Math.max(height, params.mBottom + params.bottomMargin);
                }
            }

            if (child != ignore || verticalGravity) {
                // 若RelativeLayout設置了垂直軸相關的對齊方式,且該child沒有忽略,記錄最靠左的左邊界。
                left = Math.min(left, params.mLeft - params.leftMargin);
                // 記錄最靠上的上邊界。
                top = Math.min(top, params.mTop - params.topMargin);
            }

            if (child != ignore || horizontalGravity) {
                // 記錄最靠右的右邊界。
                right = Math.max(right, params.mRight + params.rightMargin);
                // 記錄最靠下的下邊界。
                bottom = Math.max(bottom, params.mBottom + params.bottomMargin);
            }
        }
    }
    
    // ···
}

垂直方向依賴約束的測量邏輯和水平方向的類似,遍歷child,根據child設置的規則屬性設置上下邊界約束,然後爲child生成寬高測量規格,調用child測量,最後再利用child測量高度補充設置之前未設置的上下邊界。

同時在這個階段還會計算RelativeLayout至少需要的最大寬度和最大高度,以及左上右下邊界範圍。

到此RelativeLayout共經過了兩次遍歷child分發測量

調整WrapContent情況下的寬高

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // ···
    
    // 省略查找mBaselineView部分。通過遍歷未GONE的child,比較最左上位置的child,作爲mBaselineView。
    // ···
    
    if (isWrapContentWidth) {
        // RelativeLayout的寬度規格模式不爲EXACTLY
        // Width already has left padding in it since it was calculated by looking at
        // the right of each child view
        width += mPaddingRight;

        if (mLayoutParams != null && mLayoutParams.width >= 0) {
            // 和自身的LayoutParams.width比較最大值
            width = Math.max(width, mLayoutParams.width);
        }

        // 確保不小於最小寬度
        width = Math.max(width, getSuggestedMinimumWidth());
        // 獲取調整後的尺寸(根據測量規格的模式和尺寸限制調整期望尺寸,並設置狀態位)
        width = resolveSize(width, widthMeasureSpec);

        // offsetHorizontalAxis固定爲false,以下可忽略
        if (offsetHorizontalAxis) {
            for (int i = 0; i < count; i++) {
                final View child = views[i];
                if (child.getVisibility() != GONE) {
                    final LayoutParams params = (LayoutParams) child.getLayoutParams();
                    final int[] rules = params.getRules(layoutDirection);
                    if (rules[CENTER_IN_PARENT] != 0 || rules[CENTER_HORIZONTAL] != 0) {
                        centerHorizontal(child, params, width);
                    } else if (rules[ALIGN_PARENT_RIGHT] != 0) {
                        final int childWidth = child.getMeasuredWidth();
                        params.mLeft = width - mPaddingRight - childWidth;
                        params.mRight = params.mLeft + childWidth;
                    }
                }
            }
        }
    }

    if (isWrapContentHeight) {
        // RelativeLayout的高度規格模式不爲EXACTLY
        // Height already has top padding in it since it was calculated by looking at
        // the bottom of each child view
        height += mPaddingBottom;

        if (mLayoutParams != null && mLayoutParams.height >= 0) {
            // 和自身的LayoutParams.height比較最大值
            height = Math.max(height, mLayoutParams.height);
        }

        // 確保不小於最小高度
        height = Math.max(height, getSuggestedMinimumHeight());
        // 獲取調整後的尺寸(根據測量規格的模式和尺寸限制調整期望尺寸,並設置狀態位)
        height = resolveSize(height, heightMeasureSpec);

        // offsetVerticalAxis固定爲false,以下可忽略
        if (offsetVerticalAxis) {
            for (int i = 0; i < count; i++) {
                final View child = views[i];
                if (child.getVisibility() != GONE) {
                    final LayoutParams params = (LayoutParams) child.getLayoutParams();
                    final int[] rules = params.getRules(layoutDirection);
                    if (rules[CENTER_IN_PARENT] != 0 || rules[CENTER_VERTICAL] != 0) {
                        centerVertical(child, params, height);
                    } else if (rules[ALIGN_PARENT_BOTTOM] != 0) {
                        final int childHeight = child.getMeasuredHeight();
                        params.mTop = height - mPaddingBottom - childHeight;
                        params.mBottom = params.mTop + childHeight;
                    }
                }
            }
        }
    }
}

根據Gravity屬性調整位置

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // ···
    
    // RelativeLayout是否設置了gravity屬性
    if (horizontalGravity || verticalGravity) {
        final Rect selfBounds = mSelfBounds;
        // 設置RelativeLayout的區域
        selfBounds.set(mPaddingLeft, mPaddingTop, width - mPaddingRight,
                height - mPaddingBottom);

        final Rect contentBounds = mContentBounds;
        // 該方法將根據mGravity計算contentBounds應該位於selfBounds中的位置區域。
        // 假設RelativeLayout設置right|bottom對齊方式,selfBounds=Rect(0,0,680,1032),right - left=600,
        // bottom - top=400,排布方向爲LTR,則計算完成後contentBounds=Rect(80,632,680,1032)。
        Gravity.apply(mGravity, right - left, bottom - top, selfBounds, contentBounds,
                layoutDirection);

        // 計算水平和垂直偏移量
        final int horizontalOffset = contentBounds.left - left;
        final int verticalOffset = contentBounds.top - top;
        if (horizontalOffset != 0 || verticalOffset != 0) {
            for (int i = 0; i < count; i++) {
                final View child = views[i];
                if (child.getVisibility() != GONE && child != ignore) {
                    // 跳過GONE和ignoreGravity屬性設置的view
                  
                    // 爲每個child的四邊界約束加上偏移量
                    final LayoutParams params = (LayoutParams) child.getLayoutParams();
                    if (horizontalGravity) {
                        params.mLeft += horizontalOffset;
                        params.mRight += horizontalOffset;
                    }
                    if (verticalGravity) {
                        params.mTop += verticalOffset;
                        params.mBottom += verticalOffset;
                    }
                }
            }
        }
    }
}

設置RelativeLayout自身的尺寸

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // ···

    // 判斷是否從右至左佈局
    if (isLayoutRtl()) {
        final int offsetWidth = myWidth - width;
        for (int i = 0; i < count; i++) {
            final View child = views[i];
            if (child.getVisibility() != GONE) {
                final LayoutParams params = (LayoutParams) child.getLayoutParams();
                params.mLeft -= offsetWidth;
                params.mRight -= offsetWidth;
            }
        }
    }

    // 設置寬高尺寸
    setMeasuredDimension(width, height);
}

至此完成了RelativeLayout的測量流程。

onLayout佈局

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    //  The layout has actually already been performed and the positions
    //  cached.  Apply the cached values to the children.
    final int count = getChildCount();

    // 遍歷child
    for (int i = 0; i < count; i++) {
        View child = getChildAt(i);
        if (child.getVisibility() != GONE) {
            RelativeLayout.LayoutParams st =
                    (RelativeLayout.LayoutParams) child.getLayoutParams();
            // 使用child的LayoutParams的上下左右約束作爲邊界對child進行佈局。
            child.layout(st.mLeft, st.mTop, st.mRight, st.mBottom);
        }
    }
}

RelativeLayout的佈局流程很簡單,即利用測量階段確定的各child的四邊界約束進行佈局。

onDraw繪製

RelativeLayout沒有重寫該方法,無需特殊繪製。

總結

RelativeLayout較核心較複雜的邏輯都在onMeasure測量階段。在測量過程中會進行兩次child分發測量,先準確測量child寬度,再準確測量child高度。RelativeLayout在測量階段依靠依賴關係圖爲各child設置上下左右四邊界的約束,再借助四邊界約束確定child的尺寸。
在onLayout佈局階段中,直接利用各child的四邊界約束進行佈局。

發佈了27 篇原創文章 · 獲贊 2 · 訪問量 8510
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章