Karan
Karan

Reputation: 12782

onAnimationEnd is not getting called, onAnimationStart works fine

I've a ScrollView in the PopupWindow. I'm animating ScrollView contents using TranslateAnimation.

When animation starts, the onAnimationStart listener is called but the onAnimationEnd is not getting called. Any ideas ?

Layout :

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   android:background="@drawable/popup_window_bg"
   android:layout_width="match_parent"
   android:layout_height="wrap_content">
  <View
     android:layout_width="@dimen/toolbar_padding_left"
     android:layout_height="@dimen/toolbar_height"/>
  <ScrollView
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+web/toolbar"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:scrollbars="none"
     android:visibility="invisible">
    <LinearLayout
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:orientation="vertical">

       ...

    </LinearLayout>
  </ScrollView>
</LinearLayout>

Animation code :

mToolbar = mPopupContents.findViewById( R.web.toolbar );
TranslateAnimation anim =
    new TranslateAnimation(0, 0, -60, 0);
anim.setDuration(1000);
anim.setAnimationListener(new Animation.AnimationListener() {
        public void onAnimationStart(Animation a) {
            Log.d(LOGTAG, "---- animation start listener called"  );
        }
        public void onAnimationRepeat(Animation a) {}
        public void onAnimationEnd(Animation a) {
            Log.d(LOGTAG, "---- animation end listener called"  );
        }
    });
mToolbar.startAnimation(anim);

Update : I verified that the onAnimationEnd is called but it is called after some delay (provided you don't start the new animation during that time).

Upvotes: 54

Views: 41409

Answers (16)

c00021a
c00021a

Reputation: 26

I also encountered a similar problem and eventually found the root cause.

My situation is different from yours. I hope it can provide some insight for you.

Here are my test codes(base on the Andoird 10):

    public class MainActivity extends AppCompatActivity {

        private Button button_anim_start;
        private AnimatorSet animatorSet;

        private ObjectAnimator anim;

        private final String TAG = "TEST";

        private int count = 0;

        private WindowManager windowManager;

        private View view = null;
        private WindowManager.LayoutParams params;

        Handler handler;

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            EdgeToEdge.enable(this);
            setContentView(R.layout.activity_main);
            ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
                Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
                v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
                return insets;
            });
            handler = new Handler(this.getMainLooper());
            windowManager = this.getWindowManager();

            params = new WindowManager.LayoutParams(1488, 118, 128, 510, 2036, WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
                    WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN |
                    WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, PixelFormat.TRANSLUCENT);
            params.gravity = Gravity.START | Gravity.TOP;
            animatorSet = new AnimatorSet();
            button_anim_start = findViewById(R.id.button_anim_start);
            button_anim_start.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    pop_down_anim();
                }
            });
        }

        private void pop_down_anim(){
            if (animatorSet.isRunning()){
            Log.i(TAG, "pop_down_anim: animatorSet isRunning="+animatorSet.isRunning());
            Log.i(TAG, "pop_down_anim: animatorSet getListeners="+animatorSet.getListeners());
            Log.i(TAG, "pop_down_anim: animatorSet getCurrentPlayTime="+animatorSet.getCurrentPlayTime());
            Log.i(TAG, "pop_down_anim: animatorSet getDuration="+animatorSet.getDuration());
            Log.i(TAG, "pop_down_anim: animatorSet getInterpolator="+animatorSet.getInterpolator());
            Log.i(TAG, "pop_down_anim: animatorSet getTotalDuration="+animatorSet.getTotalDuration());
            Log.i(TAG, "pop_down_anim: animatorSet getStartDelay="+animatorSet.getStartDelay());
            Log.i(TAG, "pop_down_anim: animatorSet isPaused="+animatorSet.isPaused());

            
            try {
                Field mPlayingSetField = AnimatorSet.class.getDeclaredField("mPlayingSet");
                mPlayingSetField.setAccessible(true);
                ArrayList<Object> mPlayingSet = (ArrayList<Object>) mPlayingSetField.get(animatorSet);
                Log.i(TAG, "pop_down_anim: animatorSet mPlayingSet.size()=" + mPlayingSet.size());

                Field mLastEventIdField = AnimatorSet.class.getDeclaredField("mLastEventId");
                mLastEventIdField.setAccessible(true);
                int LastEventId = (int) mLastEventIdField.get(animatorSet);
                Log.i(TAG, "pop_down_anim: animatorSet mLastEventId=" + LastEventId);


                Field mEventsField = AnimatorSet.class.getDeclaredField("mEvents");
                mEventsField.setAccessible(true);
                ArrayList<Object> mEvents = (ArrayList<Object>) mEventsField.get(animatorSet);
                Log.i(TAG, "pop_down_anim: animatorSet mEvents.size()=" + mEvents.size());



            } catch (Exception e) {
                e.printStackTrace();
            }
                return;
            }

            view = LayoutInflater.from(getBaseContext()).inflate(R.layout.test_anim, null);
            ((ImageView)view.findViewById(R.id.imageView)).setImageResource(R.mipmap.test);
            windowManager.addView(view, params);


            anim = ObjectAnimator.ofFloat((ImageView) view.findViewById(R.id.imageView), "translationY", -118, 0);
            animatorSet.setInterpolator(new LinearInterpolator());
            animatorSet.setDuration(120);
            animatorSet.playTogether(anim);
            if (animatorSet.getListeners() != null) {
                animatorSet.getListeners().clear();
            }
            animatorSet.addListener(new Animator.AnimatorListener() {
                @Override
                public void onAnimationStart(Animator animation) {
                    Log.i(TAG, "onAnimationStart count="+count);

                }
                @Override
                public void onAnimationEnd(Animator animation) {
                    Log.i(TAG, "onAnimationEnd count="+count);
                    count++;
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            if(view != null){
                                windowManager.removeView(view);
                            }
                            
                            view = null;
                            pop_down_anim();

                        }
                    });
                }


                @Override
                public void onAnimationCancel(Animator animation) {
                    Log.i(TAG, "onAnimationCancel count="+count);
                }

                @Override
                public void onAnimationRepeat(Animator animation) {
                    Log.i(TAG, "onAnimationRepeat count="+count);

                }
            });

            handler.post(new Runnable() {
                @Override
                public void run() {
                    animatorSet.start();

                }
            });

        }


    }

After I click the button for a while. The onAnimationEnd() also not called.

Analyze

Investigating the process is quite complex, let me briefly describe it.

When problem occured I click button agian and I got these logs:

    2024-12-20 04:04:23.167  1423-1423  TEST     com.example.animationcallbacktest    I  pop_down_anim: animatorSet isRunning=true
    2024-12-20 04:04:23.167  1423-1423  TEST     com.example.animationcallbacktest    I  pop_down_anim: animatorSet getListeners=[com.example.animationcallbacktest.MainActivity$2@8403427]
    2024-12-20 04:04:23.167  1423-1423  TEST     com.example.animationcallbacktest    I  pop_down_anim: animatorSet getCurrentPlayTime=3306
    2024-12-20 04:04:23.167  1423-1423  TEST     com.example.animationcallbacktest    I  pop_down_anim: animatorSet getDuration=120
    2024-12-20 04:04:23.167  1423-1423  TEST     com.example.animationcallbacktest    I  pop_down_anim: animatorSet getInterpolator=android.view.animation.LinearInterpolator@6446d4
    2024-12-20 04:04:23.167  1423-1423  TEST     com.example.animationcallbacktest    I  pop_down_anim: animatorSet getTotalDuration=120
    2024-12-20 04:04:23.167  1423-1423  TEST     com.example.animationcallbacktest    I  pop_down_anim: animatorSet getStartDelay=0
    2024-12-20 04:04:23.167  1423-1423  TEST     com.example.animationcallbacktest    I  pop_down_anim: animatorSet isPaused=false
    2024-12-20 04:04:23.167  1423-1423  TEST com.example.animationcallbacktest    I  pop_down_anim: animatorSet mPlayingSet.size()=65
    2024-12-20 04:04:23.167  1423-1423  TEST com.example.animationcallbacktest    I  pop_down_anim: animatorSet mLastEventId=203
    2024-12-20 04:04:23.167  1423-1423  TEST com.example.animationcallbacktest    I  pop_down_anim: animatorSet mEvents.size()=204

I read the source code of AnimatorSet、ObjectAnimator、ValueAnimator and reached the following infomation.

  1. In my case, the finish condition of AnimatorSet is "mPlayingSet is empty && current animation event is the last one".
  2. AnimatorSet will check all the node's status at every frame.
public boolean doAnimationFrame(long frameTime) {
        // Pump a frame to the on-going animators
        for (int i = 0; i < mPlayingSet.size(); i++) {
            Node node = mPlayingSet.get(i);
            if (!node.mEnded) {
                pulseFrame(node, getPlayTimeForNodeIncludingDelay(unscaledPlayTime, node));
            }
        }

        // Remove all the finished anims
        for (int i = mPlayingSet.size() - 1; i >= 0; i--) {
            if (mPlayingSet.get(i).mEnded) {
                mPlayingSet.remove(i);
            }
        }
        boolean finished = false;
        if (mReversing) {
            if (mPlayingSet.size() == 1 && mPlayingSet.get(0) == mRootNode) {
                // The only animation that is running is the delay animation.
                finished = true;
            } else if (mPlayingSet.isEmpty() && mLastEventId < 3) {
                // The only remaining animation is the delay animation
                finished = true;
            }
        } else {
            finished = mPlayingSet.isEmpty() && mLastEventId == mEvents.size() - 1;
        }

        if (finished) {
            endAnimation();
            return true;
        }
}

(According to 1. and 2. and above logs. Some nodes in mPlayingSet are still not finished. That cause the problem)

  1. The ObjectAnimator will check their target at every frame. If the target was gone the ObjectAnimator will cancel immediately. But this end status not returned to the AnimatorSet that's the root cause.
    void animateValue(float fraction) {
        final Object target = getTarget();
        if (mTarget != null && target == null) {
            // We lost the target reference, cancel and clean up. Note: we allow null target if the
            /// target has never been set.
            cancel();
            return;
        }

        super.animateValue(fraction);
        int numValues = mValues.length;
        for (int i = 0; i < numValues; ++i) {
            mValues[i].setAnimatedValue(target);
        }
    }

Conclusion

So to sum up. If a view instance is recycled during the animation, the animation will be forcibly ended, but the end state is not returned to the AnimatorSet, so the AnimatorSet thinks the animation has not ended and does not call onAnimationEnd.

Upvotes: 0

Sir Codesalot
Sir Codesalot

Reputation: 7283

Happened to me in a scenario that isn't described in other answers. Tried this and it didn't work:

ValueAnimator.ofInt(currentHeight, desiredHeight).apply {
     duration = 500
     addListener { 
         doOnEnd {
             Log.d(TAG, "animate: animation ended")
         }
     }
     start()
}

Changing to this resolved the issue. Unfortunately can't tell why.

ValueAnimator.ofInt(currentHeight, desiredHeight).apply {
     duration = 500
     doOnEnd {
           Log.d(TAG, "animate: animation ended")
     }
      start()
}

Upvotes: 1

ralphgabb
ralphgabb

Reputation: 10518

Make sure that you are USING view.startAnimation(Animation) AND NOT view.setAnimation(Animation). This simple confusion may be a problem.

Upvotes: 12

Invincible_cooler
Invincible_cooler

Reputation: 49

Do it like belows

  1. make animation final
  2. invoke it inside a handler
  3. listener should be instantiated once.
  4. clear animation.

for example view.clearAnimation();

new Hander().post(
   run() {
final TranslateAnimation ani = new TranslateAnimation(0, 0, 0, 0);
ani.setAnimationListener(mListener);
 }
);

private Animation.AnimationListener mListener = new Animation.AnimationListener() {
}

Upvotes: 0

johnny_crq
johnny_crq

Reputation: 4391

When you start an animation command in a view that is partially offscreen, the animation start and the onStartListener is called, but the animation doesn't run completely (somehow it is interrupted in the middle). My guess is that, as the view is offscreen, it is canceled and therefore it's onFinish is not called. As a workaround for that i created my own animation listener that start's an handler and uses postDelayed to notify the user of the animation end event. In Kotlin:

abstract class PartiallyOffScreenAnimationListener : Animation.AnimationListener, AnimationListener {
    override fun onAnimationRepeat(animation: Animation?) {
        onAnimationRepeat_(animation)
    }
    override fun onAnimationEnd(animation: Animation) {}

    override fun onAnimationStart(animation: Animation) {
        onAnimationStart_(animation)
        Handler().postDelayed({
            onAnimationEnd_(animation)
            animation.setAnimationListener(null)
        }, animation.duration + 50)
    }
}

Please notice that, in the case the animation doesn't run completely, the view may be left at an inconsistent state (for example, animating layout parameters may lead to a weird middle interpolator factor being dropped. For that reason, you should verify at the end callback that the view is in the wanted state. If not, just set it manually.

Upvotes: 0

yahya
yahya

Reputation: 4860

There might be someone still having this issue and not found a solution -even though reads a lot of stackoverflow answers- like me!

So my case was: I used animatorSet and

  1. there was not a single view that i could call clearAnimation on,
  2. I wasn't calling my animation from backgroundThread -which you should never do that, btw-

As a solution, i did call animatorSet.end() right before animatorSet.start()

Upvotes: 0

MrMaffen
MrMaffen

Reputation: 1647

For anyone stumbling upon this question: Consider switching over to using the Property Animation system instead http://developer.android.com/guide/topics/graphics/prop-animation.html

I've had several problems with the old way of animating a fade-in/out on a view (through AlphaAnimation). OnAnimationEnd was not being called etc ... With the Property Animation all those problems were resolved.

If you want to support API<11 devices, Jake Wharton's https://github.com/JakeWharton/NineOldAndroids is the way to go

Upvotes: 2

Entreco
Entreco

Reputation: 12900

Also, when using animations, don't forget the setFillAfter() to true.

http://developer.android.com/reference/android/view/animation/Animation.html#setFillAfter(boolean)

If fillAfter is true, the transformation that this animation performed will persist when it is finished. Defaults to false if not set. Note that this applies when using an AnimationSet to chain animations. The transformation is not applied before the AnimationSet itself starts.

anim.setFillAfter(true);
mToolbar.startAnimation(anim);

Upvotes: 3

Lisitso
Lisitso

Reputation: 495

After I don't remember how may posts read and days spent finding out a solution for this issue I discovered that if the object to move is not on the Screen region (for example is positioned out of the screen coords) OnAnimationEnd callback is not getting called. Probably the animation fails just after started (start method is called, I coded a listener) but nothing is wrote into logcat. Maybe this is not exactly your case but this finally solved my problem and hope it can help you too.

Upvotes: 10

ShortFuse
ShortFuse

Reputation: 6804

AnimationEnd is not reliable. If you don't want to rewrite your code with custom views that override OnAnimationEnd, use postDelayed.

Here's some example code:

final FadeUpAnimation anim = new FadeUpAnimation(v);
anim.setInterpolator(new AccelerateInterpolator());
anim.setDuration(1000);
anim.setFillAfter(true);
new Handler().postDelayed(new Runnable() {
    public void run() {
        v.clearAnimation();
        //Extra work goes here
    }
}, anim.getDuration());
v.startAnimation(anim);

While it MAY seem ugly, I can guarantee it's very reliable. I use it for ListViews that are inserting new rows while removing with animation to other rows. Stress testing a listener with AnimationEnd proved unreliable. Sometimes AnimationEnd was never triggered. You might want to reapply any transformation in the postDelayed function in case the animation didn't fully finish, but that really depends on what type of animation you're using.

Upvotes: 62

sravan
sravan

Reputation: 5333

I tried your code it's working fine at OnAnimation start and inAmimationEnd also , after duration time means after finish animation onAnimationEnd is called , so your code working fine

TranslateAnimation anim =new TranslateAnimation(0, 0, -60, 0);
        anim.setDuration(1000);     
        anim.setAnimationListener(new Animation.AnimationListener() {
                public void onAnimationStart(Animation a) {
                    Log.w("Start", "---- animation start listener called"  );
                }
                public void onAnimationRepeat(Animation a) {}
                public void onAnimationEnd(Animation a) {
                    Log.d(" end  ","---- animation end listener called"  );
                }
            });
            mIv.setAnimation(anim);
            mIv.startAnimation(anim);

Upvotes: -1

Ravi A
Ravi A

Reputation: 525

Try to use overridePendingAnimation(int,int). i.e. overridePendingAnimation(0,0)

It will override your default animation and then you can define you own animation By calling the method startAnimation using the object of View.

Here is my example code. Dont know whether it will be helpful to you or not.

overridePendingTransition(0,0);
//finish();
v.startAnimation(AnimationUtils.loadAnimation(getApplicationContext(), R.anim.fadeout));
startActivity(new Intent(ContentManagerActivity.this,Mainmenu_activity.class));
overridePendingTransition(R.anim.fadein,0);

Upvotes: 0

devanshu_kaushik
devanshu_kaushik

Reputation: 969

The delay is probably due to the anim.setDuration(1000) or if you are doing this on a thread, then probably due to context switching. Try manipulating the delay time and see if you notice any difference.

Upvotes: 0

success_anil
success_anil

Reputation: 3658

I guess Its gets called after sometime becasue you are using setDuration for animation and passing 1000 ms in it. Just pass 0 and it won't take a while to get called.

Upvotes: -1

Damian Kołakowski
Damian Kołakowski

Reputation: 2741

Where do you start your animation ? If in onCreate, it is wrong. Try to do it in onResume.

Upvotes: -2

neteinstein
neteinstein

Reputation: 17613

Are you not setting another animation before that one you are waiting ends?

Without context is hard to understand if it's something like that... but it is probably one of the only things that would cause it.

Upvotes: -1

Related Questions