airbnb / epoxy

Epoxy is an Android library for building complex screens in a RecyclerView
https://goo.gl/eIK82p
Apache License 2.0
8.5k stars 733 forks source link

Reset carousel scrolling position #674

Closed AgneseBussone closed 5 years ago

AgneseBussone commented 5 years ago

Hello! Thanks for the great library!

I have a nested custom Carousel and I need to reset it to the initial position after refreshing the content. I'm using the version 2.19.0 and I load the models in the buildModels() like this

new OfferCarouselModel_()
                .id("offer")
                .padding(carouselPadding)
                .numViewsToShowOnScreen(1.1F)
                .models(generatePopularOfferModels())
                .addIf(!offersList.isEmpty(), this);

and when I need to refresh the content I clear the list in the controller, set the new data and then requestModelBuild().

Everything works fine, but if I scrolled through the carousel before refreshing its content it'll be at the old position, not at the first one. The Carousel class it's really simple

@ModelView(saveViewState = true, autoLayout = Size.MATCH_WIDTH_WRAP_HEIGHT)
public class OfferCarousel extends Carousel {
    private static final int SPAN_COUNT = 1;
    private static SnapHelperFactory offerCarouselSnapHelper =
            new SnapHelperFactory() {
                @Override
                @NonNull
                public SnapHelper buildSnapHelper(Context context) {
                    return new PagerSnapHelper();
                }
            };

    public OfferCarousel(Context context) {
    super(context);
    }

    @NonNull
    @Override
    protected LayoutManager createLayoutManager() {
        return new GridLayoutManager(getContext(), SPAN_COUNT, LinearLayoutManager.HORIZONTAL, false);
    }

    @Override
    protected SnapHelperFactory getSnapHelperFactory() {
        return offerCarouselSnapHelper;
    }

}

Am I missing something? Thank you!

elihart commented 5 years ago

Recyclerview automatically keeps scroll position when you update items in it.

you may need to programmatically scroll it to the start

AgneseBussone commented 5 years ago

My carousel is nested into an EpoxyRecyclerView that reset its position at the top automatically when I refresh the content, so I was expecting the same behaviour from the carousel.

I'm using the generated model class for the carousel and I don't see a way to reset the position programmatically (and per my understanding I'm not allowed to modify a model after the buildModels() has been called).

Should I use a different way to build the carousel?

AgneseBussone commented 5 years ago

I've tried resetting the position in the onBind() callback like this

        new OfferCarouselModel_()
                .id("offer")
                .padding(carouselPadding)
                .numViewsToShowOnScreen(1.1F)
                .models(generatePopularOfferModels())
                .onBind(new OnModelBoundListener<OfferCarouselModel_, OfferCarousel>() {
                    @Override
                    public void onModelBound(OfferCarouselModel_ model, OfferCarousel view, int position) {
                        view.scrollToPosition(0);
                    }
                })
                .addIf(!offersList.isEmpty(), this);

but still doesn't work.

elihart commented 5 years ago

try posting the call to view.scrollToPosition(0); - when you update a carousel's contents the model building is posted to the next frame, just like a normal EpoxyController requestModelBuild call

AgneseBussone commented 5 years ago

That solved my issue.

Thank you!

AgneseBussone commented 5 years ago

I need to reopen the issue because I still have some trouble with this.

I've tried this

new OfferCarouselModel_()
                .id("offer")
                .padding(carouselPadding)
                .numViewsToShowOnScreen(1.2F)
                .models(generatePopularOfferModels())
                .onBind(new OnModelBoundListener<OfferCarouselModel_, OfferCarousel>() {
                    @Override
                    public void onModelBound(OfferCarouselModel_ model, final OfferCarousel view, int position) {
                        if (resetOfferCarouselPosition()) {
                            new Handler().post(new Runnable() {
                                @Override
                                public void run() {
                                    view.scrollToPosition(0);
                                }
                            });
                        }
                    }
                })
                .addIf(!offersList.isEmpty(), this);

and it's working as expected, except that the onBind() is not always called. For example, if I build the models and then trigger the refresh of the data immediately ("swipe down to refresh" action), the onBind() won't be called, but I still need to reset the position of the carousel to the first item.

I've tried using an Interceptor, but the scrollToPosition() method is not available from the model; I need to access the view, but I'm not sure how to do that in the right way.

Is there a way to access the view from the model? Or is there a better way to handle this?

elihart commented 5 years ago

onBind() is always called when the model is bound. if it isn't called then the model isn't being bound again.

if you want direct access to the views you can use epoxyController.getAdapter().getBoundViewHolders - iterate through those and do whatever you want with them

AgneseBussone commented 5 years ago

I tried different things, but the only one that seems to work in all my use cases is the following

        if (resetOfferCarouselPosition) {
            BoundViewHolders boundViewHolders = getAdapter().getBoundViewHolders();
            EpoxyModel<?> model = getAdapter().getModelById(offerCarouselId);
            if (model != null) {
                EpoxyViewHolder holder = boundViewHolders.getHolderForModel(model);
                if (holder != null) {
                    ((OfferCarousel) holder.itemView).scrollToPosition(0);
                    resetOfferCarouselPosition = false;
                }
            }
        }

at the end of the buildModels(). Is there a better solution?