I've created a custom layout manager for RecyclerView. I've enabled auto-measuring in constructor of my manager via setAutoMeasureEnabled(true)
When RecyclerView's layout_height is set to wrap_content with this property LayoutManager is able to measure height of RecyclerView according to items inside it. It works perfectly, but when i delete the last item at bottom, the deleting animation is playing at that moment, it causes RecyclerView to measure it's height to result height before the animation have finished.
Look at this gif.
Where green background is a RecyclerView
As you may guessed this behaviour is correct for adding item, because in that case container should be measured before an animation
How can i handle this situation to make processing RecyclerView auto-measure after animation have finished?
I have all child positioning logic in onLayoutChildren but i think posting it doesn't required for this question and could be so broad and unclear.
Probably i should handle onMeasure interceptor of layoutManager manually (It called 4 times when item have deleted (one time before onItemsRemoved invocation)). So i could set measured height there, based on height, which i receive when deletion have started:
@Override
public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec) {
super.onMeasure(recycler, state, widthSpec, heightSpec);
requestSimpleAnimationsInNextLayout();
if (!isAutoMeasureEnabled()) {
setMeasuredDimension(getWidth(), preHeight);
}
}
@Override
public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
super.onItemsRemoved(recyclerView, positionStart, itemCount);
setAutoMeasureEnabled(false);
new Handler(Looper.myLooper()).postDelayed(new Runnable() {
@Override
public void run() {
setAutoMeasureEnabled(true);
requestSimpleAnimationsInNextLayout();
requestLayout();
}
}, 400);
preHeight = //calculate height before deletion
}
As you can see, i provided a handler with approximately delayed 400ms value, which execute auto measure after animations finished, so this could lead to needed results, but the duration of animation isn't static and i don't see any possibility to listen animation finished event.
So, desired behaviour:

By the way auto-measuring of LinearLayoutManager works with same way
If you pointed me to right direction with just text description of algorithm it would be enough.
I've at last figured it out. So, my solution:
Subscribe to dataChanges event in onAdapterChanged method and disable automeasuring.
@Override
public void onAdapterChanged(RecyclerView.Adapter oldAdapter,
RecyclerView.Adapter newAdapter) {
newAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
// on api 16 this is invoked before onItemsRemoved
super.onItemRangeRemoved(positionStart, itemCount);
/** we detected removing event, so should process measuring manually
*/
setAutoMeasureEnabled(false);
}
});
//Completely scrap the existing layout
removeAllViews();
}
In onMeasure in case auto-measuring disabled measure manually, use current size:
@Override
public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec) {
super.onMeasure(recycler, state, widthSpec, heightSpec);
if (!isAutoMeasureEnabled()) {
// we should perform measuring manually
// so request animations
requestSimpleAnimationsInNextLayout();
//keep size until remove animation will be completed
setMeasuredDimension(getWidth(), getHeight());
}
}
Perform auto-measuring after animation finished:
@Override
public void onItemsRemoved(final RecyclerView recyclerView, int positionStart, int itemCount) {
super.onItemsRemoved(recyclerView, positionStart, itemCount);
//subscribe to next animations tick
postOnAnimation(new Runnable() {
@Override
public void run() {
//listen removing animation
recyclerView.getItemAnimator().isRunning(new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
@Override
public void onAnimationsFinished() {
//when removing animation finished return auto-measuring back
setAutoMeasureEnabled(true);
// and process onMeasure again
requestLayout();
}
});
}
});
This callbacks chain is safe, because postOnAnimation runnable won't be executed in case layout manager is detached from RecyclerView
@Beloo's answer was not working for me, I suspect because setAutoMeasureEnabled has been deprecated. I was able to get it working by only overriding onItemsRemoved, onMeasure and also isAutoMeasureEnabled.
First create a variable that will keep track of the isAutoMeasureEnabled property:
private boolean autoMeasureEnabled = true;
Then override isAutoMeasureEnabled to use the variable:
@Override
public boolean isAutoMeasureEnabled (){
return autoMeasureEnabled;
}
Now you can set the variable autoMeasureEnabled at will. Override onItemsRemoved:
@Override
public void onItemsRemoved(@NonNull final RecyclerView recyclerView, int positionStart, int itemCount) {
super.onItemsRemoved(recyclerView, positionStart, itemCount);
autoMeasureEnabled = false;
postOnAnimation(new Runnable() {
@Override
public void run() {
recyclerView.getItemAnimator().isRunning(new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
@Override
public void onAnimationsFinished() {
autoMeasureEnabled = true;
requestLayout();
}
});
}
});
}
Also override onMeasure just like in @Beloo's answer:
@Override
public void onMeasure(@NotNull RecyclerView.Recycler recycler, @NotNull RecyclerView.State state, int widthSpec, int heightSpec) {
super.onMeasure(recycler, state, widthSpec, heightSpec);
if (!isAutoMeasureEnabled()) {
// we should perform measuring manually
// so request animations
requestSimpleAnimationsInNextLayout();
//keep size until remove animation will be completed
setMeasuredDimension(getWidth(), getHeight());
}
}
This seems to work. The animation is fast, but timed correctly.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With