Carousel
Slides one beside another, one of them on screen: pictures, cards, the steps of an introduction. A swipe turns them; what is under them says where the carousel is, and turns them for a reader with no finger on the screen.
import com.pantrixui.components.data.carousel.PantrixCarousel
- Indicators. Dots under the slides, a count ("2 / 4") or nothing:
CarouselStyle.indicator. - Previous and next buttons. They show where the platform is not used by touch, on desktop and in a browser on a desk, and not in a phone's browser. What decides is the pointer the screen is used with, not whether the screen can be touched: they show on a laptop with a touch screen too.
CarouselStyle.controlsshows them everywhere or never. - Neighbouring slides. With
peekthe edges of the slides at the two sides show: what a swipe will bring is already in sight. - Slides that go round. With
loopthe first comes after the last. - Turning by itself. With
autoAdvanceMillisthe carousel turns by itself; it waits while the reader holds a slide, while the mouse is over it and while the keyboard's focus is in it. The button beside the dots stops it and starts it again. Where the theme's motion is reduced it does not turn by itself. - Over the slide.
CarouselPlacement.Overlaidputs the buttons and the dots over the slide, on a fill of their own: in a gallery they take no room. - Height. In a carousel given a height, the slides fill the room the dots leave. A carousel given none is as tall as the slide on screen, and its height changes without a jump on the way from one slide to another.
- Touch. For a finger the dots are one target: its left half goes one slide back and its right half one slide forward; in a right-to-left layout the halves change places.
Examples
Featured: with dots
Its plainest form: four slides with dots under them. The slide's index is the screen's state; the carousel takes it with currentPage and reports it back with onPageChange when a swipe ends. The line below reads the same state.
The height was given to the carousel (180dp): the slides fill the room the dots leave. On desktop and in a browser on a desk the previous and next buttons show at the two sides as well; on a phone, in its browser too, a swipe is enough.
var slide by rememberSaveable { mutableIntStateOf(0) }
PantrixCarousel(
pageCount = TITLES.size,
currentPage = slide,
onPageChange = { slide = it },
modifier = Modifier.height(180.dp),
contentDescription = "Featured",
tagKey = "featured"
) { index ->
CarouselSlide(index, modifier = Modifier.fillMaxSize())
}
PantrixText(text = "On screen: ${TITLES[slide]}", color = PantrixTheme.colors.textSecondary)
@Composable
private fun CarouselSlide(
index: Int,
modifier: Modifier = Modifier
) {
val role = roles()[index % ROLES]
PantrixSurface(modifier = modifier, color = role.container, contentColor = role.onContainer) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
PantrixText(text = TITLES[index % TITLES.size], style = PantrixTheme.typography.title)
}
}
}
@Composable
private fun roles(): List<PantrixColor> {
val colors = PantrixTheme.colors
return listOf(colors.info, colors.success, colors.warning, colors.danger)
}
private const val ROLES = 4
private val TITLES = listOf("New arrivals", "Pick of the week", "On sale", "Last chance")
Cards whose neighbours show, with a count and buttons
With peek = 24.dp a slide's neighbours show at its two sides: what a swipe will bring is already in sight. indicator = CarouselIndicator.Count writes a count in place of the dots; controls = CarouselControls.Always shows the buttons on a phone too.
var slide by rememberSaveable { mutableIntStateOf(1) }
PantrixCarousel(
pageCount = TITLES.size,
currentPage = slide,
onPageChange = { slide = it },
contentDescription = "Offers",
style = CarouselStyle(
peek = 24.dp,
indicator = CarouselIndicator.Count,
controls = CarouselControls.Always
),
tagKey = "offers"
) { index ->
CarouselSlide(index, modifier = Modifier.fillMaxWidth().height(96.dp))
}
@Composable
private fun CarouselSlide(
index: Int,
modifier: Modifier = Modifier
) {
val role = roles()[index % ROLES]
PantrixSurface(modifier = modifier, color = role.container, contentColor = role.onContainer) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
PantrixText(text = TITLES[index % TITLES.size], style = PantrixTheme.typography.title)
}
}
}
private val TITLES = listOf("New arrivals", "Pick of the week", "On sale", "Last chance")
@Composable
private fun roles(): List<PantrixColor> {
val colors = PantrixTheme.colors
return listOf(colors.info, colors.success, colors.warning, colors.danger)
}
private const val ROLES = 4
An introduction that goes on by its own button
An introduction that cannot be swiped: swipeEnabled = false, with the indicator and the buttons off too. The steps go on only with "Continue". Which step it is on is the screen's own state; the button adds one to it, and the carousel turns there.
var step by rememberSaveable { mutableIntStateOf(0) }
val last = STEPS.lastIndex
PantrixCarousel(
pageCount = STEPS.size,
currentPage = step,
onPageChange = { step = it },
swipeEnabled = false,
contentDescription = "Introduction",
style = CarouselStyle(indicator = CarouselIndicator.None, controls = CarouselControls.Never),
tagKey = "introduction"
) { index ->
PantrixSurface(modifier = Modifier.fillMaxWidth(), border = PantrixTheme.colors.divider) {
PantrixText(text = STEPS[index], modifier = Modifier.padding(PantrixTheme.spacing.lg))
}
}
Row(
horizontalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.md),
verticalAlignment = Alignment.CenterVertically
) {
PantrixButton(
text = if (step == last) "Start over" else "Continue",
onClick = { step = if (step == last) 0 else step + 1 },
variant = ButtonVariant.Soft,
size = ButtonSize.Small,
tagKey = "introduction_next"
)
PantrixText(text = "Step ${step + 1} of ${STEPS.size}", color = PantrixTheme.colors.textSecondary)
}
private val STEPS = listOf(
"Welcome to Pantrix. This introduction takes three steps.",
"The components speak the same design language on every platform.",
"You are ready: you can go back to the list of components and open one."
)
On a fill
A carousel on a fill that is not one of the theme's grounds. No colour was given: the fill says what colour is written on it. The dot of the slide on screen and the buttons are in that colour, and the other dots in a share of it.
var slide by rememberSaveable { mutableIntStateOf(0) }
PantrixSurface(modifier = Modifier.fillMaxWidth(), color = PantrixTheme.colors.primary.color) {
PantrixCarousel(
pageCount = 3,
currentPage = slide,
onPageChange = { slide = it },
modifier = Modifier.padding(PantrixTheme.spacing.md),
contentDescription = "On a fill",
style = CarouselStyle(controls = CarouselControls.Always),
tagKey = "filled"
) { index ->
CarouselSlide(index, modifier = Modifier.fillMaxWidth().height(72.dp))
}
}
@Composable
private fun CarouselSlide(
index: Int,
modifier: Modifier = Modifier
) {
val role = roles()[index % ROLES]
PantrixSurface(modifier = modifier, color = role.container, contentColor = role.onContainer) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
PantrixText(text = TITLES[index % TITLES.size], style = PantrixTheme.typography.title)
}
}
}
@Composable
private fun roles(): List<PantrixColor> {
val colors = PantrixTheme.colors
return listOf(colors.info, colors.success, colors.warning, colors.danger)
}
private const val ROLES = 4
private val TITLES = listOf("New arrivals", "Pick of the week", "On sale", "Last chance")
A gallery that goes round by itself
Like a picture gallery: with loop the slides go round, and with autoAdvanceMillis the carousel turns by itself every six seconds. placement = CarouselPlacement.Overlaid puts the buttons and the dots over the slide, on the carousel's own fill (dark in the light theme, light in the dark one); they take no room.
The button beside the dots stops the turning and starts it again. While a slide is held, while the mouse is over the carousel and while the keyboard's focus is in it, the carousel waits by itself.
var slide by rememberSaveable { mutableIntStateOf(0) }
PantrixCarousel(
pageCount = TITLES.size,
currentPage = slide,
onPageChange = { slide = it },
modifier = Modifier.height(200.dp),
contentDescription = "Gallery",
loop = true,
autoAdvanceMillis = GALLERY_MILLIS,
style = CarouselStyle(controls = CarouselControls.Always, placement = CarouselPlacement.Overlaid),
tagKey = "gallery"
) { index ->
CarouselSlide(index, modifier = Modifier.fillMaxSize())
}
@Composable
private fun CarouselSlide(
index: Int,
modifier: Modifier = Modifier
) {
val role = roles()[index % ROLES]
PantrixSurface(modifier = modifier, color = role.container, contentColor = role.onContainer) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
PantrixText(text = TITLES[index % TITLES.size], style = PantrixTheme.typography.title)
}
}
}
private val TITLES = listOf("New arrivals", "Pick of the week", "On sale", "Last chance")
private const val GALLERY_MILLIS = 6_000
@Composable
private fun roles(): List<PantrixColor> {
val colors = PantrixTheme.colors
return listOf(colors.info, colors.success, colors.warning, colors.danger)
}
private const val ROLES = 4
Slides of different heights
The slides differ in height, and the carousel was given no height. The carousel is as tall as the slide on screen; on the way from one slide to another its height changes along with it, without a jump. The line below shows this: it moves down and up with the slide.
var slide by rememberSaveable { mutableIntStateOf(0) }
PantrixCarousel(
pageCount = NOTES.size,
currentPage = slide,
onPageChange = { slide = it },
contentDescription = "Notes",
loop = true,
tagKey = "notes"
) { index ->
PantrixSurface(modifier = Modifier.fillMaxWidth(), border = PantrixTheme.colors.divider) {
PantrixText(text = NOTES[index], modifier = Modifier.padding(PantrixTheme.spacing.lg))
}
}
PantrixText(
text = "This line moves with the height of the slides.",
color = PantrixTheme.colors.textSecondary
)
private val NOTES = listOf(
"A short note.",
"A longer note: it runs to several lines, because it has more to say. As the carousel comes to this slide, " +
"its height grows gradually too; the line below moves down.",
"A medium-sized note: it takes about two lines and is shorter than the one before."
)
In its own colours
In its own colours: CarouselColors paints the dots, the dots' track, the buttons and the scrim over the slide. Here the dots are in the colour of success, not the brand's; every colour not given is the theme's.
var slide by rememberSaveable { mutableIntStateOf(0) }
PantrixCarousel(
pageCount = TITLES.size,
currentPage = slide,
onPageChange = { slide = it },
modifier = Modifier.height(160.dp),
colors = CarouselColors(
indicator = PantrixTheme.colors.success.color,
indicatorTrack = PantrixTheme.colors.success.container,
control = PantrixTheme.colors.success.color
),
contentDescription = "Featured",
tagKey = "green"
) { index ->
CarouselSlide(index, modifier = Modifier.fillMaxSize())
}
@Composable
private fun CarouselSlide(
index: Int,
modifier: Modifier = Modifier
) {
val role = roles()[index % ROLES]
PantrixSurface(modifier = modifier, color = role.container, contentColor = role.onContainer) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
PantrixText(text = TITLES[index % TITLES.size], style = PantrixTheme.typography.title)
}
}
}
@Composable
private fun roles(): List<PantrixColor> {
val colors = PantrixTheme.colors
return listOf(colors.info, colors.success, colors.warning, colors.danger)
}
private const val ROLES = 4
private val TITLES = listOf("New arrivals", "Pick of the week", "On sale", "Last chance")
API
pageCount
Slides one beside another, one of them on screen: pictures, cards, the steps of an introduction. A swipe turns them; what is under them says where the carousel is, and turns them for a reader with no finger on the screen.
| Parameter | Type | Default | What it is |
|---|---|---|---|
pageCount | Int | how many slides there are. | |
currentPage | Int | the slide on screen, counted from zero, as a pager's is. One past either end is the nearest slide. A PantrixPagination counts its pages from one, as it writes them: its page is this and one. | |
onPageChange | (page: Int) -> Unit | told the slide a swipe ended on, and the slide a button, a dot or a key asks for, or the carousel itself when it turns by itself. | |
modifier | Modifier | Modifier | applied to the carousel: the slides and what is under them. |
contentDescription | String? | null | what the carousel holds, for a screen reader: "Featured products". |
swipeEnabled | Boolean | true | whether a swipe turns the slides. An introduction that goes on by its own button turns it off. |
loop | Boolean | false | whether the slides go round, the last on to the first. A carousel of one slide has nowhere to go. |
autoAdvanceMillis | Int | 0 | how long the carousel stays on a slide before it turns to the next by itself, or nothing for a carousel that is turned by its reader alone. Long enough to read a slide in: five seconds or more. |
style | CarouselStyle? | null | this call's own style: over every enclosing one and under the density. Only the fields it sets change, on every platform. |
colors | CarouselColors? | null | this call's own colours for what is under the slides. Only the ones it sets change. |
tagKey | String? | null | a locale-independent key for automation: the carousel's tag is Carousel_<tagKey>, and its parts are named inside it: Carousel_<tagKey>.Slides_view, which takes the keyboard's focus, .Page_<index>, .Control_previous, .Control_next, .Control_pause and .Indicator_position. Where the slides go round and a slide's neighbours show, a carousel of two slides has the same slide at both sides, twice under one tag. |
page | @Composable (index: Int) -> Unit | a slide, by its index. |
CarouselStyle
PantrixCarousel's geometry and what it shows under its slides. It is the same on every platform: the buttons come and go with the pointer the reader has, not with the platform. The rooms follow the density; a dot does not.
| Parameter | Type | Default | What it is |
|---|---|---|---|
pageSpacing | Dp | Dp.Unspecified | Between two slides, seen while one gives way to the next and beside a slide whose neighbours peek. |
peek | Dp | Dp.Unspecified | How much room is kept at each side of the current slide, in which its neighbours show. |
indicatorSize | Dp | Dp.Unspecified | A dot of the indicator. |
indicatorGap | Dp | Dp.Unspecified | Between two dots. |
indicatorSpacing | Dp | Dp.Unspecified | Between the slides and what is under them. Where that stands over the slides, between it and the slides' edges. |
indicator | CarouselIndicator? | null | What says where the carousel is. |
controls | CarouselControls? | null | Where the two buttons are shown. |
placement | CarouselPlacement? | null | Whether those stand under the slides or over them. |
CarouselColors
What a PantrixCarousel is painted with under its slides, for a carousel that does not take the theme's colours: a colour that is set is used, and one that is left out stays the theme's. The slides are the caller's own.
| Parameter | Type | Default | What it is |
|---|---|---|---|
indicator | Color | Color.Unspecified | The dot of the current slide. The primary role's color. |
indicatorTrack | Color | Color.Unspecified | The dots of the other slides. The theme's borderStrong, which is told from a ground as a control's own mark is. |
control | Color | Color.Unspecified | The buttons. The theme's onSurface, an icon's colour. |
overlay | Color | Color.Unspecified | The fill behind the dots and behind each button where they stand over the slides. The theme's onSurface, the tone opposite the theme's grounds, thinned so that the slide shows through it. One of the theme's role colours given here brings the colour the theme pairs with it for what is written on it. |
Accessibility
- Keyboard. The slides are one stop: the arrow keys turn them, and Home and End go to the first and the last.
- Screen reader. It says where the carousel is at every change: "Slide 2 of 4". A carousel that turns by itself does not break in at every turn.