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

modifier Modifier applied to the Carousel.
itemCount total number of items present in the carousel.
carouselState state associated with this carousel.
autoScrollDurationMillis duration for which item should be visible before moving to the next item.
contentTransformStartToEnd animation transform applied when we are moving from start to end in the carousel while scrolling to the next item
contentTransformEndToStart animation transform applied when we are moving from end to start in the carousel while scrolling to the next item
carouselIndicator indicator showing the position of the current item among all items.
content defines 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")
            }
        }
    }
}