Primary tabs are placed at the top of the content pane under a top app bar.
FancyAnimatedIndicatorWithModifier
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TabIndicatorScope.FancyAnimatedIndicatorWithModifier(
index: Int,
isScrollable: Boolean = false,
) {
val colors =
listOf(
MaterialTheme.colorScheme.primary,
MaterialTheme.colorScheme.secondary,
MaterialTheme.colorScheme.tertiary,
)
var startAnimatable by remember { mutableStateOf<Animatable<Dp, AnimationVector1D>?>(null) }
var endAnimatable by remember { mutableStateOf<Animatable<Dp, AnimationVector1D>?>(null) }
val coroutineScope = rememberCoroutineScope()
val indicatorColor: Color by animateColorAsState(colors[index % colors.size], label = "")
Box(
Modifier.tabIndicatorLayout {
measurable: Measurable,
constraints: Constraints,
tabPositions: List<TabPosition> ->
val newStart = tabPositions[index].left
val newEnd = tabPositions[index].right
val startAnim =
startAnimatable
?: Animatable(newStart, Dp.VectorConverter).also { startAnimatable = it }
val endAnim =
endAnimatable
?: Animatable(newEnd, Dp.VectorConverter).also { endAnimatable = it }
if (endAnim.targetValue != newEnd) {
coroutineScope.launch {
endAnim.animateTo(
newEnd,
animationSpec =
if (endAnim.targetValue < newEnd) {
spring(dampingRatio = 1f, stiffness = 1000f)
} else {
spring(dampingRatio = 1f, stiffness = 50f)
},
)
}
}
if (startAnim.targetValue != newStart) {
coroutineScope.launch {
startAnim.animateTo(
newStart,
animationSpec =
// Handle directionality here, if we are moving to the right, we
// want the right side of the indicator to move faster, if we are
// moving to the left, we want the left side to move faster.
if (startAnim.targetValue < newStart) {
spring(dampingRatio = 1f, stiffness = 50f)
} else {
spring(dampingRatio = 1f, stiffness = 1000f)
},
)
}
}
val indicatorEnd = endAnim.value.roundToPx()
val indicatorStart = startAnim.value.roundToPx()
// Apply an offset from the start to correctly position the indicator around the tab
val placeable =
measurable.measure(
constraints.copy(
maxWidth = indicatorEnd - indicatorStart,
minWidth = indicatorEnd - indicatorStart,
)
)
layout(constraints.maxWidth, constraints.maxHeight) {
val contentWidth = tabPositions[index].contentWidth.roundToPx()
val tabWidth = tabPositions[index].width.roundToPx()
val relativeOffset = if (isScrollable) (tabWidth - contentWidth) / 2 else 0
placeable.place(indicatorStart - relativeOffset, 0)
}
}
.padding(5.dp)
.fillMaxSize()
.drawWithContent {
drawRoundRect(
color = indicatorColor,
cornerRadius = CornerRadius(5.dp.toPx()),
style = Stroke(width = 2.dp.toPx()),
)
}
)
}
FancyIndicator
@Composable
fun FancyIndicator(color: Color, modifier: Modifier = Modifier) {
// Draws a rounded rectangular with border around the Tab, with a 5.dp padding from the edges
// Color is passed in as a parameter [color]
Box(
modifier
.padding(5.dp)
.fillMaxSize()
.border(BorderStroke(2.dp, color), RoundedCornerShape(5.dp))
)
}
FancyIndicatorContainerTabs
@Preview
@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun FancyIndicatorContainerTabs() {
var state by remember { mutableStateOf(0) }
val titles = listOf("Tab 1", "Tab 2", "Tab 3")
Column {
SecondaryTabRow(
selectedTabIndex = state,
indicator = { FancyAnimatedIndicatorWithModifier(state) },
) {
titles.forEachIndexed { index, title ->
Tab(selected = state == index, onClick = { state = index }, text = { Text(title) })
}
}
Text(
modifier = Modifier.align(Alignment.CenterHorizontally),
text = "Fancy transition tab ${state + 1} selected",
style = MaterialTheme.typography.bodyLarge,
)
}
}
FancyIndicatorTabs
@Preview
@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun FancyIndicatorTabs() {
var state by remember { mutableStateOf(0) }
val titles = listOf("Tab 1", "Tab 2", "Tab 3")
Column {
SecondaryTabRow(
selectedTabIndex = state,
indicator = {
FancyIndicator(
MaterialTheme.colorScheme.primary,
Modifier.tabIndicatorOffset(state),
)
},
) {
titles.forEachIndexed { index, title ->
Tab(selected = state == index, onClick = { state = index }, text = { Text(title) })
}
}
Text(
modifier = Modifier.align(Alignment.CenterHorizontally),
text = "Fancy indicator tab ${state + 1} selected",
style = MaterialTheme.typography.bodyLarge,
)
}
}
FancyTab
@Composable
fun FancyTab(title: String, onClick: () -> Unit, selected: Boolean) {
Tab(selected, onClick) {
Column(
Modifier.padding(10.dp).height(50.dp).fillMaxWidth(),
verticalArrangement = Arrangement.SpaceBetween,
) {
Box(
Modifier.size(10.dp)
.align(Alignment.CenterHorizontally)
.background(
color =
if (selected) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.background
)
)
Text(
text = title,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.align(Alignment.CenterHorizontally),
)
}
}
}
FancyTabs
@Preview
@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun FancyTabs() {
var state by remember { mutableStateOf(0) }
val titles = listOf("Tab 1", "Tab 2", "Tab 3")
Column {
SecondaryTabRow(selectedTabIndex = state) {
titles.forEachIndexed { index, title ->
FancyTab(title = title, onClick = { state = index }, selected = (index == state))
}
}
Text(
modifier = Modifier.align(Alignment.CenterHorizontally),
text = "Fancy tab ${state + 1} selected",
style = MaterialTheme.typography.bodyLarge,
)
}
}
PrimaryTextTabs
@Preview
@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun PrimaryTextTabs() {
var state by remember { mutableStateOf(0) }
val titles = listOf("Tab 1", "Tab 2", "Tab 3 with lots of text")
Column {
PrimaryTabRow(selectedTabIndex = state) {
titles.forEachIndexed { index, title ->
Tab(
selected = state == index,
onClick = { state = index },
text = { Text(text = title, maxLines = 2, overflow = TextOverflow.Ellipsis) },
)
}
}
Text(
modifier = Modifier.align(Alignment.CenterHorizontally),
text = "Primary tab ${state + 1} selected",
style = MaterialTheme.typography.bodyLarge,
)
}
}