【原創】【ViewFlow+GridView】Parameter must be a descendant of this view問題分析


關於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)
View Code

 

四、問題分析

 

4.1 異常描述

 

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     }
View Code

 

在方法最后可以看到該異常。那么該異常到底表示什么意思呢?若想知道答案,我們需要從該方法的實現入手。

通過注釋可知,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)) {
View Code

 

當Descendant View的Parent為null、非View實例、當前View時,會跳出循環進入最后的判斷。排除當前View,就只剩下兩個原因:null和非View實例

 
這就需要探究View的Parent是如何被賦值的。
 

4.2.2 View內Parent的賦值入口

 

首先,我們從最根本的View入手。
在View源碼里找到mParent的聲明和賦值代碼分別如下:
聲明:
1     /**
2      * The parent this view is attached to.
3      * {@hide}
4      *
5      * @see #getParent()
6      */
7     protected ViewParent mParent;
View Code

賦值:

 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     }
View Code

透過上述代碼,我們可以猜測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     }
View Code

該方法是把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     }
View Code

很明顯,直接調用了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 }
View Code

注意代碼內的注釋。

 
下面附上 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 Code

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 }
View Code

當然,解決問題方法不止一種!

 

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 }
View Code

 

注意第二個判斷條件:如果ViewGroup當前的焦點傳遞策略是不向下傳遞,則不指定Focused View。
 
So,下面該如何做,你懂的!整個世界清靜了~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM