問題:
問題描述起來很簡單,就是在動畫結束的時候,調用父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