以前我寫過一篇文章,介紹我在GitHub開源的滑動控件ConsecutiveScroller是如何實現佈局吸頂功能的。有興趣的朋友能夠去看一下:Android滑動佈局ConsecutiveScrollerLayout實現佈局吸頂功能。文章介紹了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如下的,那麼咱們不可能讓吸頂的功能只在5.0以上的手機有效,而無論5.0如下的手機。因此我須要找到一種方法,讓5.0如下的手機也能正常使用吸頂的功能。java
5.0如下不能使用吸頂,是由於setTranslationZ()方法是5.0方法是5.0之後有的,那麼Android是否提供了向下兼容的方法呢?因而我找到了ViewCompat.setTranslationZ()方法。android
public static void setTranslationZ(@NonNull View view, float translationZ) {
if (VERSION.SDK_INT >= 21) {
view.setTranslationZ(translationZ);
}
}
複製代碼
真是讓人失望,它只是判斷了如下版本,讓5.0如下不至於報錯,其實它什麼都沒作。既然連Android自己都沒有對5.0如下作處理,顯然讓view的Z軸向下兼容是不大可能的。git
迴歸問題自己,咱們但願吸頂view顯示在界面的最上層,不被其餘view所覆蓋。Android界面上顯示的全部內容都是繪製在一張畫布(Canvas)上面的,同一個區域,若是被繪製屢次,先繪製的內容會被後繪製的內容覆蓋。而view的繪製順序是先添加的先繪製,後添加的後繪製,因此當view重疊時,後面的view會覆蓋前面的view。只要保證吸頂的view在其餘view以後繪製,吸頂view就會顯示在其餘view之上,不會被其餘view覆蓋。那麼有沒有方法能保證吸頂view最後繪製?最簡單直接的方法固然是讓吸頂view最後添加,但問題是view的添加順序不只會影響繪製順序,一樣也會影響view的排列和顯示位置。而咱們想要的是改變view的繪製順序,不改變view的顯示位置。因此這種方法顯然也是不行的。有什麼方法能夠在不改變view的添加順序的狀況下,改變它的繪製順序呢?咱們知道佈局在measure、layout和draw的過程當中,都會遍歷它的子view,分發測量、佈局、繪製的流程。若是咱們在佈局draw以前修改子view的順序,draw以後恢復,那麼是否就保證了只改變view的繪製順序。github
ViewGroup的子view保存在mChildren數組中。面試
private View[] mChildren;
複製代碼
因爲它是private的,要獲取和修改它,須要經過反射來執行。canvas
// 獲取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); // 私有屬性必須設置訪問權限
field.set(this, children);
} catch (Exception e) {
e.printStackTrace();
}
}
複製代碼
繪製前,修改view的排序,繪製後恢復。數組
// 臨時變量,保存mChildren原數組
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數組
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覆蓋了,好像問題已經完美解決了。但是當我點擊界面上的控件時,新的問題出現了,我點擊的view和響應的view不是同一個,事件的傳遞亂了。由於咱們把view的繪製順序改變了,因此咱們實際看到的、操做的view,跟系統判斷的可能不是同一個view了。顯然這種解決方法引起了新的問題,是不可取的。ide
既然經過修改mChildren的方法行不通,只能另尋方案。我嘗試跟蹤view的繪製源碼,期待能有一些新思路。ViewGroup繪製子view的源碼調用路徑是:draw()-->dispatchDraw()。ViewGroup中的dispatchDraw()方法是繪製子view的關鍵代碼,經過閱讀源碼,我發現了幾句關鍵代碼。佈局
@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:根據繪製順序獲取view下標
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
// step 4:根據下標獲取子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。不然執行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,自定義的排序才生效,這是咱們須要關注的第一個方法。 第三步:根據繪製順序獲取view下標。直接看代碼:post
private int getAndVerifyPreorderedIndex(int childrenCount, int i, boolean customOrder) {
final int childIndex;
if (customOrder) {
// 若是自定義排序,根據順序獲取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的繪製順序。這是咱們須要關注的第二個方法。
第四步:根據下標,調用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;
}
複製代碼
這個方法很簡單,就是根據下標或者view,若是有預約義排序,就從preorderedList中獲取,不然就從children數組獲取,children數組就是保存子view的數組,按添加順序排列。
第五步:drawChild,就是調用child的draw方法繪製子view。
如今咱們知道,想要改變ViewGroup的子view繪製順序,只有開啓自定義排序,而且覆寫getChildDrawingOrder方法就能夠了。
在自定義ViewGroup的構造方法中調用:
// 開啓自定義排序
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方法是根據繪製的順序drawingPosition返回須要繪製的子view下標,因此咱們須要提早知道最終繪製的順序,才能根據drawingPosition找到相應的index,因此須要提早對view排序好。而把排序的時機選擇在onLayout,是由於在個人需求裏,子view的添加、移除、和setLayoutParams都有可能改變排序,而這些操做剛好都會從新調用父佈局的onLayout方法。最後排序的方式是先添加非吸頂view,後添加吸頂view,這樣保證了吸頂view在最後繪製,view重疊時也就不會被其餘view覆蓋了。
最後覆寫getChildDrawingOrder
@Override
protected int getChildDrawingOrder(int childCount, int drawingPosition) {
if (mViews.size() > drawingPosition) {
// 根據drawingPosition找到子view,返回子view在ViewGroup中的index
return indexOfChild(mViews.get(drawingPosition));
}
return super.getChildDrawingOrder(childCount, drawingPosition);
}
複製代碼
至此,咱們的功能就實現好了。
這篇文章的重點就一個getChildDrawingOrder方法,可是若是我只是想告訴你們有這麼一個方法,那麼徹底沒有必要寫這篇文章。我寫這篇文章的主要目的是記錄這個問題的解決過程,中間會踩坑,也會有意外收穫。網上有朋友吐槽,面試時面試官會問:「你遇到過哪些難題,最後時怎麼解決的」。不少人都不知道怎麼回答,由於全部已經被解決的問題都不是問題,而沒有被解決的問題你是不會提起的。就拿個人這個問題來講,若是我早知道有這麼個方法,這仍是問題嗎?咱們每每在解決問題後就忽略了問題的解決過程,甚至是問題自己,決定原來這個問題如此簡單。殊不知,這個過程對咱們纔是最有意義和收穫的。
最後說一句:從源碼中尋找答案,永遠是解決問題的最有效方法。