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