New Compose Multiplatform components arrived on Composables UICheck it out →

Carousel

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

Last updated:

Installation

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

Overloads

@Composable
@ExperimentalTvMaterial3Api
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

@Composable
@Sampled
@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalAnimationApi::class
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

@Composable
@Sampled
@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalAnimationApi::class
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")
            }
        }
    }
}