We just launched Compose Examples featuring over 150+ components! Check it out →

Carousel

Android

Component in Tv Material Compose

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

Note: The animations and focus management features have been dropped temporarily due to some technical challenges. If you need them, consider using the previous version of the library (1.0.0-alpha10) or kindly wait until the next alpha version (1.1.0-alpha01).

Last updated:

Installation

dependencies {
   implementation("androidx.tv:tv-material:1.0.0")
}

Overloads

@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

namedescription
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

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")
            }
        }
    }
}

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")
            }
        }
    }
}
by @alexstyl