问题:
问题描述起来很简单,就是在动画结束的时候,调用父view删除子view,出现崩溃,信息如下:
java.lang.NullPointerException
Attempt to read from field 'int android.view.View.mViewFlags' on a null object reference
android.view.ViewGroup.dispatchDraw(ViewGroup.java:4111)
android.view.View.updateDisplayListIfDirty(View.java:19073)
android.view.View.draw(View.java:19935)
android.view.ViewGroup.drawChild(ViewGroup.java:4333)
android.view.ViewGroup.dispatchDraw(ViewGroup.java:4112)
下面是问题的核心代码
//设置动画回调
animation.setAnimationListener(new Animation.AnimationListener(){
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
//container 是fromView 的父view,是一个viewGroup
container.removeViewInLayout(fromView);
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
//执行动画
fromView.startAnimation(animation);
源码分析:
不想看源码的同学 ,也可以直接去下面看解决方法。
问题出在哪里,就在哪里断点,看看到底是什么问题。
下面先把断点调试的代码,截图出来,方便看到具体的值。两图的代码都是dispatchDraw函数里的,
下面的代码,是上面代码的文字版本
@Override
protected void dispatchDraw(Canvas canvas) {
...省略若干代码.....
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
...省略若干代码.....
// Only use the preordered list if not HW accelerated, since the HW pipeline will do the
// draw reordering internally
final ArrayList<View> preorderedList = usingRenderNodeProperties
? null : buildOrderedChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
for (int i = 0; i < childrenCount; i++) {
while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
final View transientChild = mTransientViews.get(transientIndex);
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
more |= drawChild(canvas, transientChild, drawingTime);
}
transientIndex++;
if (transientIndex >= transientCount) {
transientIndex = -1;
}
}
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
//这里发生了空指针异常,child为null
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
...省略若干代码.....
}
我们看到,直接原因是child 为null,导致获取child.mViewFlags 出现NullPointerException
代码再往上找,函数getAndVerifyPreorderedView 来获取child,具体的参数情况,是 children 里的个数是2,但是childIndex是2,得到的结果肯定null。
childIndex 是通过函数getAndVerifyPreorderedIndex(childrenCount, i, customOrder)来获取的,根据当前的参数情况,childIndex 取得是i 的值,i的值是在循环中根据childrenCount来递增的。
继续跟进childrenCount,在dispatchDraw()函数的前面进行赋值
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
mChildrenCount 就是mChildren的数量,凡是在mChildren中添加或者删除view,mChildrenCount 都会相应变化。
通过上面的分析,大概知道原因就是开始时mChildrenCount的值是3,赋给了childrenCount ,mChildren里面也是3个view。继续往下执行的时候,出现了mChildren 里面的view被删除了一个,mChildrenCount的值也变成了2。于是就出现了上面的崩溃。
那为什么view 会少了一个呢?
我们接着看代码
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
在dispatchDraw中会执行到这个代码,绘制子view,
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
注意这里调用draw是三个参数的,和平时看的measure,layout,draw的draw函数不是同一个
View.java 中的函数
/**
* This method is called by ViewGroup.drawChild() to have each child view draw itself.
*
* This is where the View specializes rendering behavior based on layer type,
* and hardware acceleration.
*/
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime){
...省略若干代码.....
//获取是否有动画
final Animation a = getAnimation();
if (a != null) {
//若果有动画,需要应用(处理)遗留的动画
more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
concatMatrix = a.willChangeTransformationMatrix();
if (concatMatrix) {
mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
}
transformToApply = parent.getChildTransformation();
}
...省略若干代码.....
}
Animation.java 中的函数
/**
* Gets the transformation to apply at a specified point in time. Implementations of this
* method should always replace the specified Transformation or document they are doing
* otherwise.
*
* @param currentTime Where we are in the animation. This is wall clock time.
* @param outTransformation A transformation object that is provided by the
* caller and will be filled in by the animation.
* @param scale Scaling factor to apply to any inputs to the transform operation, such
* pivot points being rotated or scaled around.
* @return True if the animation is still running
*/
public boolean getTransformation(long currentTime, Transformation outTransformation,
float scale) {
mScaleFactor = scale;
return getTransformation(currentTime, outTransformation);
}
接着调用getTransformation,这里调用到AnimationSet.java 里的函数
/**
* The transformation of an animation set is the concatenation of all of its
* component animations.
*
* @see android.view.animation.Animation#getTransformation
*/
@Override
public boolean getTransformation(long currentTime, Transformation t) {
...省略若干代码.....
boolean ended = true;
if (ended != mEnded) {
if (mListener != null) {
// 这里调用了动画结束的回调
mListener.onAnimationEnd(this);
}
mEnded = ended;
}
...省略若干代码.....
return more;
}
到这里原因就彻底搞清楚了
解决办法:
知道了原因,再来解决就很简单了。以最开始的核心问题代码,来演示如何解决。
问题出现remove view的时候,在dispatchDraw 中改变了viewGroup已有的子view的数量,导致只有N个view,最大索引是N-1,想要获取第N个view,出现了异常。
那么我们可以考虑不在本次执行中,remove view。在下一次的loop消息中执行remove 操作,那么就通过post 或 handler 发送消息来操作view
提供两种解决方法:
第一种:
//设置动画回调
animation.setAnimationListener(new Animation.AnimationListener(){
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
//get the parentView...
container.post(new Runnable() {
public void run () {
// it works without the runOnUiThread, but all UI updates must
// be done on the UI thread
activity.runOnUiThread(new Runnable() {
public void run() {
//container 是fromView 的父view,是一个viewGroup
container.removeViewInLayout(fromView);
}
});
}
}
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
//执行动画
fromView.startAnimation(animation);
第二种:
//执行动画
fromView.startAnimation(animation);
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
try {
container.removeViewInLayout(fromView);
} catch (Exception ignored) {
}
}
},animation.getDuration());
请点赞、收藏,感谢大家的支持,有任何疑问可在评论区回复
结语:
后来分析完源码,在动画结束后,删除view 应该也算是一个合理的需求,于是上网一搜,果然有人也遇到这个问题,第一种解决方法