Android 在动画结束回调onAnimationEnd()中remove view的崩溃解决方法及源码分析

Posted 薛瑄

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 在动画结束回调onAnimationEnd()中remove view的崩溃解决方法及源码分析相关的知识,希望对你有一定的参考价值。

问题:

问题描述起来很简单,就是在动画结束的时候,调用父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 应该也算是一个合理的需求,于是上网一搜,果然有人也遇到这个问题,第一种解决方法,就是这里的
Android - remove View when its animation finished

以上是关于Android 在动画结束回调onAnimationEnd()中remove view的崩溃解决方法及源码分析的主要内容,如果未能解决你的问题,请参考以下文章

CALayer的动画结束回调?

如何在动画结束时设置属性或任何回调? [复制]

ngAnimate,addClass 回调在动画结束后不运行

Angular 2动画结束回调函数的一个例子

jquery动画函数里面可以跟一个回调函数,表示动画结束后执行的代码

JS如何监听动画结束