Android 記一次解決問題的過程

之前我寫過一篇文章茬缩,介紹我在GitHub開源的滑動控件ConsecutiveScroller是如何實現(xiàn)布局吸頂功能的。有興趣的朋友可以去看一下:Android滑動布局ConsecutiveScrollerLayout實現(xiàn)布局吸頂功能吼旧。文章介紹了ConsecutiveScrollerLayout是如何通過計算布局的滑動距離凰锡,給吸頂view設置y軸偏移量,讓它懸停在頂部圈暗。不過當view懸停在頂部時寡夹,它會與后面的view重疊而被覆蓋。這是由于Android布局的顯示層級厂置,兩個view重疊時,后添加的會將先添加的覆蓋魂角。而我們希望的是當吸頂view與其他view重疊時昵济,吸頂view能顯示在最上層,覆蓋后面的view野揪。當時我的解決方法是給吸頂view設置translationZ访忿,讓它的顯示圖層高于其他的view,這樣它就不會被其他view覆蓋了斯稳。這樣做的確很好的解決了view重疊顯示的問題海铆,不過美中不足的是,translationZ是Android 5.0才支持的方法卧斟,5.0以下的手機無法使用這個方法設置view的顯示圖層高度憎茂。這使得ConsecutiveScrollerLayout的吸頂功能只能在Android 5.0以上的手機才能使用,這大大的限制了它的適用范圍竖幔。如果我們的項目是支持5.0以下的,那么我們不可能讓吸頂?shù)墓δ苤辉?.0以上的手機有效募逞,而不管5.0以下的手機。所以我需要找到一種方法放接,讓5.0以下的手機也能正常使用吸頂?shù)墓δ堋?/p>

分析問題

5.0以下不能使用吸頂,是因為setTranslationZ()方法是5.0方法是5.0以后有的洪燥,那么Android是否提供了向下兼容的方法呢乳乌?于是我找到了ViewCompat.setTranslationZ()方法。

    public static void setTranslationZ(@NonNull View view, float translationZ) {
        if (VERSION.SDK_INT >= 21) {
            view.setTranslationZ(translationZ);
        }
    }

真是讓人失望再来,它只是判斷了以下版本磷瘤,讓5.0以下不至于報錯,其實它什么都沒做针炉。既然連Android本身都沒有對5.0以下做處理扳抽,顯然讓view的Z軸向下兼容是不大可能的。

回歸問題本身镰烧,我們希望吸頂view顯示在界面的最上層楞陷,不被其他view所覆蓋。Android界面上顯示的所有內(nèi)容都是繪制在一張畫布(Canvas)上面的结执,同一個區(qū)域艾凯,如果被繪制多次,先繪制的內(nèi)容會被后繪制的內(nèi)容覆蓋览芳。而view的繪制順序是先添加的先繪制,后添加的后繪制铸敏,所以當view重疊時,后面的view會覆蓋前面的view闪水。只要保證吸頂?shù)膙iew在其他view之后繪制蒙具,吸頂view就會顯示在其他view之上,不會被其他view覆蓋持钉。那么有沒有方法能保證吸頂view最后繪制篱昔?最簡單直接的方法當然是讓吸頂view最后添加,但問題是view的添加順序不僅會影響繪制順序州刽,同樣也會影響view的排列和顯示位置。而我們想要的是改變view的繪制順序辨绊,不改變view的顯示位置匹表。所以這種方法顯然也是不行的。有什么方法可以在不改變view的添加順序的情況下,改變它的繪制順序呢框冀?我們知道布局在measure、layout和draw的過程中宣虾,都會遍歷它的子view温数,分發(fā)測量、布局撑刺、繪制的流程。如果我們在布局draw之前修改子view的順序甫菠,draw之后恢復,那么是否就保證了只改變view的繪制順序拂苹。

解決方案 1.0

ViewGroup的子view保存在mChildren數(shù)組中痰洒。

private View[] mChildren;

由于它是private的,要獲取和修改它脯宿,需要通過反射來執(zhí)行仓犬。

// 獲取mChildren
private View[] getChildren() {
    try {
        Class aClass = Class.forName("android.view.ViewGroup");
        Field field = aClass.getDeclaredField("mChildren");
        field.setAccessible(true); 
        Object resultValue = field.get(this);
        return (View[]) resultValue;
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}
// 設置mChildren
private void setChildren(View[] children) {
    try {
        Class aClass = Class.forName("android.view.ViewGroup");
        Field field = aClass.getDeclaredField("mChildren");
        field.setAccessible(true); // 私有屬性必須設置訪問權(quán)限
        field.set(this, children);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

繪制前搀继,修改view的排序,繪制后恢復叽躯。

// 臨時變量,保存mChildren原數(shù)組
private View[] tempViews = null;

@Override
public void draw(Canvas canvas) {
   // 兼容5.0以下吸頂功能
   if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP
           && !getStickyChildren().isEmpty()) {
       tempViews = getChildren();
       if (tempViews != null) {
         // 修改mChildren
           setChildren(sortViews(tempViews.length));
       }
    }

    super.draw(canvas);

   // 兼容5.0以下吸頂功能
   if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP
           && !getStickyChildren().isEmpty() && tempViews != null) {
     // 恢復mChildren
       setChildren(tempViews);
   }
}

// 返回排序后的children數(shù)組
private View[] sortViews(int size) {
    View[] views = new View[size];
    int index = 0;
    int count = getChildCount();
    for (int i = 0; i < count; i++) {
        View child = getChildAt(i);
        // 普通view
        if (!isStickyChild(child)) {
            views[index] = child;
            index++;
        }
    }

    for (int i = 0; i < count; i++) {
        View child = getChildAt(i);
        // 吸頂view
        if (isStickyChild(child)) {
            views[index] = child;
            index++;
        }
    }
    return views;
}

修改好点骑,運行測試一下,當view吸頂時憨募,能正常顯示在最上層袁辈,不會被下面的view覆蓋了,好像問題已經(jīng)完美解決了尾膊≤癖耍可是當我點擊界面上的控件時,新的問題出現(xiàn)了鸣皂,我點擊的view和響應的view不是同一個暮蹂,事件的傳遞亂了椎侠。因為我們把view的繪制順序改變了措拇,所以我們實際看到的、操作的view浅悉,跟系統(tǒng)判斷的可能不是同一個view了券犁。顯然這種解決方法引發(fā)了新的問題,是不可取的粘衬。

分析源碼

既然通過修改mChildren的方法行不通,只能另尋方案勘伺。我嘗試跟蹤view的繪制源碼褂删,期待能有一些新思路。ViewGroup繪制子view的源碼調(diào)用路徑是:draw()-->dispatchDraw()缅帘。ViewGroup中的dispatchDraw()方法是繪制子view的關鍵代碼难衰,通過閱讀源碼,我發(fā)現(xiàn)了幾句關鍵代碼失暂。

    @Override
    protected void dispatchDraw(Canvas canvas) {
        
                // step 1:獲取預定義的排序列表
        final ArrayList<View> preorderedList = usingRenderNodeProperties
                ? null : buildOrderedChildList();
                
                // step 2:判斷是否需要自定義排序
        final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled();
                
        for (int i = 0; i < childrenCount; i++) {
                        // step 3:根據(jù)繪制順序獲取view下標
            final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
                        // step 4:根據(jù)下標獲取子view
            final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                                // step 5:繪制子view
                more |= drawChild(canvas, child, drawingTime);
            }
        }
    }

第一步:獲取預定義的排序列表趣席。如果開啟了硬件加速usingRenderNodeProperties為true醇蝴,preorderedList為null想罕。否則執(zhí)行buildOrderedChildList()方法霉涨,這個方法大部分情況下也直接返回null惭适,所以preorderedList一般都是null的。buildOrderedChildList()方法只有在沒有設置硬件加速往枷,并且子view設置了Z軸高度的情況下才不會返回null凄杯。我們知道,Android 4.0后戒突,默認都是開啟硬件加速的,而5.0前导而,是不支持view的Z軸的隔崎,所以只有在5.0后關閉硬件加速,并且設置了子view的Z軸洼滚,buildOrderedChildList()方法才不會返回null技潘,這個方法就是處理這種情況的,而且它對view的排序處理跟我們下面分析的邏輯基本一樣享幽,所以這個方法我們可以忽略不看。
第二步:判斷是否需要自定義排序摆霉。既然preorderedList為null奔坟,那么是否需要自定義排序的判斷就是isChildrenDrawingOrderEnabled()方法,這個方法默認為false婉支,只有設置為true澜建,自定義的排序才生效蝌以,這是我們需要關注的第一個方法何之。
第三步:根據(jù)繪制順序獲取view下標。直接看代碼:

    private int getAndVerifyPreorderedIndex(int childrenCount, int i, boolean customOrder) {
        final int childIndex;
        if (customOrder) {
          // 如果自定義排序徊件,根據(jù)順序獲取view下標
            final int childIndex1 = getChildDrawingOrder(childrenCount, i);
            if (childIndex1 >= childrenCount) {
                throw new IndexOutOfBoundsException("getChildDrawingOrder() "
                        + "returned invalid index " + childIndex1
                        + " (child count is " + childrenCount + ")");
            }
            childIndex = childIndex1;
        } else {
          // 不是自定義排序庇忌,下標和順序一致
            childIndex = i;
        }
        return childIndex;
    }

在這個方法里舰褪,如果不排序,返回的下標和順序一樣占拍,所以默認繪制順序就是view的添加順序。如果需要排序表牢,通過getChildDrawingOrder獲取需要繪制的view的下標贝次,繪制順序由這個方法的返回值決定。

protected int getChildDrawingOrder(int childCount, int drawingPosition) {
    return drawingPosition;
}

可以看到敲茄,這個方法的返回值依然是順序本身山析,所以它的默認繪制順序也view的添加順序。但是這個方法是protected笋轨,也就是說我們可以覆寫這個方法,返回我們想要的index仅讽,改變view的繪制順序钾挟。這是我們需要關注的第二個方法。

第四步:根據(jù)下標等龙,調(diào)用getAndVerifyPreorderedView或者需要繪制的子view。

    private static View getAndVerifyPreorderedView(ArrayList<View> preorderedList, View[] children,
            int childIndex) {
        final View child;
        if (preorderedList != null) {
            child = preorderedList.get(childIndex);
            if (child == null) {
                throw new RuntimeException("Invalid preorderedList contained null child at index "
                        + childIndex);
            }
        } else {
            child = children[childIndex];
        }
        return child;
    }

這個方法很簡單罐栈,就是根據(jù)下標或者view荠诬,如果有預定義排序,就從preorderedList中獲取柑贞,否則就從children數(shù)組獲取聂抢,children數(shù)組就是保存子view的數(shù)組,按添加順序排列有决。

第五步:drawChild空盼,就是調(diào)用child的draw方法繪制子view。

最終實現(xiàn)

現(xiàn)在我們知道揽趾,想要改變ViewGroup的子view繪制順序,只有開啟自定義排序苟呐,并且覆寫getChildDrawingOrder方法就可以了俐筋。

在自定義ViewGroup的構(gòu)造方法中調(diào)用:

// 開啟自定義排序
setChildrenDrawingOrderEnabled(true);

預先處理view的排序

// 保存預先處理的排序
private final List<View> mViews = new ArrayList<>();

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
  
  //忽略其他的代碼 
  
    // 排序
    sortViews();
}

private void sortViews() {
    List<View> list = new ArrayList<>();
    int count = getChildCount();
    for (int i = 0; i < count; i++) {
        View child = getChildAt(i);
      // 添加非吸頂view
        if (!isStickyChild(child)) {
            list.add(child);
        }
    }

    for (int i = 0; i < count; i++) {
        View child = getChildAt(i);
      // 添加吸頂view
        if (isStickyChild(child)) {
            list.add(child);
        }
    }
    mViews.clear();
    mViews.addAll(list);
}

這里要說明一下,因為getChildDrawingOrder方法是根據(jù)繪制的順序drawingPosition返回需要繪制的子view下標两波,所以我們需要提前知道最終繪制的順序闷哆,才能根據(jù)drawingPosition找到相應的index,所以需要提前對view排序好劣坊。而把排序的時機選擇在onLayout屈留,是因為在我的需求里测蘑,子view的添加康二、移除、和setLayoutParams都有可能改變排序挨约,而這些操作恰好都會重新調(diào)用父布局的onLayout方法产雹。最后排序的方式是先添加非吸頂view,后添加吸頂view蔓挖,這樣保證了吸頂view在最后繪制,view重疊時也就不會被其他view覆蓋了隘弊。

最后覆寫getChildDrawingOrder

@Override
protected int getChildDrawingOrder(int childCount, int drawingPosition) {
    if (mViews.size() > drawingPosition) {
      // 根據(jù)drawingPosition找到子view荒适,返回子view在ViewGroup中的index
        return indexOfChild(mViews.get(drawingPosition));
    }
    return super.getChildDrawingOrder(childCount, drawingPosition);
}

至此,我們的功能就實現(xiàn)好了咽扇。

寫在最后

這篇文章的重點就一個getChildDrawingOrder方法陕壹,但是如果我只是想告訴大家有這么一個方法,那么完全沒有必要寫這篇文章嘶伟。我寫這篇文章的主要目的是記錄這個問題的解決過程,中間會踩坑九昧,也會有意外收獲毕匀。網(wǎng)上有朋友吐槽,面試時面試官會問:“你遇到過哪些難題皂岔,最后時怎么解決的”。很多人都不知道怎么回答剖毯,因為所有已經(jīng)被解決的問題都不是問題,而沒有被解決的問題你是不會提起的擂达。就拿我的這個問題來說,如果我早知道有這么個方法,這還是問題嗎舒憾?我們往往在解決問題后就忽略了問題的解決過程,甚至是問題本身丁溅,決定原來這個問題如此簡單探遵。卻不知,這個過程對我們才是最有意義和收獲的箱季。

最后說一句:從源碼中尋找答案,永遠是解決問題的最有效方法拷况。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末赚瘦,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子起意,更是在濱河造成了極大的恐慌病瞳,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件心褐,死亡現(xiàn)場離奇詭異笼踩,居然都是意外死亡,警方通過查閱死者的電腦和手機掘而,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來知染,“玉大人斑胜,你說我怎么就攤上這事≈古耍” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵涧狮,是天一觀的道長者冤。 經(jīng)常有香客問我,道長涉枫,這世上最難降的妖魔是什么腐螟? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮尼桶,結(jié)果婚禮上锯仪,老公的妹妹穿的比我還像新娘。我一直安慰自己庶喜,他們只是感情好,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布秩冈。 她就那樣靜靜地躺著斥扛,像睡著了一般入问。 火紅的嫁衣襯著肌膚如雪芬失。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天棱烂,我揣著相機與錄音,去河邊找鬼哩治。 笑死,一個胖子當著我的面吹牛衬鱼,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播馁启,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼惯疙,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了霉颠?” 一聲冷哼從身側(cè)響起荆虱,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎怀读,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體苍糠,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡岳瞭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年蚊锹,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片牡昆。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖钻心,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情摊沉,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布说墨,位于F島的核電站苍柏,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏试吁。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一烛恤、第九天 我趴在偏房一處隱蔽的房頂上張望余耽。 院中可真熱鬧,春花似錦碟贾、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蔬崩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間沥阳,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工脉让, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人溅潜。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像粗仓,于是被迫代替她去往敵國和親设捐。 傳聞我的和親對象是個殘疾皇子借浊,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354