背景
最近在項(xiàng)目中使用 RelativeLayout 的過程當(dāng)中發(fā)現(xiàn) RelativeLayout 內(nèi)部的孩子 View 節(jié)點(diǎn)會收到兩次 measure 消息术吝,導(dǎo)致項(xiàng)目中某個自定義控件的尺寸和預(yù)期的不一致计济。本著出了問題需要知道為什么茸苇,并且需要給出合理的解決方案這樣的原則,因此在閱讀分析完源碼的基礎(chǔ)上輸出個人對 RelativeLayout 的一些理解沦寂,在此進(jìn)行記錄和分享学密。
是什么
- RelativeLayout
- Android SDK API 1 中添加的一個類,該類用于解決界面中多個控件之間的相對布局問題传藏,可以通過描述當(dāng)前孩子與其它孩子或者與父節(jié)點(diǎn)的相對位置決定的當(dāng)前孩子布局信息
- 在類的繼承結(jié)構(gòu)上腻暮,它是 ViewGroup 的子類
- 在運(yùn)行時,它是 View 控件樹中的非葉子節(jié)點(diǎn)毯侦,它可以包含孩子節(jié)點(diǎn)
- 拓?fù)渑判?
- 是一種求有向無環(huán)圖的拓?fù)湫蛄袝r使用的排序方法
- 該方法的輸入是有向無環(huán)圖哭靖,輸出是拓?fù)湫蛄?/li>
為什么
- 為了解決大量應(yīng)用層開發(fā)者在開發(fā) APP 時需要實(shí)現(xiàn)界面中多個控件之間的相對位置的需求,SDK 開發(fā)者們設(shè)計(jì)并實(shí)現(xiàn)的一個類/庫/模塊侈离。
- 思考一下试幽,如果讓我設(shè)計(jì)并實(shí)現(xiàn)一個模塊來解決這些具有相對位置關(guān)系的控件的布局問題,我會怎么做卦碾?這是目前我認(rèn)為能夠很好地考驗(yàn)一個人解決問題的能力的思維方式
內(nèi)部原理
在閱讀 RelativeLayout 的源碼時铺坞,發(fā)現(xiàn)其為了解決多個孩子節(jié)點(diǎn)之間相對位置的問題,應(yīng)用了數(shù)據(jù)結(jié)構(gòu)中圖的拓?fù)渑判騺泶_定孩子節(jié)點(diǎn)測量的先后順序洲胖。我們一起來看下如何計(jì)算出一些具有依賴關(guān)系的任務(wù)的執(zhí)行順序济榨。當(dāng)然這只是一種參考解法,肯定還有其他解法宾濒。
- 數(shù)據(jù)結(jié)構(gòu)(記錄相關(guān)信息)
- 節(jié)點(diǎn)
android.widget.RelativeLayout.DependencyGraph.Node
依賴關(guān)系圖中的一個節(jié)點(diǎn)腿短,封裝了一個 View,以及 View 的依賴節(jié)點(diǎn)和被依賴節(jié)點(diǎn)绘梦。簡單來說就是有向圖中的一個節(jié)點(diǎn),并且該節(jié)點(diǎn)知道自己的入度和出度信息赴魁。入度為 0 的節(jié)點(diǎn)被稱為根節(jié)點(diǎn) - 恍斗睢(邊)
android.widget.RelativeLayout.LayoutParams#mRules
每個 RelativeLayout 的孩子節(jié)點(diǎn)的布局參數(shù)中都記錄了當(dāng)前節(jié)點(diǎn)依賴于其它節(jié)點(diǎn)的信息。孩子節(jié)點(diǎn)采用 id 標(biāo)識颖御,每個節(jié)點(diǎn)可以用于描述的相對位置參數(shù)有 22 個(android.widget.RelativeLayout#VERB_COUNT)榄棵。采用 int 類型的數(shù)組記錄依賴信息,數(shù)組下標(biāo)是相對位置參數(shù)潘拱,數(shù)組里面的值是依賴節(jié)點(diǎn)的 ID - 圖
android.widget.RelativeLayout.DependencyGraph
記錄依賴關(guān)系的圖信息疹鳄。記錄一個 RelativeLayout 實(shí)例所持有的孩子節(jié)點(diǎn)信息。
- 節(jié)點(diǎn)
- 算法(給定輸入/輸出芦岂,實(shí)現(xiàn)計(jì)算過程)
- 添加節(jié)點(diǎn)
android.widget.RelativeLayout.DependencyGraph#add
- 添加節(jié)點(diǎn)
void add(View view) {
final int id = view.getId();
// 把 View 包裝到 Node 實(shí)例內(nèi)部
final Node node = Node.acquire(view);
if (id != View.NO_ID) {
// 記錄 ID 到 Node 的映射關(guān)系瘪弓,為了接下來的查詢效率
mKeyNodes.put(id, node);
}
// 添加一個圖節(jié)點(diǎn)
mNodes.add(node);
}
- 添加邊
android.widget.RelativeLayout.LayoutParams#LayoutParams(android.content.Context, android.util.AttributeSet)
在創(chuàng)建 LayoutParams 對象時,從 AttributeSet 中解析邊信息
final int N = a.getIndexCount();
// 遍歷屬性集合
for (int i = 0; i < N; i++) {
// 讀取屬性索引號
int attr = a.getIndex(i);
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:
// 讀取被依賴 View 的 ID
// 當(dāng)前節(jié)點(diǎn) LEFT_OF 于 attr 對應(yīng)的 res id
rules[LEFT_OF] = a.getResourceId(attr, 0);
break;
// 省略其它依賴規(guī)則解析
...
- 計(jì)算拓?fù)湫蛄?br> android.widget.RelativeLayout.DependencyGraph#getSortedViews
// 輸出指定邊規(guī)則后的拓?fù)湫蛄?void getSortedViews(View[] sorted, int... rules) {
// 找到所有根節(jié)點(diǎn)放進(jìn)雙端隊(duì)列存放起來
final ArrayDeque<Node> roots = findRoots(rules);
int index = 0;
Node node;
// 遍歷根節(jié)點(diǎn)隊(duì)列
while ((node = roots.pollLast()) != null) {
//解封得到 View 對象
final View view = node.view;
final int key = view.getId();
// 輸出到拓?fù)湫蛄袛?shù)組中
sorted[index++] = view;
// 找到當(dāng)前根節(jié)點(diǎn)的出度信息
final ArrayMap<Node, DependencyGraph> dependents = node.dependents;
final int count = dependents.size();
// 遍歷出度節(jié)點(diǎn)
for (int i = 0; i < count; i++) {
final Node dependent = dependents.keyAt(i);
// 入度信息
final SparseArray<Node> dependencies = dependent.dependencies;
// 刪除入度
dependencies.remove(key);
// 入度為 0
if (dependencies.size() == 0) {
// 成為新的根節(jié)點(diǎn)禽最,加入到雙端隊(duì)列中
roots.add(dependent);
}
}
}
// 如果拓?fù)湫蛄兄泄?jié)點(diǎn)個數(shù)和圖中所有節(jié)點(diǎn)的個數(shù)不等腺怯,則存在環(huán)
if (index < sorted.length) {
throw new IllegalStateException("Circular dependencies cannot exist"
+ " in RelativeLayout");
}
}
android.widget.RelativeLayout.DependencyGraph#findRoots
// 根據(jù)參數(shù)中選擇的依賴規(guī)則找到所有根節(jié)點(diǎn)
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
// 遍歷圖中所有節(jié)點(diǎn)袱饭,構(gòu)建節(jié)點(diǎn)的入度和出度信息
for (int i = 0; i < count; i++) {
// 讀取當(dāng)前節(jié)點(diǎn)
final Node node = nodes.get(i);
final LayoutParams layoutParams = (LayoutParams) node.view.getLayoutParams();
// 從 LayoutParams 中讀取依賴信息
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
// 根據(jù)參數(shù)中選取的部分規(guī)則構(gòu)建依賴信息
for (int j = 0; j < rulesCount; j++) {
// 拿到被依賴對象的 ID
final int rule = rules[rulesFilter[j]];
if (rule > 0) {
// The node this node depends on
final Node dependency = keyNodes.get(rule);
// Skip unknowns and self dependencies
if (dependency == null || dependency == node) {
continue;
}
// Add the current node as a dependent
// 被依賴節(jié)點(diǎn)記錄入度信息
dependency.dependents.put(node, this);
// Add a dependency to the current node
// 當(dāng)前節(jié)點(diǎn)記錄出度信息
node.dependencies.put(rule, dependency);
}
}
}
final ArrayDeque<Node> roots = mRoots;
// 清理之前的計(jì)算結(jié)果
roots.clear();
// Finds all the roots in the graph: all nodes with no dependencies
// 遍歷所有節(jié)點(diǎn)
for (int i = 0; i < count; i++) {
final Node node = nodes.get(i);
if (node.dependencies.size() == 0)
// 入度為 0 則為根節(jié)點(diǎn)
roots.addLast(node);
}
// 輸出根節(jié)點(diǎn)隊(duì)列
return roots;
}
練習(xí)題
- 拷貝 RelativeLayout 的代碼和資源到自己的工程中,并應(yīng)用拷貝過的 RelativeLayout 進(jìn)行基本使用
- 修改拷貝的 RelativeLayout 代碼呛占,打印 RelativeLayout 添加節(jié)點(diǎn)虑乖、添加邊、拓?fù)渑判虻墓ぷ髁鞒倘罩拘畔?/li>
- 實(shí)現(xiàn)圖的鄰接矩陣晾虑、鄰接鏈表的基本操作
- 根據(jù)百科中拓?fù)渑判虻乃惴ń榻B疹味,手動實(shí)現(xiàn)簡單的拓?fù)渑判?/li>
- 同一個圖支持多次根據(jù)輸入的邊信息輸出拓?fù)湫蛄?/li>
- 刪除 RelativeLayout 中的相關(guān)拓?fù)渑判?API 的實(shí)現(xiàn),自己手動實(shí)現(xiàn)一遍帜篇,并確保測試用例可以跑過
- 輸出拓?fù)渑判蛟?RelativeLayout 中應(yīng)用的文章
總結(jié)
到目前為止佛猛,RelativeLayout 已經(jīng)計(jì)算出所有孩子節(jié)點(diǎn)的依賴關(guān)系,接下來可以根據(jù)拓?fù)湫蛄兄械捻樞騺硐蚝⒆庸?jié)點(diǎn)發(fā)送 measure 消息坠狡,從而計(jì)算出 RelativeLayout 的尺寸信息
參考
RelativeLayout 文檔
RelativeLayout 代碼
拓?fù)渑判?/a>