關於ViewFlow和GridView嵌套導致Parameter must be a descendant of this view問題的解決方案
【關於ViewFlow】
ViewFlow是一款基於ViewGroup實現的可以水平滑動的開源UI Widget,可以從
https://code.google.com/p/andro-views/ 下載。
它使用Adapter進行條目綁定,主要用於不確定數目的視圖間的切換,和ViewPager功能類似,但是可擴展性更強。
本例就是使用ViewFlow來實現頁面水平切換。
【關於文章所用源碼】
本文所屬異常由於是從Android 4.2設備上拋出,所以文章內出現的所有源碼都是Android 4.2源碼,具體地址如下:
http://grepcode.com/snapshot/repository.grepcode.com/java/ext/com.google.android/android/4.2.1_r1.2/
一、功能描述
采用ViewFlow+GridView的方式實現手勢切屏功能,每屏以九宮格模式顯示。
長按GridView里的Item切換到編輯模式,可以對Item進行刪除。
二、復現場景
2.1 復現環境
本人拿了多款Android 4.2系列手機進行測試,目前只在兩部手機上必現,在其他非 4.2 手機上偶爾出現。
華為Ascend P6,Android 4.2.2
聯想K900,Android 4.2.1
2.2 復現步驟
進入應用后,以下三種操作都會導致所述問題:
1、Home到后台,再切換回來,Crash
2、長按Item,待切換到編輯模式后,Home到后台,再切換回來,Crash
3、左右切換幾次屏幕,Home到后台,再切換回來,Crash
三、Crash Stack Info

1 java.lang.IllegalArgumentException: parameter must be a descendant of this view 2 at android.view.ViewGroup.offsetRectBetweenParentAndChild(ViewGroup.java:4295) 3 at android.view.ViewGroup.offsetDescendantRectToMyCoords(ViewGroup.java:4232) 4 at android.view.ViewRootImpl.scrollToRectOrFocus(ViewRootImpl.java:2440) 5 at android.view.ViewRootImpl.draw(ViewRootImpl.java:2096) 6 at android.view.ViewRootImpl.performDraw(ViewRootImpl.java:2045) 7 at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1854) 8 at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:989) 9 at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:4351) 10 at android.view.Choreographer$CallbackRecord.run(Choreographer.java:749) 11 at android.view.Choreographer.doCallbacks(Choreographer.java:562) 12 at android.view.Choreographer.doFrame(Choreographer.java:532) 13 at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:735) 14 at android.os.Handler.handleCallback(Handler.java:725) 15 at android.os.Handler.dispatchMessage(Handler.java:92) 16 at android.os.Looper.loop(Looper.java:137) 17 at android.app.ActivityThread.main(ActivityThread.java:5041) 18 at java.lang.reflect.Method.invokeNative(Native Method) 19 at java.lang.reflect.Method.invoke(Method.java:511) 20 at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:793) 21 at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:560) 22 at dalvik.system.NativeStart.main(Native Method)
四、問題分析
4.1 異常描述
首先讓我們看一下這個Exception是如何拋出的。參考:
http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.2.1_r1.2/android/view/ViewGroup.java#ViewGroup
Android 4.2.1_r1.2中ViewGroup的offsetRectBetweenParentAndChild方法如下:

1 /** 2 * Helper method that offsets a rect either from parent to descendant or 3 * descendant to parent. 4 */ 5 void offsetRectBetweenParentAndChild(View descendant, Rect rect, 6 boolean offsetFromChildToParent, boolean clipToBounds) { 7 8 // already in the same coord system :) 9 if (descendant == this) { 10 return; 11 } 12 13 ViewParent theParent = descendant.mParent; 14 15 // search and offset up to the parent 16 while ((theParent != null) 17 && (theParent instanceof View) 18 && (theParent != this)) { 19 20 if (offsetFromChildToParent) { 21 rect.offset(descendant.mLeft - descendant.mScrollX, 22 descendant.mTop - descendant.mScrollY); 23 if (clipToBounds) { 24 View p = (View) theParent; 25 rect.intersect(0, 0, p.mRight - p.mLeft, p.mBottom - p.mTop); 26 } 27 } else { 28 if (clipToBounds) { 29 View p = (View) theParent; 30 rect.intersect(0, 0, p.mRight - p.mLeft, p.mBottom - p.mTop); 31 } 32 rect.offset(descendant.mScrollX - descendant.mLeft, 33 descendant.mScrollY - descendant.mTop); 34 } 35 36 descendant = (View) theParent; 37 theParent = descendant.mParent; 38 } 39 40 // now that we are up to this view, need to offset one more time 41 // to get into our coordinate space 42 if (theParent == this) { 43 if (offsetFromChildToParent) { 44 rect.offset(descendant.mLeft - descendant.mScrollX, 45 descendant.mTop - descendant.mScrollY); 46 } else { 47 rect.offset(descendant.mScrollX - descendant.mLeft, 48 descendant.mScrollY - descendant.mTop); 49 } 50 } else { 51 throw new IllegalArgumentException("parameter must be a descendant of this view"); 52 } 53 }
在方法最后可以看到該異常。那么該異常到底表示什么意思呢?若想知道答案,我們需要從該方法的實現入手。
通過注釋可知,offsetRectBetweenParentAndChild方法的功能有兩個:
1、計算一個Rect在某個Descendant View所在坐標系上所表示的區域或者是在該坐標系上和該Descendant View重疊的區域;
2、計算一個Rect從某個Descendant View所在坐標系折回到Parent View所在坐標系所表示的區域,即與功能1相反。
分析實現代碼可以看出,它是通過所給Descendant View逐級向上尋找Parent View,同時將Rect轉換到同級坐標系。在方法末尾處指出:如果最后尋找的Parent View和當前View(即調用offsetRectBetweenParentAndChild方法的View)不一致,則會拋出 IllegalArgumentException(
"parameter must be a descendant of this view")異常,亦即該文所指異常。
說白了,就是所給Descendant View必須是當前View的子孫.
那么,什么時候最后的Parent View和當前View不一致呢?請看下節分析。
4.2 原因探究
4.2.1 異常條件
我們來看offsetRectBetweenParentAndChild里的這段代碼:

1 ViewParent theParent = descendant.mParent; 2 3 // search and offset up to the parent 4 while ((theParent != null) 5 && (theParent instanceof View) 6 && (theParent != this)) {
當Descendant View的Parent為null、非View實例、當前View時,會跳出循環進入最后的判斷。排除當前View,就只剩下兩個原因:null和非View實例。
這就需要探究View的Parent是如何被賦值的。
4.2.2 View內Parent的賦值入口
首先,我們從最根本的View入手。
在View源碼里找到mParent的聲明和賦值代碼分別如下:
聲明:
View Code

1 /** 2 * The parent this view is attached to. 3 * {@hide} 4 * 5 * @see #getParent() 6 */ 7 protected ViewParent mParent;
賦值:

1 /* 2 * Caller is responsible for calling requestLayout if necessary. 3 * (This allows addViewInLayout to not request a new layout.) 4 */ 5 void assignParent(ViewParent parent) { 6 if (mParent == null) { 7 mParent = parent; 8 } else if (parent == null) { 9 mParent = null; 10 } else { 11 throw new RuntimeException("view " + this + " being added, but" 12 + " it already has a parent"); 13 } 14 }
透過上述代碼,我們可以猜測mParent的賦值方式有兩種:直接賦值和調用assignParent方法賦值。
4.2.3 ViewGroup為Descendant指定Parent
接下來查看ViewGroup的addView方法,並最終追蹤到addViewInner方法內,注意下圖紅框所示代碼:


紅框內的代碼驗證了我們的猜想,即:
一旦一個View被添加進ViewGroup內,其mParent所指向的就是該ViewGroup實例。很顯然,ViewGroup是View的實例。這樣異常條件就只剩下一種可能:Descendant View的Parent為null。
但是,什么情況下為null呢?
4.2.4 ViewGroup如何移除Descendant
查找並篩選ViewGroup內所有確定最后將Parent設置為null的方法,最后找到四個方法:
- removeFromArray(int index)------------------移除指定位置的Child
- removeFromArray(int start, int count)-------移除指定位置開始的count個Child
- removeAllViewsInLayout()---------------------移除所有Child
- detachAllViewsFromParent--------------------把所有Child從Parent中分離
從上述四個方法中不難看出,當View從ViewGroup中移除的時候,其Parent將被設為null。
由此可以斷定,ViewGroup使用了一個已經被移除的Descendant View來通過offsetRectBetweenParentAndChild方法計算坐標。
那么,既然使用被移除的Descendant View必定會導致該異常,ViewGroup又為何要使用它呢?
4.3 原因深究
4.3.1 ViewGroup為何使用被移除的Descendant
我們根據Crash Stack Info追溯到ViewRootImpl類的
boolean scrollToRectOrFocus(Rect rectangle, boolean immediate)方法,注意圖片中紅框所圈代碼:


由標記1、3處代碼可知,ViewGroup使用的Descendant View其實就是焦點當前真正所在的View,即Focused View。
問題就出在這里,如果Focused View是一個正常的View倒是可以,但是如果它是一個已經被移除的View,根據我們在4.2的分析可知,它的Parent為null,勢必會導致所述異常。
但是,Focused View是為什么會被移除呢?
4.3.2 Focused View為什么會被移除
4.2提到的四個方法中,第三個方法
removeAllViewsInLayout在移除Child Views的同時清除了Focused View的標記,排除。第四個方法
detachAllViewsFromParent在Activity Destory后才調用,排除。
方法一和方法二是重載方法,實現類似,可以斷定Focused View肯定是在這兩個方法中被移除的。
分析ViewFlow移除Child的操作,一共有兩處,分別在
recycleView(View v)和
resetFocus()方法內。
resetFocus方法內調用了
removeAllViewsInLayout方法,根據上一段分析可以安全排除。那么就剩下
recycleView(View v)方法,我們來看代碼:

1 protected void recycleView(View v) { 2 if (v == null) 3 return ; 4 5 mRecycledViews.add(v); 6 detachViewFromParent(v); 7 }
該方法是把ViewFlow的Child移除,並回收到循環利用列表。注意最后一行,調用了detachViewFromParent(View v)方法,代碼如下:

1 /** 2 * Detaches a view from its parent. Detaching a view should be temporary and followed 3 * either by a call to {@link #attachViewToParent(View, int, android.view.ViewGroup.LayoutParams)} 4 * or a call to {@link #removeDetachedView(View, boolean)}. When a view is detached, 5 * its parent is null and cannot be retrieved by a call to {@link #getChildAt(int)}. 6 * 7 * @param child the child to detach 8 * 9 * @see #detachViewFromParent(int) 10 * @see #detachViewsFromParent(int, int) 11 * @see #detachAllViewsFromParent() 12 * @see #attachViewToParent(View, int, android.view.ViewGroup.LayoutParams) 13 * @see #removeDetachedView(View, boolean) 14 */ 15 protected void detachViewFromParent(View child) { 16 removeFromArray(indexOfChild(child)); 17 }
很明顯,直接調用了removeFromArray(int index)方法,正是在4.2.4節中指出的第一個方法,而該方法已經在本節開頭被確定為真凶!
設想一下,如果recycleView(View v)的參數v正是Focused View的話,Focused View就會從ViewFlow中被移除,但是當前焦點仍然在其上邊。這時候offsetRectBetweenParentAndChild方法使用它必定會導致本文所指異常,這正是症結所在!
五、解決方案
5.1 普通方案與文藝方案
經過上述分析,不難想到解決方案:
在ViewFlow的recycleView(View v)方法內移除View的時候,判斷如果恰好是Focused View,則將焦點一並移除。
詳細代碼如下:

1 protected void recycleView(View v) { 2 if (v == null) 3 return; 4 5 // 方法一:普通方案,已驗證可行 6 // 如果被移除的View恰好是ViewFlow內當前焦點所在View 7 // 則清除焦點(clearChildFocus方法在清除焦點的同時 8 // 也把ViewGroup內保存的Focused View引用清除) 9 if (v == findFocus()) { 10 clearChildFocus(v); 11 } 12 13 // 方法二:文藝方案,請自行驗證! 14 // 下面這個方法也是把View的焦點清除,但是其是否起作用 15 // 這里不講,請讀者自行驗證、比較。 16 // v.clearFocus(); 17 18 mRecycledViews.add(v); 19 detachViewFromParent(v); 20 }
注意代碼內的注釋。
下面附上
ViewGroup.clearChildFocus(View v)和
View.clearFocus()這兩個方法的源碼以供參考:
ViewGroup.clearChildFocus(View v):

1 /** 2 * {@inheritDoc} 3 */ 4 public void clearChildFocus(View child) { 5 if (DBG) { 6 System.out.println(this + " clearChildFocus()"); 7 } 8 9 mFocused = null; 10 if (mParent != null) { 11 mParent.clearChildFocus(this); 12 } 13 }
View.clearFocus():

1 /** 2 * Called when this view wants to give up focus. This will cause 3 * {@link #onFocusChanged(boolean, int, android.graphics.Rect)} to be called. 4 */ 5 public void clearFocus() { 6 if (DBG) { 7 System.out.println(this + " clearFocus()"); 8 } 9 10 if ((mPrivateFlags & FOCUSED) != 0) { 11 mPrivateFlags &= ~FOCUSED; 12 13 if (mParent != null) { 14 mParent.clearChildFocus(this); 15 } 16 17 onFocusChanged(false, 0, null); 18 refreshDrawableState(); 19 } 20 }
當然,解決問題方法不止一種!
5.2 2B方案
注意,該方案僅適用於ViewGroup的Child不需要獲取焦點的情況,其他情況下請使用上一節介紹的方案。
既然是ViewGroup內的Focused View惹的禍,那干脆把這家伙斬草除根一了百了!
ViewGroup內的Child在獲取焦點的時候會調用
requestChildFocus(View child, View focused)方法,代碼如下:

1 /** 2 * {@inheritDoc} 3 */ 4 public void requestChildFocus(View child, View focused) { 5 if (DBG) { 6 System.out.println(this + " requestChildFocus()"); 7 } 8 if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) { 9 return; 10 } 11 12 // Unfocus us, if necessary 13 super.unFocus(); 14 15 // We had a previous notion of who had focus. Clear it. 16 if (mFocused != child) { 17 if (mFocused != null) { 18 mFocused.unFocus(); 19 } 20 21 mFocused = child; 22 } 23 if (mParent != null) { 24 mParent.requestChildFocus(this, focused); 25 } 26 }
注意第二個判斷條件:如果ViewGroup當前的焦點傳遞策略是不向下傳遞,則不指定Focused View。
So,下面該如何做,你懂的!整個世界清靜了~