Detect animation finish in Android's RecyclerView
Solution 1
Currently the only working way I've found to solve this problem is to extend ItemAnimator
and pass it to RecyclerView
like this:
recyclerView.setItemAnimator(new DefaultItemAnimator() {
@Override
public void onAnimationFinished(RecyclerView.ViewHolder viewHolder) {
updateEmptyView();
}
});
But this technique is not universal, because I have to extend from concrete ItemAnimator
implementation being used by RecyclerView
. In case of private inner CoolItemAnimator
inside CoolRecyclerView
, my method will not work at all.
PS: My colleague suggested to wrap ItemAnimator
inside the decorator in a following manner:
recyclerView.setItemAnimator(new ListenableItemAnimator(recyclerView.getItemAnimator()));
It would be nice, despite seems like overkill for a such trivial task, but creating the decorator in this case is not possible anyway, because ItemAnimator
has a method setListener()
which is package protected so I obviously can't wrap it, as well as several final methods.
Solution 2
I have a little bit more generic case where I want to detect when the recycler view have finished animating completely when one or many items are removed or added at the same time.
I've tried Roman Petrenko's answer, but it does not work in this case. The problem is that onAnimationFinished
is called for each entry in the recycler view. Most entries have not changed so onAnimationFinished
is called more or less instantaneous. But for additions and removals the animation takes a little while so there it's called later.
This leads to at least two problems. Assume you have a method called doStuff()
that you want to run when the animation is done.
If you simply call
doStuff()
inonAnimationFinished
you will call it once for every item in the recycler view which might not be what you want to do.If you just call
doStuff()
the first timeonAnimationFinished
is called you may be calling this long before the last animation has been completed.
If you could know how many items there are to be animated you could make sure you call doStuff()
when the last animation finishes. But I have not found any way of knowing how many remaining animations there are queued up.
My solution to this problem is to let the recycler view first start animating by using new Handler().post()
, then set up a listener with isRunning()
that is called when the animation is ready. After that it repeats the process until all views have been animated.
void changeAdapterData() {
// ...
// Changes are made to the data held by the adapter
recyclerView.getAdapter().notifyDataSetChanged();
// The recycler view have not started animating yet, so post a message to the
// message queue that will be run after the recycler view have started animating.
new Handler().post(waitForAnimationsToFinishRunnable);
}
private Runnable waitForAnimationsToFinishRunnable = new Runnable() {
@Override
public void run() {
waitForAnimationsToFinish();
}
};
// When the data in the recycler view is changed all views are animated. If the
// recycler view is animating, this method sets up a listener that is called when the
// current animation finishes. The listener will call this method again once the
// animation is done.
private void waitForAnimationsToFinish() {
if (recyclerView.isAnimating()) {
// The recycler view is still animating, try again when the animation has finished.
recyclerView.getItemAnimator().isRunning(animationFinishedListener);
return;
}
// The recycler view have animated all it's views
onRecyclerViewAnimationsFinished();
}
// Listener that is called whenever the recycler view have finished animating one view.
private RecyclerView.ItemAnimator.ItemAnimatorFinishedListener animationFinishedListener =
new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
@Override
public void onAnimationsFinished() {
// The current animation have finished and there is currently no animation running,
// but there might still be more items that will be animated after this method returns.
// Post a message to the message queue for checking if there are any more
// animations running.
new Handler().post(waitForAnimationsToFinishRunnable);
}
};
// The recycler view is done animating, it's now time to doStuff().
private void onRecyclerViewAnimationsFinished() {
doStuff();
}
Solution 3
What worked for me is the following:
- detect that a view holder was removed
- in this case, register a listener to be notified when
dispatchAnimationsFinished()
is called - when all animations are finished, call a listener to perform the task (
updateEmptyView()
)
public class CompareItemAnimator extends DefaultItemAnimator implements RecyclerView.ItemAnimator.ItemAnimatorFinishedListener {
private OnItemAnimatorListener mOnItemAnimatorListener;
public interface OnItemAnimatorListener {
void onAnimationsFinishedOnItemRemoved();
}
@Override
public void onAnimationsFinished() {
if (mOnItemAnimatorListener != null) {
mOnItemAnimatorListener.onAnimationsFinishedOnItemRemoved();
}
}
public void setOnItemAnimatorListener(OnItemAnimatorListener onItemAnimatorListener) {
mOnItemAnimatorListener = onItemAnimatorListener;
}
@Override
public void onRemoveFinished(RecyclerView.ViewHolder viewHolder) {
isRunning(this);
}}
Solution 4
Here's a little Kotlin extension method that builds on the answer by nibarius.
fun RecyclerView.executeAfterAllAnimationsAreFinished(
callback: (RecyclerView) -> Unit
) = post(
object : Runnable {
override fun run() {
if (isAnimating) {
// itemAnimator is guaranteed to be non-null after isAnimating() returned true
itemAnimator!!.isRunning {
post(this)
}
} else {
callback(this@executeAfterAllAnimationsAreFinished)
}
}
}
)
Solution 5
Check from latest androidx.recyclerview:recyclerview:1.2.0
inside ItemAnimator
method:
boolean isRunning(@Nullable ItemAnimatorFinishedListener listener)
Example (Kotlin):
recyclerView.itemAnimator?.isRunning {
// do whatever you need to
}
Roman Petrenko
Updated on July 07, 2022Comments
-
Roman Petrenko almost 2 years
The
RecyclerView
, unlike toListView
, doesn't have a simple way to set an empty view to it, so one has to manage it manually, making empty view visible in case of adapter's item count is 0.Implementing this, at first I tried to call empty view logic right after modifying underlaying structure (
ArrayList
in my case), for example:btnRemoveFirst.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { devices.remove(0); // remove item from ArrayList adapter.notifyItemRemoved(0); // notify RecyclerView's adapter updateEmptyView(); } });
It does the thing, but has a drawback: when the last element is being removed, empty view appears before animation of removing is finished, immediately after removal. So I decided to wait until end of animation and then update UI.
To my surprise, I couldn't find a good way to listen for animation events in RecyclerView. First thing coming to mind is to use
isRunning
method like this:btnRemoveFirst.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { devices.remove(0); // remove item from ArrayList adapter.notifyItemRemoved(0); // notify RecyclerView's adapter recyclerView.getItemAnimator().isRunning(new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() { @Override public void onAnimationsFinished() { updateEmptyView(); } }); } });
Unfortunately, callback in this case runs immediately, because at that moment inner
ItemAnimator
still isn't in the "running" state. So, the questions are: how to properly use ItemAnimator.isRunning() method and is there a better way to achieve the desired result, i.e. show empty view after removal animation of the single element is finished? -
Ryan about 8 yearsHey Roman. Did you ever come across a more centralized solution to this problem?
-
Roman Petrenko about 8 years@Ryan: Unfortunately, no. But I had no research of this problem since the time of my answer.
-
Ciro Mine over 6 yearsIt works perfectly, plus I tried when the method
notifyItemRemoved(position)
and it works too regards. -
Lumii almost 6 yearsPretty elegant solution. Thank you.
-
Shayan_Aryan over 4 yearsdidn't work. callback gets called immediately before animation
-
Makks129 over 4 yearsThanks so much for this @nibarius
-
Kobato about 3 yearsInteresting thing. This method is getting called 3 times for me. 2 when entering activity, and once when it finished exiting. Although, it does seem to be reliable otherwise. I'll just add a flag to let it run code only once.
-
Kumar Santanu over 2 years@nibarius it say - new Handler() implement Method
-
nibarius over 2 yearsThe default constructor of
Handler
is deprecated these days I thinknew Handler(Looper.getMainLooper())
might work better.