Skip to content

Carousel

Eli Hart edited this page Jan 22, 2019 · 11 revisions

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. Snapping Support

  7. Customization

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.

// From the EpoxyController of the 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 outside padding to the Carousel and set setClipToPadding to false so that child views are shown through the padding.

Carousel enables this behavior with default outside padding and setClipToPadding disabled. You can call setPaddingDp or setPaddingRes to specify a custom padding value to use if needed.

This padding is applied to all outside edges, as well as in between items in the carousel via normal EpoxyRecyclerView item spacing

Alternatively you can use the Carousel.Padding class and setPadding(Padding) to flexibility control an individual padding amount of each side of the Carousel and in between items.

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

Snapping Support

By default a LinearSnapHelper is attached to all Carousel instances. If you would like to change the default snap behavior you can call Carousel.setDefaultGlobalSnapHelperFactory(...) and pass a factory object to create your snap helper. Null can be passed to disable snapping by default.

You may prefer to use something like this library for a different default snapping behavior, or your own custom implementation.

Customization

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 VerticalGridCarousel extends Carousel {

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

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

Any other additional customizations can also be made in this way.