您的位置:首页 > 移动开发 > Android开发

【转载】Parameter must be a descendant of this view问题的解决方案

2015-09-27 19:18 537 查看
转载,原文链接为:

http://www.cnblogs.com/monodin/p/3675040.html

关于ViewFlow和GridView嵌套导致Parameter
must be a descendant ofthis 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 ofthis view
 2     atandroid.view.ViewGroup.offsetRectBetweenParentAndChild(ViewGroup.java:4295)
 3     atandroid.view.ViewGroup.offsetDescendantRectToMyCoords(ViewGroup.java:4232)
 4     atandroid.view.ViewRootImpl.scrollToRectOrFocus(ViewRootImpl.java:2440)
 5     atandroid.view.ViewRootImpl.draw(ViewRootImpl.java:2096)
 6     at android.view.ViewRootImpl.performDraw(ViewRootImpl.java:2045)
 7     atandroid.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1854)
 8     atandroid.view.ViewRootImpl.doTraversal(ViewRootImpl.java:989)
 9     atandroid.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:4351)
10     atandroid.view.Choreographer$CallbackRecord.run(Choreographer.java:749)
11     atandroid.view.Choreographer.doCallbacks(Choreographer.java:562)
12     atandroid.view.Choreographer.doFrame(Choreographer.java:532)
13     atandroid.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:735)
14     atandroid.os.Handler.handleCallback(Handler.java:725)
15     atandroid.os.Handler.dispatchMessage(Handler.java:92)
16     at android.os.Looper.loop(Looper.java:137)
17     atandroid.app.ActivityThread.main(ActivityThread.java:5041)
18     atjava.lang.reflect.Method.invokeNative(Native Method)
19     atjava.lang.reflect.Method.invoke(Method.java:511)
20     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:793)
21     atcom.android.internal.os.ZygoteInit.main(ZygoteInit.java:560)
22     at dalvik.system.NativeStart.main(NativeMethod)

View Code
四、问题分析
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的<
186ec
/span>offsetRectBetweenParentAndChild方法如下:

 1    /**
 2     * Helper method that offsets arect 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 thesame coord system :)
 9        if (descendant ==
this) {
10            return;
11         }
12 
13         ViewParent theParent =descendant.mParent;
14 
15        // search andoffset up to the parent
16        while ((theParent !=
null)
17                 && (theParentinstanceof 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 areup to this view, need to offset one more time
41        // to get into ourcoordinate 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 thisview");
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 ViewParenttheParent = descendant.mParent;

3// search and offset up to the parent
4while ((theParent !=
null)
5         && (theParentinstanceof 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 isattached to.
3     * {@hide}
4     *
5     *
@see #getParent()
6     */
7    protected ViewParent mParent;

View Code
赋值:

 1    /*
 2     * Caller is responsible forcalling requestLayout if necessary.
 3     * (This allows addViewInLayoutto 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 aparent");
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类的booleanscrollToRectOrFocus(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 ;

5         mRecycledViews.add(v);
6         detachViewFromParent(v);
7     }

View Code
该方法是把ViewFlow的Child移除,并回收到循环利用列表。注意最后一行,调用了detachViewFromParent(View
v)
方法,代码如下:

 1    /**
 2     * Detaches a view from itsparent. 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 cannotbe 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,则将焦点一并移除。

详细代码如下:

 1protected
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*/
 4public
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*/
 5public
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(Viewchild, View focused)方法,代码如下:

 1/**
 2* {@inheritDoc}
 3*/
 4public
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, ifnecessary
13    super.unFocus();
14 
15    // We had aprevious 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,下面该如何做,你懂的!整个世界清静了~
 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Android