Beloo Beloo - 1 month ago 12
Android Question

Custom RecyclerView's LayoutManager. Automeasuring after animation finished on item delete

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.
enter image description here
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:

enter image description here

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.

Answer

I've at last figured it out. So, my solution:

Subscribe to dataChanges event in onAdapterChanged method and store previosly afto-measured value of height.

 @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);
            beforeRemovingHeight = autoMeasureHeight;
            /** 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:

 @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 height until remove animation will be completed
        setMeasuredDimension(View.MeasureSpec.getSize(widthSpec), beforeRemovingHeight);
    }
}

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