之前我寫過一篇文章茬缩,介紹我在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)被解決的問題都不是問題,而沒有被解決的問題你是不會提起的擂达。就拿我的這個問題來說,如果我早知道有這么個方法,這還是問題嗎舒憾?我們往往在解決問題后就忽略了問題的解決過程,甚至是問題本身丁溅,決定原來這個問題如此簡單探遵。卻不知,這個過程對我們才是最有意義和收獲的箱季。
最后說一句:從源碼中尋找答案,永遠是解決問題的最有效方法拷况。