• 【原创】【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,下面该如何做,你懂的!整个世界清静了~

    作者:Nodin
    出处:http://www.cnblogs.com/monodin
    个人博客网站:http://www.coderself.com
    关于作者:爱摄影,爱旅行,爱自由,爱生活,更要爱自己。活在当下,也在为未来打拼!
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利。

  • 相关阅读:
    【Docker】解析器指令之 escape
    【Docker】解析器指令之 syntax
    【Docker】Dockerfile 解析器指令
    【Docker】Dockerfile 格式
    【Docker】Dockerfile 介绍
    【Docker】Spring Boot 和 Docker
    【Docker】理解 Docker 中的 OverlayFS
    【Docker】使用 OverlayFS 存储驱动
    【Docker】选择存储驱动
    kuangbin专题 专题一 简单搜索 Fire! UVA
  • 原文地址:https://www.cnblogs.com/monodin/p/3675040.html
Copyright © 2020-2023  润新知