{% setvar book_path %}/reference/kotlin/dokkatest/_book.yaml{% endsetvar %} {% include "_shared/_reference-head-tags.html" %}
Unit |
TabRow( A TabRow contains a row of Tabs, and displays an indicator underneath the currently selected tab. |
fun TabRow(
selectedTabIndex: Int,
modifier: String = "Modifier",
backgroundColor: String = "MaterialTheme.colors.primarySurface",
contentColor: String = "contentColorFor(backgroundColor)",
indicator: (List<String>) -> Unit = { tabPositions -> listOf( listOf(tabPositions[selectedTabIndex]) ) },
divider: () -> Unit = { listOf<Unit>() },
tabs: () -> Unit
): Unit
A TabRow contains a row of Tabs, and displays an indicator underneath the currently selected tab. A TabRow places its tabs evenly spaced along the entire row, with each tab taking up an equal amount of space. See ScrollableTabRow for a tab row that does not enforce equal size, and allows scrolling to tabs that do not fit on screen.
A simple example with text tabs looks like:
import androidx.compose.foundation.layout.Column import androidx.compose.material.Tab import androidx.compose.material.TabRow import androidx.compose.material.Text import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember var state by remember { mutableStateOf(0) } val titles = listOf("TAB 1", "TAB 2", "TAB 3 WITH LOTS OF TEXT") Column { TabRow(selectedTabIndex = state) { titles.forEachIndexed { index, title -> Tab( text = { Text(title) }, selected = state == index, onClick = { state = index } ) } } Text( modifier = Modifier.align(Alignment.CenterHorizontally), text = "Text tab ${state + 1} selected", style = MaterialTheme.typography.body1 ) }
You can also provide your own custom tab, such as:
import androidx.compose.foundation.layout.Column import androidx.compose.material.TabRow import androidx.compose.material.Text import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember var state by remember { mutableStateOf(0) } val titles = listOf("TAB 1", "TAB 2", "TAB 3") Column { TabRow(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.body1 ) }
Where the custom tab itself could look like:
import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size import androidx.compose.material.Tab import androidx.compose.material.Text 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) Color.Red else Color.White) ) Text( text = title, style = MaterialTheme.typography.body1, modifier = Modifier.align(Alignment.CenterHorizontally) ) } }
As well as customizing the tab, you can also provide a custom indicator
, to customize the indicator displayed for a tab. indicator
will be placed to fill the entire TabRow, so it should internally take care of sizing and positioning the indicator to match changes to selectedTabIndex
.
For example, given an indicator that draws a rounded rectangle near the edges of the Tab:
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape // 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)) )
We can reuse TabRowDefaults.tabIndicatorOffset and just provide this indicator, as we aren't changing how the size and position of the indicator changes between tabs:
import androidx.compose.foundation.layout.Column import androidx.compose.material.Tab import androidx.compose.material.TabRowDefaults.tabIndicatorOffset import androidx.compose.material.TabRow import androidx.compose.material.Text import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember var state by remember { mutableStateOf(0) } val titles = listOf("TAB 1", "TAB 2", "TAB 3") // Reuse the default offset animation modifier, but use our own indicator val indicator = @Composable { tabPositions: List<TabPosition> -> FancyIndicator(Color.White, Modifier.tabIndicatorOffset(tabPositions[state])) } Column { TabRow( selectedTabIndex = state, indicator = indicator ) { titles.forEachIndexed { index, title -> Tab( text = { Text(title) }, selected = state == index, onClick = { state = index } ) } } Text( modifier = Modifier.align(Alignment.CenterHorizontally), text = "Fancy indicator tab ${state + 1} selected", style = MaterialTheme.typography.body1 ) }
You may also want to use a custom transition, to allow you to dynamically change the appearance of the indicator as it animates between tabs, such as changing its color or size. indicator
is stacked on top of the entire TabRow, so you just need to provide a custom transition that animates the offset of the indicator from the start of the TabRow. For example, take the following example that uses a transition to animate the offset, width, and color of the same FancyIndicator from before, also adding a physics based 'spring' effect to the indicator in the direction of motion:
import androidx.compose.animation.animateColor import androidx.compose.animation.core.animateDp import androidx.compose.animation.core.spring import androidx.compose.animation.core.updateTransition import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize val colors = listOf(Color.Yellow, Color.Red, Color.Green) val transition = updateTransition(selectedTabIndex) val indicatorStart by transition.animateDp( transitionSpec = { // 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 (initialState < targetState) { spring(dampingRatio = 1f, stiffness = 50f) } else { spring(dampingRatio = 1f, stiffness = 1000f) } } ) { tabPositions[it].left } val indicatorEnd by transition.animateDp( transitionSpec = { // 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 (initialState < targetState) { spring(dampingRatio = 1f, stiffness = 1000f) } else { spring(dampingRatio = 1f, stiffness = 50f) } } ) { tabPositions[it].right } val indicatorColor by transition.animateColor { colors[it % colors.size] } FancyIndicator( // Pass the current color to the indicator indicatorColor, modifier = Modifier // Fill up the entire TabRow, and place the indicator at the start .fillMaxSize() .wrapContentSize(align = Alignment.BottomStart) // Apply an offset from the start to correctly position the indicator around the tab .offset(x = indicatorStart) // Make the width of the indicator follow the animated width as we move between tabs .width(indicatorEnd - indicatorStart) )
We can now just pass this indicator directly to TabRow:
import androidx.compose.foundation.layout.Column import androidx.compose.material.Tab import androidx.compose.material.TabRow import androidx.compose.material.Text import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember var state by remember { mutableStateOf(0) } val titles = listOf("TAB 1", "TAB 2", "TAB 3") val indicator = @Composable { tabPositions: List<TabPosition> -> FancyAnimatedIndicator(tabPositions = tabPositions, selectedTabIndex = state) } Column { TabRow( selectedTabIndex = state, indicator = indicator ) { titles.forEachIndexed { index, title -> Tab( text = { Text(title) }, selected = state == index, onClick = { state = index } ) } } Text( modifier = Modifier.align(Alignment.CenterHorizontally), text = "Fancy transition tab ${state + 1} selected", style = MaterialTheme.typography.body1 ) }
Parameters | |
---|---|
selectedTabIndex: Int |
the index of the currently selected tab |
modifier: String = "Modifier" |
optional Modifier for this TabRow |
backgroundColor: String = "MaterialTheme.colors.primarySurface" |
The background color for the TabRow. Use Color.Transparent to have no color. |
contentColor: String = "contentColorFor(backgroundColor)" |
The preferred content color provided by this TabRow to its children. Defaults to either the matching content color for |
indicator: (List<String>) -> Unit = { tabPositions ->
listOf(
listOf(tabPositions[selectedTabIndex])
)
} |
the indicator that represents which tab is currently selected. By default this will be a TabRowDefaults.Indicator, using a TabRowDefaults.tabIndicatorOffset modifier to animate its position. Note that this indicator will be forced to fill up the entire TabRow, so you should use TabRowDefaults.tabIndicatorOffset or similar to animate the actual drawn indicator inside this space, and provide an offset from the start. |
divider: () -> Unit = {
listOf<Unit>()
} |
the divider displayed at the bottom of the TabRow. This provides a layer of separation between the TabRow and the content displayed underneath. |
tabs: () -> Unit |
the tabs inside this TabRow. Typically this will be multiple Tabs. Each element inside this lambda will be measured and placed evenly across the TabRow, each taking up equal space. |