Carousel

Composable Component

Composes a hero card rotator to highlight a piece of content.

Android
@ExperimentalTvMaterial3Api
@Composable
fun Carousel(
    itemCount: Int,
    modifier: Modifier = Modifier,
    carouselState: CarouselState = rememberCarouselState(),
    autoScrollDurationMillis: Long = CarouselDefaults.TimeToDisplayItemMillis,
    contentTransformStartToEnd: ContentTransform = CarouselDefaults.contentTransform,
    contentTransformEndToStart: ContentTransform = CarouselDefaults.contentTransform,
    carouselIndicator: @Composable BoxScope.() -> Unit = {
        CarouselDefaults.IndicatorRow(
            itemCount = itemCount,
            activeItemIndex = carouselState.activeItemIndex,
            modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp),
        )
    },
    content: @Composable AnimatedContentScope.(index: Int) -> Unit
)

Parameters

modifierModifier applied to the Carousel.
itemCounttotal number of items present in the carousel.
carouselStatestate associated with this carousel.
autoScrollDurationMillisduration for which item should be visible before moving to the next item.
contentTransformStartToEndanimation transform applied when we are moving from start to end in the carousel while scrolling to the next item
contentTransformEndToStartanimation transform applied when we are moving from end to start in the carousel while scrolling to the next item
carouselIndicatorindicator showing the position of the current item among all items.
contentdefines the items for a given index.

Code Examples

CarouselIndicatorWithRectangleShape

@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalAnimationApi::class)
@Composable
fun CarouselIndicatorWithRectangleShape() {
    val backgrounds =
        listOf(
            Color.Red.copy(alpha = 0.3f),
            Color.Yellow.copy(alpha = 0.3f),
            Color.Green.copy(alpha = 0.3f)
        )
    val carouselState = rememberCarouselState()
    Carousel(
        itemCount = backgrounds.size,
        modifier = Modifier.height(300.dp).fillMaxWidth(),
        carouselState = carouselState,
        carouselIndicator = {
            CarouselDefaults.IndicatorRow(
                itemCount = backgrounds.size,
                activeItemIndex = carouselState.activeItemIndex,
                modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp),
                indicator = { isActive ->
                    val activeColor = Color.Red
                    val inactiveColor = activeColor.copy(alpha = 0.5f)
                    Box(
                        modifier =
                            Modifier.size(8.dp)
                                .background(
                                    color = if (isActive) activeColor else inactiveColor,
                                    shape = RectangleShape,
                                ),
                    )
                }
            )
        },
        contentTransformEndToStart = fadeIn(tween(1000)).togetherWith(fadeOut(tween(1000))),
        contentTransformStartToEnd = fadeIn(tween(1000)).togetherWith(fadeOut(tween(1000)))
    ) { itemIndex ->
        Box(
            modifier =
                Modifier.background(backgrounds[itemIndex])
                    .border(2.dp, Color.White.copy(alpha = 0.5f))
                    .fillMaxSize()
        ) {
            var isFocused by remember { mutableStateOf(false) }
            Button(
                onClick = {},
                modifier =
                    Modifier.onFocusChanged { isFocused = it.isFocused }
                        // Duration of animation here should be less than or equal to carousel's
                        // contentTransform duration to ensure the item below does not disappear
                        // abruptly.
                        .animateEnterExit(
                            enter = slideInHorizontally(animationSpec = tween(1000)) { it / 2 },
                            exit = slideOutHorizontally(animationSpec = tween(1000))
                        )
                        .padding(40.dp)
                        .border(
                            width = 2.dp,
                            color = if (isFocused) Color.Red else Color.Transparent,
                            shape = RoundedCornerShape(50)
                        )
                        .padding(vertical = 2.dp, horizontal = 5.dp)
            ) {
                Text(text = "Play")
            }
        }
    }
}

SimpleCarousel

@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalAnimationApi::class)
@Composable
fun SimpleCarousel() {
    @Composable
    fun Modifier.onFirstGainingVisibility(onGainingVisibility: () -> Unit): Modifier {
        var isVisible by remember { mutableStateOf(false) }
        LaunchedEffect(isVisible) { if (isVisible) onGainingVisibility() }
        return onPlaced { isVisible = true }
    }
    @Composable
    fun Modifier.requestFocusOnFirstGainingVisibility(): Modifier {
        val focusRequester = remember { FocusRequester() }
        return focusRequester(focusRequester).onFirstGainingVisibility {
            focusRequester.requestFocus()
        }
    }
    val backgrounds =
        listOf(
            Color.Red.copy(alpha = 0.3f),
            Color.Yellow.copy(alpha = 0.3f),
            Color.Green.copy(alpha = 0.3f)
        )
    var carouselFocused by remember { mutableStateOf(false) }
    Carousel(
        itemCount = backgrounds.size,
        modifier =
            Modifier.height(300.dp).fillMaxWidth().onFocusChanged {
                carouselFocused = it.isFocused
            },
        contentTransformEndToStart = fadeIn(tween(1000)).togetherWith(fadeOut(tween(1000))),
        contentTransformStartToEnd = fadeIn(tween(1000)).togetherWith(fadeOut(tween(1000)))
    ) { itemIndex ->
        Box(
            modifier =
                Modifier.background(backgrounds[itemIndex])
                    .border(2.dp, Color.White.copy(alpha = 0.5f))
                    .fillMaxSize()
        ) {
            var buttonFocused by remember { mutableStateOf(false) }
            val buttonModifier =
                if (carouselFocused) {
                    Modifier.requestFocusOnFirstGainingVisibility()
                } else {
                    Modifier
                }
            Button(
                onClick = {},
                modifier =
                    buttonModifier
                        .onFocusChanged { buttonFocused = it.isFocused }
                        .padding(40.dp)
                        .border(
                            width = 2.dp,
                            color = if (buttonFocused) Color.Red else Color.Transparent,
                            shape = RoundedCornerShape(50)
                        )
                        // Duration of animation here should be less than or equal to carousel's
                        // contentTransform duration to ensure the item below does not disappear
                        // abruptly.
                        .animateEnterExit(
                            enter = slideInHorizontally(animationSpec = tween(1000)) { it / 2 },
                            exit = slideOutHorizontally(animationSpec = tween(1000))
                        )
                        .padding(vertical = 2.dp, horizontal = 5.dp)
            ) {
                Text(text = "Play")
            }
        }
    }
}

Create your own Component Library

Material Components are meant to be used as is and they do not allow customizations. To build your own Jetpack Compose component library use Compose Unstyled