文章目錄
前言
通過學習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可以設置ignoreGravity和gravity屬性,其中gravity屬性會凌駕於子view的相對位置規則屬性,而ignoreGravity可以設置不受gravity屬性影響的子view。
舉例說明:
- 雖然子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>
- 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的測量大致可以分成以下幾個階段:
- 構建依賴關係圖
- 初始化輔助計算變量參數
- 計算水平方向測量規格
- 計算垂直方向測量規格
- 調整wrap_content情況下的寬高
- 根據Gravity屬性調整位置
- 設置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進行排序,將結果保存在mSortedVerticalChildren和mSortedHorizontalChildren數組中。
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_VERTICAL和RULES_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的四邊界約束進行佈局。