拓?fù)渑判蛟?RelativeLayout 中的應(yīng)用

背景

最近在項(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)信息。
  • 算法(給定輸入/輸出芦岂,實(shí)現(xiàn)計(jì)算過程)
    • 添加節(jié)點(diǎn)
      android.widget.RelativeLayout.DependencyGraph#add
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>

  • 序言:七十年代末继找,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子逃沿,更是在濱河造成了極大的恐慌婴渡,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件凯亮,死亡現(xiàn)場離奇詭異边臼,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)假消,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進(jìn)店門柠并,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人富拗,你說我怎么就攤上這事臼予。” “怎么了啃沪?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵粘拾,是天一觀的道長。 經(jīng)常有香客問我创千,道長缰雇,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任追驴,我火速辦了婚禮械哟,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘殿雪。我一直安慰自己暇咆,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布冠摄。 她就那樣靜靜地躺著糯崎,像睡著了一般几缭。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上沃呢,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天年栓,我揣著相機(jī)與錄音,去河邊找鬼薄霜。 笑死某抓,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的惰瓜。 我是一名探鬼主播否副,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼崎坊!你這毒婦竟也來了备禀?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤奈揍,失蹤者是張志新(化名)和其女友劉穎曲尸,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體男翰,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡另患,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蛾绎。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片昆箕。...
    茶點(diǎn)故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖租冠,靈堂內(nèi)的尸體忽然破棺而出鹏倘,到底是詐尸還是另有隱情,我是刑警寧澤肺稀,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布第股,位于F島的核電站,受9級特大地震影響话原,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜诲锹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一繁仁、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧归园,春花似錦黄虱、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽晤揣。三九已至,卻和暖如春朱灿,著一層夾襖步出監(jiān)牢的瞬間昧识,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工盗扒, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留跪楞,地道東北人。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓侣灶,卻偏偏與公主長得像甸祭,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子褥影,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,627評論 2 350

推薦閱讀更多精彩內(nèi)容