关于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

 java.lang.IllegalArgumentException: parameter must be a descendant of this view
at android.view.ViewGroup.offsetRectBetweenParentAndChild(ViewGroup.java:4295)
at android.view.ViewGroup.offsetDescendantRectToMyCoords(ViewGroup.java:4232)
at android.view.ViewRootImpl.scrollToRectOrFocus(ViewRootImpl.java:2440)
at android.view.ViewRootImpl.draw(ViewRootImpl.java:2096)
at android.view.ViewRootImpl.performDraw(ViewRootImpl.java:2045)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1854)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:989)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:4351)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:749)
at android.view.Choreographer.doCallbacks(Choreographer.java:562)
at android.view.Choreographer.doFrame(Choreographer.java:532)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:735)
at android.os.Handler.handleCallback(Handler.java:725)
at android.os.Handler.dispatchMessage(Handler.java:92)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:5041)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:793)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:560)
at dalvik.system.NativeStart.main(Native Method)

四、问题分析

4.1 异常描述

Android 4.2.1_r1.2中ViewGroup的offsetRectBetweenParentAndChild方法如下:
     /**
* Helper method that offsets a rect either from parent to descendant or
* descendant to parent.
*/
void offsetRectBetweenParentAndChild(View descendant, Rect rect,
boolean offsetFromChildToParent, boolean clipToBounds) { // already in the same coord system :)
if (descendant == this) {
return;
} ViewParent theParent = descendant.mParent; // search and offset up to the parent
while ((theParent != null)
&& (theParent instanceof View)
&& (theParent != this)) { if (offsetFromChildToParent) {
rect.offset(descendant.mLeft - descendant.mScrollX,
descendant.mTop - descendant.mScrollY);
if (clipToBounds) {
View p = (View) theParent;
rect.intersect(0, 0, p.mRight - p.mLeft, p.mBottom - p.mTop);
}
} else {
if (clipToBounds) {
View p = (View) theParent;
rect.intersect(0, 0, p.mRight - p.mLeft, p.mBottom - p.mTop);
}
rect.offset(descendant.mScrollX - descendant.mLeft,
descendant.mScrollY - descendant.mTop);
} descendant = (View) theParent;
theParent = descendant.mParent;
} // now that we are up to this view, need to offset one more time
// to get into our coordinate space
if (theParent == this) {
if (offsetFromChildToParent) {
rect.offset(descendant.mLeft - descendant.mScrollX,
descendant.mTop - descendant.mScrollY);
} else {
rect.offset(descendant.mScrollX - descendant.mLeft,
descendant.mScrollY - descendant.mTop);
}
} else {
throw new IllegalArgumentException("parameter must be a descendant of this view");
}
}

在方法最后可以看到该异常。那么该异常到底表示什么意思呢?若想知道答案,我们需要从该方法的实现入手。

通过注释可知,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里的这段代码:
 ViewParent theParent = descendant.mParent;

 // search and offset up to the parent
while ((theParent != null)
&& (theParent instanceof View)
&& (theParent != this)) {

当Descendant View的Parent为null、非View实例、当前View时,会跳出循环进入最后的判断。排除当前View,就只剩下两个原因:null和非View实例

 
这就需要探究View的Parent是如何被赋值的。
 

4.2.2 View内Parent的赋值入口

首先,我们从最根本的View入手。
在View源码里找到mParent的声明和赋值代码分别如下:
声明:

     /**
* The parent this view is attached to.
* {@hide}
*
* @see #getParent()
*/
protected ViewParent mParent;

赋值:

     /*
* Caller is responsible for calling requestLayout if necessary.
* (This allows addViewInLayout to not request a new layout.)
*/
void assignParent(ViewParent parent) {
if (mParent == null) {
mParent = parent;
} else if (parent == null) {
mParent = null;
} else {
throw new RuntimeException("view " + this + " being added, but"
+ " it already has a parent");
}
}

透过上述代码,我们可以猜测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)方法,我们来看代码:
      protected void recycleView(View v) {
if (v == null)
return ; mRecycledViews.add(v);
detachViewFromParent(v);
}

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

     /**
* Detaches a view from its parent. Detaching a view should be temporary and followed
* either by a call to {@link #attachViewToParent(View, int, android.view.ViewGroup.LayoutParams)}
* or a call to {@link #removeDetachedView(View, boolean)}. When a view is detached,
* its parent is null and cannot be retrieved by a call to {@link #getChildAt(int)}.
*
* @param child the child to detach
*
* @see #detachViewFromParent(int)
* @see #detachViewsFromParent(int, int)
* @see #detachAllViewsFromParent()
* @see #attachViewToParent(View, int, android.view.ViewGroup.LayoutParams)
* @see #removeDetachedView(View, boolean)
*/
protected void detachViewFromParent(View child) {
removeFromArray(indexOfChild(child));
}

很明显,直接调用了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,则将焦点一并移除。
详细代码如下:
 protected void recycleView(View v) {
if (v == null)
return; // 方法一:普通方案,已验证可行
// 如果被移除的View恰好是ViewFlow内当前焦点所在View
// 则清除焦点(clearChildFocus方法在清除焦点的同时
// 也把ViewGroup内保存的Focused View引用清除)
if (v == findFocus()) {
clearChildFocus(v);
} // 方法二:文艺方案,请自行验证!
// 下面这个方法也是把View的焦点清除,但是其是否起作用
// 这里不讲,请读者自行验证、比较。
// v.clearFocus(); mRecycledViews.add(v);
detachViewFromParent(v);
}

注意代码内的注释。

 
下面附上ViewGroup.clearChildFocus(View v)View.clearFocus()这两个方法的源码以供参考:
ViewGroup.clearChildFocus(View v):
 /**
* {@inheritDoc}
*/
public void clearChildFocus(View child) {
if (DBG) {
System.out.println(this + " clearChildFocus()");
} mFocused = null;
if (mParent != null) {
mParent.clearChildFocus(this);
}
}

View.clearFocus():

 /**
* Called when this view wants to give up focus. This will cause
* {@link #onFocusChanged(boolean, int, android.graphics.Rect)} to be called.
*/
public void clearFocus() {
if (DBG) {
System.out.println(this + " clearFocus()");
} if ((mPrivateFlags & FOCUSED) != 0) {
mPrivateFlags &= ~FOCUSED; if (mParent != null) {
mParent.clearChildFocus(this);
} onFocusChanged(false, 0, null);
refreshDrawableState();
}
}

当然,解决问题方法不止一种!

5.2 2B方案

注意,该方案仅适用于ViewGroup的Child不需要获取焦点的情况,其他情况下请使用上一节介绍的方案。
 
既然是ViewGroup内的Focused View惹的祸,那干脆把这家伙斩草除根一了百了!
 
ViewGroup内的Child在获取焦点的时候会调用requestChildFocus(View child, View focused)方法,代码如下:
 /**
* {@inheritDoc}
*/
public void requestChildFocus(View child, View focused) {
if (DBG) {
System.out.println(this + " requestChildFocus()");
}
if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
return;
} // Unfocus us, if necessary
super.unFocus(); // We had a previous notion of who had focus. Clear it.
if (mFocused != child) {
if (mFocused != null) {
mFocused.unFocus();
} mFocused = child;
}
if (mParent != null) {
mParent.requestChildFocus(this, focused);
}
}
注意第二个判断条件:如果ViewGroup当前的焦点传递策略是不向下传递,则不指定Focused View。
 
So,下面该如何做,你懂的!整个世界清静了~

【原创】【ViewFlow+GridView】Parameter must be a descendant of this view问题分析的更多相关文章

  1. GridView事件DataBinding,DataBound,RowCreated,RowDataBound区别及执行顺序分析

    严格的说,DataBinding,DataBound并不是GridView特有的事件,其他的控件诸如ListBox等也有DataBinding,DataBound事件. DataBinding事件MS ...

  2. 【原创】C++11:左值和右值(深度分析)

    ——原创,引用请附带博客地址 2019-12-06 23:42:18 这篇文章分析的还是不行,先暂时放在这以后再更新. 本篇比较长,需要耐心阅读 以一个实际问题开始分析 class Sub{} Sub ...

  3. 【原创】Linux中断子系统(一)-中断控制器及驱动分析

    背景 Read the fucking source code! --By 鲁迅 A picture is worth a thousand words. --By 高尔基 说明: Kernel版本: ...

  4. 收藏的技术文章链接(ubuntu,python,android等)

    我的收藏 他山之石,可以攻玉 转载请注明出处:https://ahangchen.gitbooks.io/windy-afternoon/content/ 开发过程中收藏在Chrome书签栏里的技术文 ...

  5. ListView中多个EditText设置焦点 多次点击异常报错

    08-17 18:23:09.825: ERROR/AndroidRuntime(1608): FATAL EXCEPTION: main 08-17 18:23:09.825: ERROR/Andr ...

  6. EditText的焦点问题

    问题说明: activity中有个三级菜单,三个ListView嵌套,最后一层ListView的item中有EditText控件.要求EditText不仅能手动输入,还能点击加减进行改变.EditTe ...

  7. Android ViewFlow的一个例子

    完成这个例子的步骤: 1.下载ViewFlow的源码,然后将类ViewFlow放在自己的工程的src的某个包下. 2.下载的源码里有2个工程view flow,viewflow-example.将vi ...

  8. Pytorch中Module,Parameter和Buffer的区别

    下文都将torch.nn简写成nn Module: 就是我们常用的torch.nn.Module类,你定义的所有网络结构都必须继承这个类. Buffer: buffer和parameter相对,就是指 ...

  9. Android GridView 通过seletor 设置状态和默认状态

    Android中可以通过selector控制GridView Item 的状态,而省去使用代码控制 GridView View Selector Xml文件 <?xml version=&quo ...

随机推荐

  1. C++ 学习笔记(一)

    只是记录自己学习C++ 笔记实例来自<c++ primer> 1.static: static 局部对象确保不迟于在程序执行流程第一次经过该对象的定义语句时进行初始化.这种对象一旦被创建, ...

  2. java获取常见文本文件的编码 解决乱码问题

    乱码问题的产生一般是,由字节流转字符流的时候,读文件的编码与文件的系统编码不一致造成的. 解决方式:先自动判断文件系统编码类型,然后读的时候用这个类型去读就ok了. 自动判断文件系统编码类型代码如下, ...

  3. [原创] JavaScript 图片放大镜插件 enlarge.js 以及移动版 enlarge.touch.js

    拖拖拉拉准本了一个月,终于把网站做好了.也终于可以分享这两个插件了.这两个插件,一个是 jQuery 版本,适合鼠标使用的,另一个是原生 JavaScript 制作,适合触摸屏使用(touch 事件) ...

  4. OGNL表达式介绍

    OGNL是Object-Graph Navigation Language的缩写,它是一种功能强大的表达式语言(Expression Language,简称为EL),通过它简单一致的表达式语法,可以存 ...

  5. Python中的计数(词频)

    1,对于list列表来说 a.用自定义函数来统计技术 def get_count(sequence): counts={} for x in sequence: if x in sequence: c ...

  6. 利用Shell命令获取IP地址

    一 :获取单个网卡的IPv4地址,方法如下: 方法一:$/sbin/ifconfig ethX | awk '/inet addr/ {print $2}' | cut -f2 -d ":& ...

  7. Mysql数据库表排序规则不一致导致联表查询,索引不起作用问题

    Mysql数据库表排序规则不一致导致联表查询,索引不起作用问题 表更描述: 将mysql数据库中的worktask表添加ishaspic字段. 具体操作:(1)数据库worktask表新添是否有图片字 ...

  8. Linux sort --copy

    Source: http://www.cnblogs.com/51linux/archive/2012/05/23/2515299.html sort是在Linux里非常常用的一个命令,管排序的,集中 ...

  9. objc swift 混编

    原链接:http://blog.csdn.net/xuanwenchao/article/details/30226823 在xocde6出来我们大部分代码都是用objective-c写的(部分C/C ...

  10. 引擎设计跟踪(九.14.2g) 将GNUMake集成到Visual Studio

    最近在做纹理压缩工具, 以及数据包的生成. shader编译已经在vs工程里面了, 使用custom build tool, build命令是调用BladeShaderComplier, 并且每个文件 ...