Merge "[Tabs] Add primary/secondary indicators" into androidx-main
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index 1f5b379..b6e6d45 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -758,16 +758,20 @@
}
@androidx.compose.runtime.Immutable public final class TabPosition {
+ method public float getContentWidth();
method public float getLeft();
method public float getRight();
method public float getWidth();
+ property public final float contentWidth;
property public final float left;
property public final float right;
property public final float width;
}
public final class TabRowDefaults {
- method @androidx.compose.runtime.Composable public void Indicator(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
+ method @Deprecated @androidx.compose.runtime.Composable public void Indicator(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
+ method @androidx.compose.runtime.Composable public void PrimaryIndicator(optional androidx.compose.ui.Modifier modifier, optional float width, optional float height, optional long color, optional androidx.compose.ui.graphics.Shape shape);
+ method @androidx.compose.runtime.Composable public void SecondaryIndicator(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
method @androidx.compose.runtime.Composable public long getContainerColor();
method @androidx.compose.runtime.Composable public long getContentColor();
method public androidx.compose.ui.Modifier tabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.material3.TabPosition currentTabPosition);
diff --git a/compose/material3/material3/api/public_plus_experimental_current.txt b/compose/material3/material3/api/public_plus_experimental_current.txt
index b2471cb..a2231ee 100644
--- a/compose/material3/material3/api/public_plus_experimental_current.txt
+++ b/compose/material3/material3/api/public_plus_experimental_current.txt
@@ -1145,16 +1145,20 @@
}
@androidx.compose.runtime.Immutable public final class TabPosition {
+ method public float getContentWidth();
method public float getLeft();
method public float getRight();
method public float getWidth();
+ property public final float contentWidth;
property public final float left;
property public final float right;
property public final float width;
}
public final class TabRowDefaults {
- method @androidx.compose.runtime.Composable public void Indicator(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
+ method @Deprecated @androidx.compose.runtime.Composable public void Indicator(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
+ method @androidx.compose.runtime.Composable public void PrimaryIndicator(optional androidx.compose.ui.Modifier modifier, optional float width, optional float height, optional long color, optional androidx.compose.ui.graphics.Shape shape);
+ method @androidx.compose.runtime.Composable public void SecondaryIndicator(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
method @androidx.compose.runtime.Composable public long getContainerColor();
method @androidx.compose.runtime.Composable public long getContentColor();
method public androidx.compose.ui.Modifier tabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.material3.TabPosition currentTabPosition);
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index 1f5b379..b6e6d45 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -758,16 +758,20 @@
}
@androidx.compose.runtime.Immutable public final class TabPosition {
+ method public float getContentWidth();
method public float getLeft();
method public float getRight();
method public float getWidth();
+ property public final float contentWidth;
property public final float left;
property public final float right;
property public final float width;
}
public final class TabRowDefaults {
- method @androidx.compose.runtime.Composable public void Indicator(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
+ method @Deprecated @androidx.compose.runtime.Composable public void Indicator(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
+ method @androidx.compose.runtime.Composable public void PrimaryIndicator(optional androidx.compose.ui.Modifier modifier, optional float width, optional float height, optional long color, optional androidx.compose.ui.graphics.Shape shape);
+ method @androidx.compose.runtime.Composable public void SecondaryIndicator(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
method @androidx.compose.runtime.Composable public long getContainerColor();
method @androidx.compose.runtime.Composable public long getContentColor();
method public androidx.compose.ui.Modifier tabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.material3.TabPosition currentTabPosition);
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
index 1d6480b..aef2f9c 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
@@ -101,6 +101,7 @@
import androidx.compose.material3.samples.PinnedTopAppBar
import androidx.compose.material3.samples.PlainTooltipSample
import androidx.compose.material3.samples.PlainTooltipWithManualInvocationSample
+import androidx.compose.material3.samples.PrimaryTabs
import androidx.compose.material3.samples.RadioButtonSample
import androidx.compose.material3.samples.RadioGroupSample
import androidx.compose.material3.samples.RangeSliderSample
@@ -113,8 +114,11 @@
import androidx.compose.material3.samples.ScaffoldWithMultilineSnackbar
import androidx.compose.material3.samples.ScaffoldWithSimpleSnackbar
import androidx.compose.material3.samples.ScrollingFancyIndicatorContainerTabs
+import androidx.compose.material3.samples.ScrollingPrimaryTabs
+import androidx.compose.material3.samples.ScrollingSecondaryTabs
import androidx.compose.material3.samples.ScrollingTextTabs
import androidx.compose.material3.samples.SearchBarSample
+import androidx.compose.material3.samples.SecondaryTabs
import androidx.compose.material3.samples.SimpleBottomAppBar
import androidx.compose.material3.samples.SimpleBottomSheetScaffoldSample
import androidx.compose.material3.samples.SimpleCenterAlignedTopAppBar
@@ -894,6 +898,20 @@
private const val TabsExampleSourceUrl = "$SampleSourceUrl/TabSamples.kt"
val TabsExamples = listOf(
Example(
+ name = ::PrimaryTabs.name,
+ description = TabsExampleDescription,
+ sourceUrl = TabsExampleSourceUrl
+ ) {
+ PrimaryTabs()
+ },
+ Example(
+ name = ::SecondaryTabs.name,
+ description = TabsExampleDescription,
+ sourceUrl = TabsExampleSourceUrl
+ ) {
+ SecondaryTabs()
+ },
+ Example(
name = ::TextTabs.name,
description = TabsExampleDescription,
sourceUrl = TabsExampleSourceUrl
@@ -922,6 +940,20 @@
LeadingIconTabs()
},
Example(
+ name = ::ScrollingPrimaryTabs.name,
+ description = TabsExampleDescription,
+ sourceUrl = TabsExampleSourceUrl
+ ) {
+ ScrollingPrimaryTabs()
+ },
+ Example(
+ name = ::ScrollingSecondaryTabs.name,
+ description = TabsExampleDescription,
+ sourceUrl = TabsExampleSourceUrl
+ ) {
+ ScrollingSecondaryTabs()
+ },
+ Example(
name = ::ScrollingTextTabs.name,
description = TabsExampleDescription,
sourceUrl = TabsExampleSourceUrl
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TabSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TabSamples.kt
index bbeb8b3..c19a692 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TabSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TabSamples.kt
@@ -19,6 +19,7 @@
import androidx.annotation.Sampled
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.animateDp
+import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.BorderStroke
@@ -29,24 +30,25 @@
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material3.Icon
+import androidx.compose.material3.LeadingIconTab
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.Tab
-import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material3.TabPosition
import androidx.compose.material3.TabRow
-import androidx.compose.material3.LeadingIconTab
+import androidx.compose.material3.TabRowDefaults
+import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material3.Text
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Favorite
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -59,6 +61,58 @@
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+@Composable
+fun PrimaryTabs() {
+ var state by remember { mutableStateOf(0) }
+ val titles = listOf("Tab 1", "Tab 2", "Tab 3 with lots of text")
+ Column {
+ TabRow(selectedTabIndex = state, indicator = @Composable { tabPositions ->
+ if (state < tabPositions.size) {
+ val width by animateDpAsState(targetValue = tabPositions[state].contentWidth)
+ TabRowDefaults.PrimaryIndicator(
+ modifier = Modifier.tabIndicatorOffset(tabPositions[state]),
+ width = width
+ )
+ }
+ }) {
+ 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
+ )
+ }
+}
+
+@Composable
+fun SecondaryTabs() {
+ 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(
+ selected = state == index,
+ onClick = { state = index },
+ text = { Text(text = title, maxLines = 2, overflow = TextOverflow.Ellipsis) }
+ )
+ }
+ }
+ Text(
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ text = "Secondary tab ${state + 1} selected",
+ style = MaterialTheme.typography.bodyLarge
+ )
+ }
+}
+
@Preview
@Sampled
@Composable
@@ -160,6 +214,80 @@
}
@Composable
+fun ScrollingPrimaryTabs() {
+ var state by remember { mutableStateOf(0) }
+ val titles = listOf(
+ "Tab 1",
+ "Tab 2",
+ "Tab 3 with lots of text",
+ "Tab 4",
+ "Tab 5",
+ "Tab 6 with lots of text",
+ "Tab 7",
+ "Tab 8",
+ "Tab 9 with lots of text",
+ "Tab 10"
+ )
+ Column {
+ ScrollableTabRow(selectedTabIndex = state, indicator = @Composable { tabPositions ->
+ if (state < tabPositions.size) {
+ val width by animateDpAsState(targetValue = tabPositions[state].contentWidth)
+ TabRowDefaults.PrimaryIndicator(
+ modifier = Modifier.tabIndicatorOffset(tabPositions[state]),
+ width = width
+ )
+ }
+ }) {
+ titles.forEachIndexed { index, title ->
+ Tab(
+ selected = state == index,
+ onClick = { state = index },
+ text = { Text(title) }
+ )
+ }
+ }
+ Text(
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ text = "Scrolling primary tab ${state + 1} selected",
+ style = MaterialTheme.typography.bodyLarge
+ )
+ }
+}
+
+@Composable
+fun ScrollingSecondaryTabs() {
+ var state by remember { mutableStateOf(0) }
+ val titles = listOf(
+ "Tab 1",
+ "Tab 2",
+ "Tab 3 with lots of text",
+ "Tab 4",
+ "Tab 5",
+ "Tab 6 with lots of text",
+ "Tab 7",
+ "Tab 8",
+ "Tab 9 with lots of text",
+ "Tab 10"
+ )
+ Column {
+ ScrollableTabRow(selectedTabIndex = state) {
+ titles.forEachIndexed { index, title ->
+ Tab(
+ selected = state == index,
+ onClick = { state = index },
+ text = { Text(title) }
+ )
+ }
+ }
+ Text(
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ text = "Scrolling secondary tab ${state + 1} selected",
+ style = MaterialTheme.typography.bodyLarge
+ )
+ }
+}
+
+@Composable
fun ScrollingTextTabs() {
var state by remember { mutableStateOf(0) }
val titles = listOf(
@@ -201,7 +329,11 @@
Column {
TabRow(selectedTabIndex = state) {
titles.forEachIndexed { index, title ->
- FancyTab(title = title, onClick = { state = index }, selected = (index == state))
+ FancyTab(
+ title = title,
+ onClick = { state = index },
+ selected = (index == state)
+ )
}
}
Text(
@@ -334,7 +466,8 @@
.align(Alignment.CenterHorizontally)
.background(
color = if (selected) MaterialTheme.colorScheme.primary
- else MaterialTheme.colorScheme.background)
+ else MaterialTheme.colorScheme.background
+ )
)
Text(
text = title,
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TabScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TabScreenshotTest.kt
index cced64f..318b5c0 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TabScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TabScreenshotTest.kt
@@ -59,7 +59,7 @@
val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL3)
@Test
- fun lightTheme() {
+ fun lightTheme_primary() {
val interactionSource = MutableInteractionSource()
var scope: CoroutineScope? = null
@@ -67,7 +67,7 @@
composeTestRule.setContent {
scope = rememberCoroutineScope()
MaterialTheme(lightColorScheme()) {
- DefaultTabs(interactionSource)
+ DefaultPrimaryTabs(interactionSource)
}
}
@@ -75,12 +75,12 @@
scope = scope!!,
interactionSource = interactionSource,
interaction = null,
- goldenIdentifier = "tabs_lightTheme"
+ goldenIdentifier = "tabs_lightTheme_primary"
)
}
@Test
- fun lightTheme_pressed() {
+ fun lightTheme_secondary() {
val interactionSource = MutableInteractionSource()
var scope: CoroutineScope? = null
@@ -88,7 +88,28 @@
composeTestRule.setContent {
scope = rememberCoroutineScope()
MaterialTheme(lightColorScheme()) {
- DefaultTabs(interactionSource)
+ DefaultSecondaryTabs(interactionSource)
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = null,
+ goldenIdentifier = "tabs_lightTheme_secondary"
+ )
+ }
+
+ @Test
+ fun lightTheme_primary_pressed() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(lightColorScheme()) {
+ DefaultPrimaryTabs(interactionSource)
}
}
@@ -96,54 +117,12 @@
scope = scope!!,
interactionSource = interactionSource,
interaction = PressInteraction.Press(Offset(10f, 10f)),
- goldenIdentifier = "tabs_lightTheme_pressed"
+ goldenIdentifier = "tabs_lightTheme_primary_pressed"
)
}
@Test
- fun darkTheme() {
- val interactionSource = MutableInteractionSource()
-
- var scope: CoroutineScope? = null
-
- composeTestRule.setContent {
- scope = rememberCoroutineScope()
- MaterialTheme(darkColorScheme()) {
- DefaultTabs(interactionSource)
- }
- }
-
- assertTabsMatch(
- scope = scope!!,
- interactionSource = interactionSource,
- interaction = null,
- goldenIdentifier = "tabs_darkTheme"
- )
- }
-
- @Test
- fun darkTheme_pressed() {
- val interactionSource = MutableInteractionSource()
-
- var scope: CoroutineScope? = null
-
- composeTestRule.setContent {
- scope = rememberCoroutineScope()
- MaterialTheme(darkColorScheme()) {
- DefaultTabs(interactionSource)
- }
- }
-
- assertTabsMatch(
- scope = scope!!,
- interactionSource = interactionSource,
- interaction = PressInteraction.Press(Offset(10f, 10f)),
- goldenIdentifier = "tabs_darkTheme_pressed"
- )
- }
-
- @Test
- fun leadingIconTabs_lightTheme() {
+ fun lightTheme_secondary_pressed() {
val interactionSource = MutableInteractionSource()
var scope: CoroutineScope? = null
@@ -151,20 +130,20 @@
composeTestRule.setContent {
scope = rememberCoroutineScope()
MaterialTheme(lightColorScheme()) {
- DefaultLeadingIconTabs(interactionSource)
+ DefaultSecondaryTabs(interactionSource)
}
}
assertTabsMatch(
scope = scope!!,
interactionSource = interactionSource,
- interaction = null,
- goldenIdentifier = "leadingIconTabs_lightTheme"
+ interaction = PressInteraction.Press(Offset(10f, 10f)),
+ goldenIdentifier = "tabs_lightTheme_secondary_pressed"
)
}
@Test
- fun leadingIconTabs_darkTheme() {
+ fun darkTheme_primary() {
val interactionSource = MutableInteractionSource()
var scope: CoroutineScope? = null
@@ -172,7 +151,7 @@
composeTestRule.setContent {
scope = rememberCoroutineScope()
MaterialTheme(darkColorScheme()) {
- DefaultLeadingIconTabs(interactionSource)
+ DefaultPrimaryTabs(interactionSource)
}
}
@@ -180,7 +159,342 @@
scope = scope!!,
interactionSource = interactionSource,
interaction = null,
- goldenIdentifier = "leadingIconTabs_darkTheme"
+ goldenIdentifier = "tabs_darkTheme_primary"
+ )
+ }
+
+ @Test
+ fun darkTheme_secondary() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(darkColorScheme()) {
+ DefaultSecondaryTabs(interactionSource)
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = null,
+ goldenIdentifier = "tabs_darkTheme_secondary"
+ )
+ }
+
+ @Test
+ fun darkTheme_primary_pressed() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(darkColorScheme()) {
+ DefaultPrimaryTabs(interactionSource)
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = PressInteraction.Press(Offset(10f, 10f)),
+ goldenIdentifier = "tabs_darkTheme_primary_pressed"
+ )
+ }
+
+ @Test
+ fun darkTheme_secondary_pressed() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(darkColorScheme()) {
+ DefaultSecondaryTabs(interactionSource)
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = PressInteraction.Press(Offset(10f, 10f)),
+ goldenIdentifier = "tabs_darkTheme_secondary_pressed"
+ )
+ }
+
+ @Test
+ fun customTabs_lightTheme_primary() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(lightColorScheme()) {
+ CustomPrimaryTabs(
+ interactionSource,
+ containerColor = MaterialTheme.colorScheme.tertiaryContainer,
+ selectedContentColor = MaterialTheme.colorScheme.onTertiary,
+ unselectedContentColor = Color.Black
+ )
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = null,
+ goldenIdentifier = "customTabs_lightTheme_primary"
+ )
+ }
+
+ @Test
+ fun customTabs_lightTheme_secondary() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(lightColorScheme()) {
+ CustomSecondaryTabs(
+ interactionSource,
+ containerColor = MaterialTheme.colorScheme.tertiaryContainer,
+ selectedContentColor = MaterialTheme.colorScheme.onTertiary,
+ unselectedContentColor = Color.Black
+ )
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = null,
+ goldenIdentifier = "customTabs_lightTheme_secondary"
+ )
+ }
+
+ @Test
+ fun customTabs_darkTheme_primary() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(darkColorScheme()) {
+ CustomPrimaryTabs(
+ interactionSource,
+ containerColor = MaterialTheme.colorScheme.tertiaryContainer,
+ selectedContentColor = MaterialTheme.colorScheme.onTertiary,
+ unselectedContentColor = Color.Black
+ )
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = null,
+ goldenIdentifier = "customTabs_darkTheme_primary"
+ )
+ }
+
+ @Test
+ fun customTabs_darkTheme_secondary() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(darkColorScheme()) {
+ CustomSecondaryTabs(
+ interactionSource,
+ containerColor = MaterialTheme.colorScheme.tertiaryContainer,
+ selectedContentColor = MaterialTheme.colorScheme.onTertiary,
+ unselectedContentColor = Color.Black
+ )
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = null,
+ goldenIdentifier = "customTabs_darkTheme_secondary"
+ )
+ }
+
+ @Test
+ fun leadingIconTabs_lightTheme_primary() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(lightColorScheme()) {
+ DefaultPrimaryLeadingIconTabs(interactionSource)
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = null,
+ goldenIdentifier = "leadingIconTabs_lightTheme_primary"
+ )
+ }
+
+ @Test
+ fun leadingIconTabs_lightTheme_secondary() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(lightColorScheme()) {
+ DefaultSecondaryLeadingIconTabs(interactionSource)
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = null,
+ goldenIdentifier = "leadingIconTabs_lightTheme_secondary"
+ )
+ }
+
+ @Test
+ fun leadingIconTabs_darkTheme_primary() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(darkColorScheme()) {
+ DefaultPrimaryLeadingIconTabs(interactionSource)
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = null,
+ goldenIdentifier = "leadingIconTabs_darkTheme_primary"
+ )
+ }
+
+ @Test
+ fun leadingIconTabs_darkTheme_secondary() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(darkColorScheme()) {
+ DefaultSecondaryLeadingIconTabs(interactionSource)
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = null,
+ goldenIdentifier = "leadingIconTabs_darkTheme_secondary"
+ )
+ }
+
+ @Test
+ fun lightTheme_primary_scrollable() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(lightColorScheme()) {
+ DefaultPrimaryScrollableTabs(interactionSource)
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = null,
+ goldenIdentifier = "tabs_lightTheme_primary_scrollable"
+ )
+ }
+
+ @Test
+ fun lightTheme_secondary_scrollable() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(lightColorScheme()) {
+ DefaultSecondaryScrollableTabs(interactionSource)
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = null,
+ goldenIdentifier = "tabs_lightTheme_secondary_scrollable"
+ )
+ }
+
+ @Test
+ fun darkTheme_primary_scrollable() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(darkColorScheme()) {
+ DefaultPrimaryScrollableTabs(interactionSource)
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = null,
+ goldenIdentifier = "tabs_darkTheme_primary_scrollable"
+ )
+ }
+
+ @Test
+ fun darkTheme_secondary_scrollable() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(darkColorScheme()) {
+ DefaultSecondaryScrollableTabs(interactionSource)
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = null,
+ goldenIdentifier = "tabs_darkTheme_secondary_scrollable"
)
}
@@ -213,23 +527,66 @@
}
// Capture and compare screenshots
- composeTestRule.onNodeWithTag(Tag)
+ composeTestRule.onNodeWithTag(TAG)
.captureToImage()
.assertAgainstGolden(screenshotRule, goldenIdentifier)
}
}
/**
- * Default colored [TabRow] with three [Tab]s. The first [Tab] is selected, and the rest are not.
+ * Default primary colored [TabRow] with three [Tab]s. The first [Tab] is selected, and the rest are not.
*
* @param interactionSource the [MutableInteractionSource] for the first [Tab], to control its
* visual state.
*/
@Composable
-private fun DefaultTabs(
+private fun DefaultPrimaryTabs(
interactionSource: MutableInteractionSource
) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(Tag)) {
+ Box(
+ Modifier
+ .semantics(mergeDescendants = true) {}
+ .testTag(TAG)) {
+ TabRow(selectedTabIndex = 0, indicator = @Composable { tabPositions ->
+ TabRowDefaults.PrimaryIndicator(
+ modifier = Modifier.tabIndicatorOffset(tabPositions[0]),
+ width = tabPositions[0].contentWidth
+ )
+ }) {
+ Tab(
+ selected = true,
+ onClick = {},
+ text = { Text("TAB") },
+ interactionSource = interactionSource
+ )
+ Tab(
+ selected = false,
+ onClick = {},
+ text = { Text("TAB") }
+ )
+ Tab(
+ selected = false,
+ onClick = {},
+ text = { Text("TAB") }
+ )
+ }
+ }
+}
+
+/**
+ * Default secondary colored [TabRow] with three [Tab]s. The first [Tab] is selected, and the rest are not.
+ *
+ * @param interactionSource the [MutableInteractionSource] for the first [Tab], to control its
+ * visual state.
+ */
+@Composable
+private fun DefaultSecondaryTabs(
+ interactionSource: MutableInteractionSource
+) {
+ Box(
+ Modifier
+ .semantics(mergeDescendants = true) {}
+ .testTag(TAG)) {
TabRow(selectedTabIndex = 0) {
Tab(
selected = true,
@@ -252,7 +609,7 @@
}
/**
- * Custom colored [TabRow] with three [Tab]s. The first [Tab] is selected, and the rest are not.
+ * Custom primary colored [TabRow] with three [Tab]s. The first [Tab] is selected, and the rest are not.
*
* @param interactionSource the [MutableInteractionSource] for the first [Tab], to control its
* visual state.
@@ -261,17 +618,75 @@
* @param unselectedContentColor the content color for an unselected [Tab] (second and third tabs)
*/
@Composable
-private fun CustomTabs(
+private fun CustomPrimaryTabs(
interactionSource: MutableInteractionSource,
containerColor: Color,
selectedContentColor: Color,
unselectedContentColor: Color
) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(Tag)) {
+ Box(
+ Modifier
+ .semantics(mergeDescendants = true) {}
+ .testTag(TAG)) {
TabRow(selectedTabIndex = 0,
containerColor = containerColor,
indicator = @Composable { tabPositions ->
- TabRowDefaults.Indicator(
+ TabRowDefaults.PrimaryIndicator(
+ modifier = Modifier.tabIndicatorOffset(tabPositions[0]),
+ width = tabPositions[0].contentWidth,
+ color = selectedContentColor
+ )
+ }) {
+ Tab(
+ selected = true,
+ onClick = {},
+ text = { Text("TAB") },
+ selectedContentColor = selectedContentColor,
+ unselectedContentColor = unselectedContentColor,
+ interactionSource = interactionSource
+ )
+ Tab(
+ selected = false,
+ onClick = {},
+ text = { Text("TAB") },
+ selectedContentColor = selectedContentColor,
+ unselectedContentColor = unselectedContentColor
+ )
+ Tab(
+ selected = false,
+ onClick = {},
+ text = { Text("TAB") },
+ selectedContentColor = selectedContentColor,
+ unselectedContentColor = unselectedContentColor
+ )
+ }
+ }
+}
+
+/**
+ * Custom secondary colored [TabRow] with three [Tab]s. The first [Tab] is selected, and the rest are not.
+ *
+ * @param interactionSource the [MutableInteractionSource] for the first [Tab], to control its
+ * visual state.
+ * @param containerColor the containerColor of the [TabRow]
+ * @param selectedContentColor the content color for a selected [Tab] (first tab)
+ * @param unselectedContentColor the content color for an unselected [Tab] (second and third tabs)
+ */
+@Composable
+private fun CustomSecondaryTabs(
+ interactionSource: MutableInteractionSource,
+ containerColor: Color,
+ selectedContentColor: Color,
+ unselectedContentColor: Color
+) {
+ Box(
+ Modifier
+ .semantics(mergeDescendants = true) {}
+ .testTag(TAG)) {
+ TabRow(selectedTabIndex = 0,
+ containerColor = containerColor,
+ indicator = @Composable { tabPositions ->
+ TabRowDefaults.SecondaryIndicator(
modifier = Modifier.tabIndicatorOffset(tabPositions[0]),
color = selectedContentColor
)
@@ -303,17 +718,64 @@
}
/**
- * Default colored [TabRow] with three [LeadingIconTab]s. The first [LeadingIconTab] is selected,
+ * Default primary colored [TabRow] with three [LeadingIconTab]s. The first [LeadingIconTab] is selected,
* and the rest are not.
*
* @param interactionSource the [MutableInteractionSource] for the first [LeadingIconTab], to control its
* visual state.
*/
@Composable
-private fun DefaultLeadingIconTabs(
+private fun DefaultPrimaryLeadingIconTabs(
interactionSource: MutableInteractionSource
) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(Tag)) {
+ Box(
+ Modifier
+ .semantics(mergeDescendants = true) {}
+ .testTag(TAG)) {
+ TabRow(selectedTabIndex = 0, indicator = @Composable { tabPositions ->
+ TabRowDefaults.PrimaryIndicator(
+ modifier = Modifier.tabIndicatorOffset(tabPositions[0]),
+ width = tabPositions[0].contentWidth
+ )
+ }) {
+ LeadingIconTab(
+ selected = true,
+ onClick = {},
+ text = { Text("TAB") },
+ icon = { Icon(Icons.Filled.Favorite, contentDescription = "Favorite") },
+ interactionSource = interactionSource
+ )
+ LeadingIconTab(
+ selected = false,
+ onClick = {},
+ text = { Text("TAB") },
+ icon = { Icon(Icons.Filled.Favorite, contentDescription = "Favorite") }
+ )
+ LeadingIconTab(
+ selected = false,
+ onClick = {},
+ text = { Text("TAB") },
+ icon = { Icon(Icons.Filled.Favorite, contentDescription = "Favorite") }
+ )
+ }
+ }
+}
+
+/**
+ * Default secondary colored [TabRow] with three [LeadingIconTab]s. The first [LeadingIconTab] is selected,
+ * and the rest are not.
+ *
+ * @param interactionSource the [MutableInteractionSource] for the first [LeadingIconTab], to control its
+ * visual state.
+ */
+@Composable
+private fun DefaultSecondaryLeadingIconTabs(
+ interactionSource: MutableInteractionSource
+) {
+ Box(
+ Modifier
+ .semantics(mergeDescendants = true) {}
+ .testTag(TAG)) {
TabRow(selectedTabIndex = 0) {
LeadingIconTab(
selected = true,
@@ -338,4 +800,79 @@
}
}
-private const val Tag = "Tab"
+/**
+ * Default primary colored [ScrollableTabRow] with three [Tab]s. The first [Tab] is selected, and the rest are not.
+ *
+ * @param interactionSource the [MutableInteractionSource] for the first [Tab], to control its
+ * visual state.
+ */
+@Composable
+private fun DefaultPrimaryScrollableTabs(
+ interactionSource: MutableInteractionSource
+) {
+ Box(
+ Modifier
+ .semantics(mergeDescendants = true) {}
+ .testTag(TAG)) {
+ ScrollableTabRow(selectedTabIndex = 0, indicator = @Composable { tabPositions ->
+ TabRowDefaults.PrimaryIndicator(
+ modifier = Modifier.tabIndicatorOffset(tabPositions[0]),
+ width = tabPositions[0].contentWidth
+ )
+ }) {
+ Tab(
+ selected = true,
+ onClick = {},
+ text = { Text("TAB") },
+ interactionSource = interactionSource
+ )
+ Tab(
+ selected = false,
+ onClick = {},
+ text = { Text("TAB") }
+ )
+ Tab(
+ selected = false,
+ onClick = {},
+ text = { Text("TAB") }
+ )
+ }
+ }
+}
+
+/**
+ * Default secondary colored [ScrollableTabRow] with three [Tab]s. The first [Tab] is selected, and the rest are not.
+ *
+ * @param interactionSource the [MutableInteractionSource] for the first [Tab], to control its
+ * visual state.
+ */
+@Composable
+private fun DefaultSecondaryScrollableTabs(
+ interactionSource: MutableInteractionSource
+) {
+ Box(
+ Modifier
+ .semantics(mergeDescendants = true) {}
+ .testTag(TAG)) {
+ ScrollableTabRow(selectedTabIndex = 0) {
+ Tab(
+ selected = true,
+ onClick = {},
+ text = { Text("TAB") },
+ interactionSource = interactionSource
+ )
+ Tab(
+ selected = false,
+ onClick = {},
+ text = { Text("TAB") }
+ )
+ Tab(
+ selected = false,
+ onClick = {},
+ text = { Text("TAB") }
+ )
+ }
+ }
+}
+
+private const val TAG = "Tab"
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TabTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TabTest.kt
index 4ff880a..7b9ed6e 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TabTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TabTest.kt
@@ -27,7 +27,7 @@
import androidx.compose.material3.samples.LeadingIconTabs
import androidx.compose.material3.samples.ScrollingTextTabs
import androidx.compose.material3.samples.TextTabs
-import androidx.compose.material3.tokens.PrimaryNavigationTabTokens
+import androidx.compose.material3.tokens.DividerTokens
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
@@ -326,9 +326,9 @@
rule.onNodeWithTag("divider", true)
.assertPositionInRootIsEqualTo(
expectedLeft = 0.dp,
- expectedTop = tabRowBounds.height - PrimaryNavigationTabTokens.DividerHeight
+ expectedTop = tabRowBounds.height - DividerTokens.Thickness
)
- .assertHeightIsEqualTo(PrimaryNavigationTabTokens.DividerHeight)
+ .assertHeightIsEqualTo(DividerTokens.Thickness)
}
@Test
@@ -435,7 +435,7 @@
}
@Test
- fun LeadingIconTab_textAndIconPosition() {
+ fun leadingIconTab_textAndIconPosition() {
rule.setMaterialContent(lightColorScheme()) {
Box {
TabRow(
@@ -564,9 +564,9 @@
rule.onNodeWithTag("divider", true)
.assertPositionInRootIsEqualTo(
expectedLeft = 0.dp,
- expectedTop = tabRowBounds.height - PrimaryNavigationTabTokens.DividerHeight,
+ expectedTop = tabRowBounds.height - DividerTokens.Thickness,
)
- .assertHeightIsEqualTo(PrimaryNavigationTabTokens.DividerHeight)
+ .assertHeightIsEqualTo(DividerTokens.Thickness)
}
@Test
@@ -593,8 +593,10 @@
TextTabs()
}
+ val nodes = rule.onAllNodes(isSelectable())
+
// Only the first tab should be selected
- rule.onAllNodes(isSelectable())
+ nodes
.assertCountEquals(3)
.apply {
get(0).assertIsSelected()
@@ -603,10 +605,10 @@
}
// Click the last tab
- rule.onAllNodes(isSelectable())[2].performClick()
+ nodes[2].performClick()
// Now only the last tab should be selected
- rule.onAllNodes(isSelectable())
+ nodes
.assertCountEquals(3)
.apply {
get(0).assertIsNotSelected()
@@ -747,7 +749,7 @@
val titles = listOf("TAB 1", "TAB 2", "TAB 3 WITH LOTS OF TEXT")
val indicator = @Composable { tabPositions: List<TabPosition> ->
- TabRowDefaults.Indicator(
+ TabRowDefaults.SecondaryIndicator(
Modifier
.tabIndicatorOffset(tabPositions[state])
.testTag("indicator")
@@ -791,7 +793,7 @@
@Test
fun testInspectorValue() {
- val pos = TabPosition(10.0.dp, 200.0.dp)
+ val pos = TabPosition(10.0.dp, 200.0.dp, 0.dp)
rule.setContent {
val modifier = Modifier.tabIndicatorOffset(pos) as InspectableValue
assertThat(modifier.nameFallback).isEqualTo("tabIndicatorOffset")
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tab.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tab.kt
index d726302..31ab35e 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tab.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tab.kt
@@ -315,7 +315,11 @@
) { text() }
}
if (icon != null) {
- Box(Modifier.layoutId("icon")) { icon() }
+ Box(
+ Modifier
+ .layoutId("icon")
+ .padding(horizontal = HorizontalTextPadding)
+ ) { icon() }
}
}
) { measurables, constraints ->
@@ -430,7 +434,7 @@
private const val TabFadeOutAnimationDuration = 100
// The horizontal padding on the left and right of text
-private val HorizontalTextPadding = 16.dp
+internal val HorizontalTextPadding = 16.dp
// Distance from the top of the indicator to the text baseline when there is one line of text and an
// icon
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TabRow.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TabRow.kt
index 6480116..82d1d85 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TabRow.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TabRow.kt
@@ -24,9 +24,11 @@
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.rememberScrollState
@@ -43,6 +45,8 @@
import androidx.compose.ui.composed
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.Constraints
@@ -52,8 +56,9 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
-// TODO: Provide M3 tab row asset and docs when available.
/**
+ * <a href="https://m3.material.io/components/tabs/overview" class="external" target="_blank">Material Design tabs</a>
+ *
* Material Design fixed tabs.
*
* Fixed tabs display all tabs in a set simultaneously. They are best for switching between related
@@ -113,7 +118,7 @@
* matching content color for [containerColor], or to the current [LocalContentColor] if
* [containerColor] is not a color from the theme.
* @param indicator the indicator that represents which tab is currently selected. By default this
- * will be a [TabRowDefaults.Indicator], using a [TabRowDefaults.tabIndicatorOffset] modifier to
+ * will be a [TabRowDefaults.SecondaryIndicator], using a [TabRowDefaults.tabIndicatorOffset] modifier to
* animate its position. Note that this indicator will be forced to fill up the entire tab row, so
* you should use [TabRowDefaults.tabIndicatorOffset] or similar to animate the actual drawn
* indicator inside this space, and provide an offset from the start.
@@ -130,7 +135,7 @@
contentColor: Color = TabRowDefaults.contentColor,
indicator: @Composable (tabPositions: List<TabPosition>) -> Unit = @Composable { tabPositions ->
if (selectedTabIndex < tabPositions.size) {
- TabRowDefaults.Indicator(
+ TabRowDefaults.SecondaryIndicator(
Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex])
)
}
@@ -140,6 +145,18 @@
},
tabs: @Composable () -> Unit
) {
+ TabRowImpl(modifier, containerColor, contentColor, indicator, divider, tabs)
+}
+
+@Composable
+private fun TabRowImpl(
+ modifier: Modifier,
+ containerColor: Color,
+ contentColor: Color,
+ indicator: @Composable (tabPositions: List<TabPosition>) -> Unit,
+ divider: @Composable () -> Unit,
+ tabs: @Composable () -> Unit
+) {
Surface(
modifier = modifier.selectableGroup(),
color = containerColor,
@@ -169,7 +186,10 @@
}
val tabPositions = List(tabCount) { index ->
- TabPosition(tabWidth.toDp() * index, tabWidth.toDp())
+ var contentWidth =
+ minOf(tabMeasurables[index].maxIntrinsicWidth(tabRowHeight), tabWidth).toDp()
+ contentWidth -= HorizontalTextPadding * 2
+ TabPosition(tabWidth.toDp() * index, tabWidth.toDp(), contentWidth)
}
layout(tabRowWidth, tabRowHeight) {
@@ -192,8 +212,9 @@
}
}
-// TODO: Provide M3 tab row asset and docs when available.
/**
+ * <a href="https://m3.material.io/components/tabs/overview" class="external" target="_blank">Material Design tabs</a>
+ *
* Material Design scrollable tabs.
*
* When a set of tabs cannot fit on screen, use scrollable tabs. Scrollable tabs can use longer text
@@ -215,7 +236,7 @@
* and the tabs inside the row. This padding helps inform the user that this tab row can be
* scrolled, unlike a [TabRow].
* @param indicator the indicator that represents which tab is currently selected. By default this
- * will be a [TabRowDefaults.Indicator], using a [TabRowDefaults.tabIndicatorOffset] modifier to
+ * will be a [TabRowDefaults.SecondaryIndicator], using a [TabRowDefaults.tabIndicatorOffset] modifier to
* animate its position. Note that this indicator will be forced to fill up the entire tab row, so
* you should use [TabRowDefaults.tabIndicatorOffset] or similar to animate the actual drawn
* indicator inside this space, and provide an offset from the start.
@@ -232,7 +253,7 @@
contentColor: Color = TabRowDefaults.contentColor,
edgePadding: Dp = ScrollableTabRowPadding,
indicator: @Composable (tabPositions: List<TabPosition>) -> Unit = @Composable { tabPositions ->
- TabRowDefaults.Indicator(
+ TabRowDefaults.SecondaryIndicator(
Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex])
)
},
@@ -276,8 +297,20 @@
minHeight = layoutHeight,
maxHeight = layoutHeight,
)
- val tabPlaceables = tabMeasurables
- .map { it.measure(tabConstraints) }
+
+ val tabPlaceables = mutableListOf<Placeable>()
+ val tabContentWidths = mutableListOf<Dp>()
+ tabMeasurables.forEach {
+ val placeable = it.measure(tabConstraints)
+ var contentWidth =
+ minOf(
+ it.maxIntrinsicWidth(placeable.height),
+ placeable.width
+ ).toDp()
+ contentWidth -= HorizontalTextPadding * 2
+ tabPlaceables.add(placeable)
+ tabContentWidths.add(contentWidth)
+ }
val layoutWidth = tabPlaceables.fold(initial = padding * 2) { curr, measurable ->
curr + measurable.width
@@ -288,10 +321,16 @@
// Place the tabs
val tabPositions = mutableListOf<TabPosition>()
var left = padding
- tabPlaceables.forEach {
- it.placeRelative(left, 0)
- tabPositions.add(TabPosition(left = left.toDp(), width = it.width.toDp()))
- left += it.width
+ tabPlaceables.forEachIndexed { index, placeable ->
+ placeable.placeRelative(left, 0)
+ tabPositions.add(
+ TabPosition(
+ left = left.toDp(),
+ width = placeable.width.toDp(),
+ contentWidth = tabContentWidths[index]
+ )
+ )
+ left += placeable.width
}
// The divider is measured with its own height, and width equal to the total width
@@ -333,9 +372,11 @@
* @property left the left edge's x position from the start of the [TabRow]
* @property right the right edge's x position from the start of the [TabRow]
* @property width the width of this tab
+ * @property contentWidth the content width of this tab
*/
@Immutable
-class TabPosition internal constructor(val left: Dp, val width: Dp) {
+class TabPosition internal constructor(val left: Dp, val width: Dp, val contentWidth: Dp) {
+
val right: Dp get() = left + width
override fun equals(other: Any?): Boolean {
@@ -344,6 +385,7 @@
if (left != other.left) return false
if (width != other.width) return false
+ if (contentWidth != other.contentWidth) return false
return true
}
@@ -351,11 +393,12 @@
override fun hashCode(): Int {
var result = left.hashCode()
result = 31 * result + width.hashCode()
+ result = 31 * result + contentWidth.hashCode()
return result
}
override fun toString(): String {
- return "TabPosition(left=$left, right=$right, width=$width)"
+ return "TabPosition(left=$left, right=$right, width=$width, contentWidth=$contentWidth)"
}
}
@@ -364,12 +407,14 @@
*/
object TabRowDefaults {
/** Default container color of a tab row. */
- val containerColor: Color @Composable get() =
- PrimaryNavigationTabTokens.ContainerColor.toColor()
+ val containerColor: Color
+ @Composable get() =
+ PrimaryNavigationTabTokens.ContainerColor.toColor()
/** Default content color of a tab row. */
- val contentColor: Color @Composable get() =
- PrimaryNavigationTabTokens.ActiveLabelTextColor.toColor()
+ val contentColor: Color
+ @Composable get() =
+ PrimaryNavigationTabTokens.ActiveLabelTextColor.toColor()
/**
* Default indicator, which will be positioned at the bottom of the [TabRow], on top of the
@@ -380,6 +425,12 @@
* @param color color of the indicator
*/
@Composable
+ @Deprecated(
+ message = "Use SecondaryIndicator instead.",
+ replaceWith = ReplaceWith(
+ "SecondaryIndicator(modifier, height, color)"
+ )
+ )
fun Indicator(
modifier: Modifier = Modifier,
height: Dp = PrimaryNavigationTabTokens.ActiveIndicatorHeight,
@@ -395,6 +446,54 @@
}
/**
+ * Primary indicator, which will be positioned at the bottom of the [TabRow], on top of the
+ * divider.
+ *
+ * @param modifier modifier for the indicator's layout
+ * @param width width of the indicator
+ * @param height height of the indicator
+ * @param color color of the indicator
+ * @param shape shape of the indicator
+ */
+ @Composable
+ fun PrimaryIndicator(
+ modifier: Modifier = Modifier,
+ width: Dp = 0.dp,
+ height: Dp = PrimaryNavigationTabTokens.ActiveIndicatorHeight,
+ color: Color = PrimaryNavigationTabTokens.ActiveIndicatorColor.toColor(),
+ shape: Shape = PrimaryNavigationTabTokens.ActiveIndicatorShape
+ ) {
+ Spacer(
+ modifier
+ .requiredSize(width, height)
+ .background(color = color, shape = shape)
+ )
+ }
+
+ /**
+ * Secondary indicator, which will be positioned at the bottom of the [TabRow], on top of the
+ * divider.
+ *
+ * @param modifier modifier for the indicator's layout
+ * @param height height of the indicator
+ * @param color color of the indicator
+ */
+ @Composable
+ fun SecondaryIndicator(
+ modifier: Modifier = Modifier,
+ height: Dp = PrimaryNavigationTabTokens.ActiveIndicatorHeight,
+ color: Color =
+ MaterialTheme.colorScheme.fromToken(PrimaryNavigationTabTokens.ActiveIndicatorColor)
+ ) {
+ Box(
+ modifier
+ .fillMaxWidth()
+ .height(height)
+ .background(color = color)
+ )
+ }
+
+ /**
* [Modifier] that takes up all the available width inside the [TabRow], and then animates
* the offset of the indicator it is applied to, depending on the [currentTabPosition].
*
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/PrimaryNavigationTabTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/PrimaryNavigationTabTokens.kt
index 3fd0e91..4622e75 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/PrimaryNavigationTabTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/PrimaryNavigationTabTokens.kt
@@ -13,7 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-// VERSION: v0_103
+
+// VERSION: v0_162
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.compose.material3.tokens
@@ -29,8 +30,6 @@
val ContainerElevation = ElevationTokens.Level0
val ContainerHeight = 48.0.dp
val ContainerShape = ShapeKeyTokens.CornerNone
- val DividerColor = ColorSchemeKeyTokens.SurfaceVariant
- val DividerHeight = 1.0.dp
val ActiveFocusIconColor = ColorSchemeKeyTokens.Primary
val ActiveHoverIconColor = ColorSchemeKeyTokens.Primary
val ActiveIconColor = ColorSchemeKeyTokens.Primary
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SecondaryNavigationTabTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SecondaryNavigationTabTokens.kt
new file mode 100644
index 0000000..6d34a48
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SecondaryNavigationTabTokens.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// VERSION: v0_162
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.compose.material3.tokens
+
+import androidx.compose.ui.unit.dp
+
+internal object SecondaryNavigationTabTokens {
+ val ActiveLabelTextColor = ColorSchemeKeyTokens.OnSurface
+ val ContainerColor = ColorSchemeKeyTokens.Surface
+ val ContainerElevation = ElevationTokens.Level0
+ val ContainerHeight = 48.0.dp
+ val ContainerShape = ShapeKeyTokens.CornerNone
+ val DividerColor = ColorSchemeKeyTokens.SurfaceVariant
+ val DividerHeight = 1.0.dp
+ val FocusLabelTextColor = ColorSchemeKeyTokens.OnSurface
+ val HoverLabelTextColor = ColorSchemeKeyTokens.OnSurface
+ val InactiveLabelTextColor = ColorSchemeKeyTokens.OnSurfaceVariant
+ val LabelTextFont = TypographyKeyTokens.TitleSmall
+ val PressedLabelTextColor = ColorSchemeKeyTokens.OnSurface
+ val ActiveIconColor = ColorSchemeKeyTokens.OnSurface
+ val FocusIconColor = ColorSchemeKeyTokens.OnSurface
+ val HoverIconColor = ColorSchemeKeyTokens.OnSurface
+ val IconSize = 24.0.dp
+ val InactiveIconColor = ColorSchemeKeyTokens.OnSurfaceVariant
+ val PressedIconColor = ColorSchemeKeyTokens.OnSurface
+}
\ No newline at end of file