Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 638
Beloo Avatar asked Oct 25 '16 13:10

Beloo


2 Answers

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

like image 156
Beloo Avatar answered Sep 24 '22 23:09

Beloo


@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.

like image 37
Malachi Holden Avatar answered Sep 22 '22 23:09

Malachi Holden