Carousel with MotionLayout

Carousel is a motion helper object to build custom carousel views that show a list of elements that the user can skim through. Compared to other ways to implement such views, this helper lets you quickly create complex motion and dimension changes for your Carousel by taking advantage of MotionLayout.

The Carousel widget supports lists with a start and end as well as circular wrap-around lists.

How Carousel with MotionLayout works

Suppose you want to build a horizontal Carousel view, with the center item enlarged:

This basic layout contains several views representing the Carousel items:

Create a MotionLayout with the following three states and give them IDs:

  • previous
  • start
  • next

If the start state corresponds to the base layout, in the previous state and the next state the Carousel items are shifted by one to the left and to the right, respectively.

For example, take the five views in figure 3 and assume that in the start state, views B, C, and D are visible, and A and E are outside of the screen. Set up the previous state so that the positions of A, B, C, and D are where B, C, D, and E were, with the views moving from left to right. In the next state, the opposite needs to happen, with B, C, D, and E moving to where A, B, C, and D were, and the views moving from right to left. This is shown in figure 4:

It's critical that the views end up exactly where the original views start. Carousel gives the illusion of an infinite collection of elements by moving the actual views back to where they were, but reinitializing them with the new matching content. The following diagram shows this mechanism. Pay attention to the "item #" values):

Transitions

With these three constraint sets defined in your motion scene file, create two transitions—forward and backward—between the start and next states and the start and previous states. Add an OnSwipe handler to trigger the transitions in response to a gesture, as shown in the following example:

    <Transition
        motion:constraintSetStart="@id/start"
        motion:constraintSetEnd="@+id/next"
        motion:duration="1000"
        android:id="@+id/forward">
        <OnSwipe
            motion:dragDirection="dragLeft"
            motion:touchAnchorSide="left" />
    </Transition>

    <Transition
        motion:constraintSetStart="@+id/start"
        motion:constraintSetEnd="@+id/previous"
        android:id="@+id/backward">
        <OnSwipe
            motion:dragDirection="dragRight"
            motion:touchAnchorSide="right" />
    </Transition>

After this basic motion scene is created, add a Carousel helper to the layout and reference the views in the same order you implement your previous and next animation.

Set the following attributes for the Carousel helper:

  • app:carousel_firstView: the view that represents the first element of the Carousel—in this example, C.
  • app:carousel_previousState: the ConstraintSet ID of the previous state.
  • app:carousel_nextState: the ConstraintSet ID of the next state.
  • app:carousel_backwardTransition: the Transition ID applied between the start and previous states.
  • app:carousel_forwardTransition: the Transition ID applied between the start and next states.

For example, you have something like this in your layout XML file:

    <androidx.constraintlayout.motion.widget.MotionLayout ... >

        <ImageView  android:id="@+id/imageView0" .. />
        <ImageView  android:id="@+id/imageView1" .. />
        <ImageView  android:id="@+id/imageView2" .. />
        <ImageView  android:id="@+id/imageView3" .. />
        <ImageView  android:id="@+id/imageView4" .. />

        <androidx.constraintlayout.helper.widget.Carousel
            android:id="@+id/carousel"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:carousel_forwardTransition="@+id/forward"
            app:carousel_backwardTransition="@+id/backward"
            app:carousel_previousState="@+id/previous"
            app:carousel_nextState="@+id/next"
            app:carousel_infinite="true"
            app:carousel_firstView="@+id/imageView2"
            app:constraint_referenced_ids="imageView0,imageView1,imageView2,imageView3,imageView4" />

    </androidx.constraintlayout.motion.widget.MotionLayout>

Set up a Carousel adapter in code:

Kotlin

carousel.setAdapter(object : Carousel.Adapter {
            override fun count(): Int {
              // Return the number of items in the Carousel.
            }

            override fun populate(view: View, index: Int) {
                // Implement this to populate the view at the given index.
            }

            override fun onNewItem(index: Int) {
                // Called when an item is set.
            }
        })

Java

carousel.setAdapter(new Carousel.Adapter() {
            @Override
            public int count() {
                // Return the number of items in the Carousel.
            }

            @Override
            public void populate(View view, int index) {
                // Populate the view at the given index.
            }

            @Override
            public void onNewItem(int index) {
                 // Called when an item is set.
            }
        });

Additional notes

Depending on the current item "selected" in the Carousel, the views representing the items before or after might need to be hidden to correctly account for the Carousel start and end. The Carousel helper handles this automatically. By default, it marks those views as View.INVISIBLE in these situations, so the overall layout doesn't change.

An alternative mode is available in which the Carousel helper instead marks those views as View.GONE. You can set this mode using the following property:

app:carousel_emptyViewsBehavior="gone"

Examples

For more examples using the Carousel helper, see the example projects on GitHub.