Skip to content

Carousel

Eli Hart edited this page Sep 27, 2017 · 11 revisions

Since 2.6.0. This is in Beta, please leave feedback by opening an issue!

This is intended as a plug and play "Carousel" view - a RecyclerView with horizontal scrolling. It comes with common defaults and performance optimizations and can be either used as a top level RecyclerView, or nested within a vertical RecyclerView.

This class provides:

  1. Automatic integration with Epoxy for nested RecyclerView usage

  2. Default horizontal padding for carousel peeking

  3. Easily control how many items are shown on screen at a time

  4. All of the benefits of EpoxyRecyclerView

  5. Kotlin Extensions

  6. Snap Helper and other customizations

If you need further flexibility you can subclass this view to change its width, height, scrolling direction, etc. You can annotate a subclass with @ModelView to generate a new EpoxyModel.

As a Nested RecyclerView

The Carousel class can be used via xml in an Activity or Fragment like a normal RecyclerView, but a very common use case will be as a nested horizontal list in a vertically scrolling RecyclerView.

Normally nested RecyclerViews are a bit tricky to set up correctly, as you need to coordinate sharing a view pool, recycling views correctly on unbind, setting up "peeking", optimizing for the number of items shown, and so on. Epoxy handles all of this for you.

A CarouselModel_ class is generated from the Carousel view that you can use in the EpoxyController of your parent, vertical RecyclerView.

void buildModels() {
   ...

   List<PhotoViewModel_> photoModels = new ArrayList();
   for (photo in photos) {
      photoModels.add(new PhotoViewModel_()
                 .id(photo.id())
                 .url(photo.url))
   }

   new CarouselModel_()
      .id("carousel")
      .models(photoModels)
      .addTo(this);
}

See the sample app for detailed example code of setting up complex nested Carousels.

Horizontal Padding

A common behavior in a carousel is to allow the previous and next items to "peek" from the sides. This indicates to the user that there is more content to scroll to.

We may still want our first item aligned to a certain margin, so a good way to implement this is to add side padding to the Carousel and set setClipToPadding to false so that child views are shown in the padding.

Carousel enables this behavior with a default side padding and disables setClipToPadding. You can call setCarouselPadding to specify a custom padding value to use if needed.

If the carousel scrolls horizontally then this padding is applied to the left and right sides. If it is vertically scrolling it is applied to the top and bottom.

Items on Screen

You can set the number of views to show on screen in a carousel at a time with setNumViewsToShowOnScreen.

This is useful where you want to easily control for the number of items on screen, regardless of screen size. For example, you could set this to 1.2f so that one view is shown in full and 20% of the next view "peeks" from the edge to indicate that there is more content to scroll to.

Another pattern is setting a different view count depending on whether the device is phone or tablet.

Additionally, if a LinearLayoutManager is used this value will be forwarded to LinearLayoutManager#setInitialPrefetchItemCount as a performance optimization.

If you want to change the prefetch count without changing the view size you can simply use setInitialPrefetchItemCount(int)

Kotlin Extensions

With Kotlin extension functions we can further simplify the model building above to just

fun buildModels() {
   ...

   carousel {
        id("carousel")
        numViewsToShowOnScreen(5)

        withModelsFrom(photos) {
            PhotoViewModel_()
                   .id(it.id)
                   .url(it.url)
          }
    }
}

This uses the functions

/** For use in the buildModels method of EpoxyController. A shortcut for creating a Carousel model, initializing it, and adding it to the controller.
 *
 */
inline fun EpoxyController.carousel(modelInitializer: CarouselModelBuilder.() -> Unit) {
    CarouselModel_().apply {
        modelInitializer()
    }.addTo(this)
}

/** Add models to a CarouselModel_ by transforming a list of items into EpoxyModels.
 *
 * @param items The items to transform to models
 * @param modelBuilder A function that take an item and returns a new EpoxyModel for that item.
 */
inline fun <T> CarouselModelBuilder.withModelsFrom(
        items: List<T>,
        modelBuilder: (T) -> EpoxyModel<*>
) {
    models(items.map { modelBuilder(it) })
}

Epoxy does not yet package Kotlin extensions, but you can add this to your own project if needed.

Snap Helper and Other Customizations

If you would like to add new behavior to the Carousel you can simply subclass it. For example:

@ModelView(autoLayout = Size.WRAP_WIDTH_MATCH_HEIGHT)
static class VerticalSnappingCarousel extends Carousel {

    public SnappingCarousel(Context context) {
      super(context);
      new LinearSnapHelper().attachToRecyclerView(this);
    }

    @Override
    protected LayoutManager createLayoutManager() {
      return new GridLayoutManager(getContext(), 2, GridLayoutManager.VERTICAL, false);
    }
}

By annotating our subclass with ModelView Epoxy will generate a new EpoxyModel for this view. We have also changed the size here to be a column via the autoLayout param. Alternatively you could provide a defaultLayout param to style the view with a layout xml file.

By overriding createLayoutManager we have changed the layout to a vertical grid.

Lastly, in the constructor we attached a snap helper to provide the snapping behavior that is commonly used in Carousels. This uses a default Android snap helper, but you may also prefer to use something like this library for a different snapping behavior.