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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章