feat: add customization of tab colors in different states
* Add content color customization for tab for different state combinations
* Add @Experimental annotation to TabRow and Tab
Test: Updated failing Instrumentation tests
Relnote: "Add tabContentColor customization for different state
combination and mark TabRow and Tab as @Experimental"
Change-Id: I95ee854cada5c232032d38585f83cb4a8aa19a25
diff --git a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/TopNavigation.kt b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/TopNavigation.kt
index 1b34fd8..44d7542 100644
--- a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/TopNavigation.kt
+++ b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/TopNavigation.kt
@@ -29,6 +29,7 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.LocalContentColor
import androidx.tv.material3.Tab
import androidx.tv.material3.TabRow
@@ -66,6 +67,7 @@
/**
* Pill indicator tab row for reference
*/
+@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun PillIndicatorTabRow(
tabs: List<String>,
diff --git a/tv/samples/src/main/java/androidx/tv/samples/TabRowSamples.kt b/tv/samples/src/main/java/androidx/tv/samples/TabRowSamples.kt
index b2c506e..126c1e8 100644
--- a/tv/samples/src/main/java/androidx/tv/samples/TabRowSamples.kt
+++ b/tv/samples/src/main/java/androidx/tv/samples/TabRowSamples.kt
@@ -34,6 +34,7 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.LocalContentColor
import androidx.tv.material3.Tab
import androidx.tv.material3.TabDefaults
@@ -45,6 +46,7 @@
/**
* Tab row with a Pill indicator
*/
+@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
@Sampled
fun PillIndicatorTabRow() {
@@ -74,6 +76,7 @@
/**
* Tab row with an Underlined indicator
*/
+@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
@Sampled
fun UnderlinedIndicatorTabRow() {
@@ -109,6 +112,7 @@
/**
* Tab row with delay between tab changes
*/
+@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
@Sampled
fun TabRowWithDebounce() {
@@ -147,6 +151,7 @@
/**
* Tab changes onClick instead of onFocus
*/
+@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
@Sampled
fun OnClickNavigation() {
diff --git a/tv/tv-material/api/current.txt b/tv/tv-material/api/current.txt
index 0b46921..8151caa 100644
--- a/tv/tv-material/api/current.txt
+++ b/tv/tv-material/api/current.txt
@@ -65,31 +65,10 @@
public final class ShapesKt {
}
- public final class TabColors {
- }
-
- public final class TabDefaults {
- method @androidx.compose.runtime.Composable public androidx.tv.material3.TabColors pillIndicatorTabColors(optional long activeContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long disabledActiveContentColor, optional long disabledSelectedContentColor);
- method @androidx.compose.runtime.Composable public androidx.tv.material3.TabColors underlinedIndicatorTabColors(optional long activeContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long disabledActiveContentColor, optional long disabledSelectedContentColor);
- field public static final androidx.tv.material3.TabDefaults INSTANCE;
- }
-
public final class TabKt {
- method @androidx.compose.runtime.Composable public static void Tab(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onFocus, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional boolean enabled, optional androidx.tv.material3.TabColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
- }
-
- public final class TabRowDefaults {
- method @androidx.compose.runtime.Composable public void PillIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
- method @androidx.compose.runtime.Composable public void TabSeparator();
- method @androidx.compose.runtime.Composable public void UnderlinedIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
- method @androidx.compose.runtime.Composable public long contentColor();
- method public long getContainerColor();
- property public final long ContainerColor;
- field public static final androidx.tv.material3.TabRowDefaults INSTANCE;
}
public final class TabRowKt {
- method @androidx.compose.runtime.Composable public static void TabRow(int selectedTabIndex, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> separator, optional kotlin.jvm.functions.Function1<? super java.util.List<androidx.compose.ui.unit.DpRect>,kotlin.Unit> indicator, kotlin.jvm.functions.Function0<kotlin.Unit> tabs);
}
public final class TextKt {
diff --git a/tv/tv-material/api/public_plus_experimental_current.txt b/tv/tv-material/api/public_plus_experimental_current.txt
index 475b193..69a09ad 100644
--- a/tv/tv-material/api/public_plus_experimental_current.txt
+++ b/tv/tv-material/api/public_plus_experimental_current.txt
@@ -188,20 +188,20 @@
public final class ShapesKt {
}
- public final class TabColors {
+ @androidx.tv.material3.ExperimentalTvMaterial3Api public final class TabColors {
}
- public final class TabDefaults {
- method @androidx.compose.runtime.Composable public androidx.tv.material3.TabColors pillIndicatorTabColors(optional long activeContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long disabledActiveContentColor, optional long disabledSelectedContentColor);
- method @androidx.compose.runtime.Composable public androidx.tv.material3.TabColors underlinedIndicatorTabColors(optional long activeContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long disabledActiveContentColor, optional long disabledSelectedContentColor);
+ @androidx.tv.material3.ExperimentalTvMaterial3Api public final class TabDefaults {
+ method @androidx.compose.runtime.Composable public androidx.tv.material3.TabColors pillIndicatorTabColors(optional long activeContentColor, optional long contentColor, optional long selectedContentColor, optional long focusedContentColor, optional long focusedSelectedContentColor, optional long disabledActiveContentColor, optional long disabledContentColor, optional long disabledSelectedContentColor);
+ method @androidx.compose.runtime.Composable public androidx.tv.material3.TabColors underlinedIndicatorTabColors(optional long activeContentColor, optional long contentColor, optional long selectedContentColor, optional long focusedContentColor, optional long focusedSelectedContentColor, optional long disabledActiveContentColor, optional long disabledContentColor, optional long disabledSelectedContentColor);
field public static final androidx.tv.material3.TabDefaults INSTANCE;
}
public final class TabKt {
- method @androidx.compose.runtime.Composable public static void Tab(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onFocus, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional boolean enabled, optional androidx.tv.material3.TabColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Tab(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onFocus, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional boolean enabled, optional androidx.tv.material3.TabColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
}
- public final class TabRowDefaults {
+ @androidx.tv.material3.ExperimentalTvMaterial3Api public final class TabRowDefaults {
method @androidx.compose.runtime.Composable public void PillIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
method @androidx.compose.runtime.Composable public void TabSeparator();
method @androidx.compose.runtime.Composable public void UnderlinedIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
@@ -212,7 +212,7 @@
}
public final class TabRowKt {
- method @androidx.compose.runtime.Composable public static void TabRow(int selectedTabIndex, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> separator, optional kotlin.jvm.functions.Function1<? super java.util.List<androidx.compose.ui.unit.DpRect>,kotlin.Unit> indicator, kotlin.jvm.functions.Function0<kotlin.Unit> tabs);
+ method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void TabRow(int selectedTabIndex, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> separator, optional kotlin.jvm.functions.Function1<? super java.util.List<androidx.compose.ui.unit.DpRect>,kotlin.Unit> indicator, kotlin.jvm.functions.Function0<kotlin.Unit> tabs);
}
public final class TextKt {
diff --git a/tv/tv-material/api/restricted_current.txt b/tv/tv-material/api/restricted_current.txt
index 0b46921..8151caa 100644
--- a/tv/tv-material/api/restricted_current.txt
+++ b/tv/tv-material/api/restricted_current.txt
@@ -65,31 +65,10 @@
public final class ShapesKt {
}
- public final class TabColors {
- }
-
- public final class TabDefaults {
- method @androidx.compose.runtime.Composable public androidx.tv.material3.TabColors pillIndicatorTabColors(optional long activeContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long disabledActiveContentColor, optional long disabledSelectedContentColor);
- method @androidx.compose.runtime.Composable public androidx.tv.material3.TabColors underlinedIndicatorTabColors(optional long activeContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long disabledActiveContentColor, optional long disabledSelectedContentColor);
- field public static final androidx.tv.material3.TabDefaults INSTANCE;
- }
-
public final class TabKt {
- method @androidx.compose.runtime.Composable public static void Tab(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onFocus, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional boolean enabled, optional androidx.tv.material3.TabColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
- }
-
- public final class TabRowDefaults {
- method @androidx.compose.runtime.Composable public void PillIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
- method @androidx.compose.runtime.Composable public void TabSeparator();
- method @androidx.compose.runtime.Composable public void UnderlinedIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
- method @androidx.compose.runtime.Composable public long contentColor();
- method public long getContainerColor();
- property public final long ContainerColor;
- field public static final androidx.tv.material3.TabRowDefaults INSTANCE;
}
public final class TabRowKt {
- method @androidx.compose.runtime.Composable public static void TabRow(int selectedTabIndex, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> separator, optional kotlin.jvm.functions.Function1<? super java.util.List<androidx.compose.ui.unit.DpRect>,kotlin.Unit> indicator, kotlin.jvm.functions.Function0<kotlin.Unit> tabs);
}
public final class TextKt {
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/TabRowTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/TabRowTest.kt
index 0a835b2..fd811f2 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/TabRowTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/TabRowTest.kt
@@ -149,6 +149,7 @@
rule.onNodeWithTag(firstTab).assertIsFocused()
}
+ @OptIn(ExperimentalTvMaterial3Api::class)
@Test
fun tabRow_changeActiveTabOnClick() {
val tabs = constructTabs(count = 2)
@@ -227,6 +228,7 @@
}
}
+@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
private fun TabRowSample(
tabs: List<String>,
@@ -288,6 +290,7 @@
}
}
+@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
private fun TabSample(
selected: Boolean,
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Tab.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Tab.kt
index 792f0c9..bed889f 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Tab.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Tab.kt
@@ -27,7 +27,9 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
@@ -60,6 +62,7 @@
* and customize the appearance / behavior of this tab in different states.
* @param content content of the [Tab]
*/
+@ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
@Composable
fun Tab(
selected: Boolean,
@@ -71,11 +74,13 @@
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
content: @Composable RowScope.() -> Unit
) {
+ var isFocused by remember { mutableStateOf(false) }
val contentColor by
animateColorAsState(
getTabContentColor(
colors = colors,
- anyTabFocused = LocalTabRowHasFocus.current,
+ isTabRowActive = LocalTabRowHasFocus.current,
+ focused = isFocused,
selected = selected,
enabled = enabled,
)
@@ -89,6 +94,7 @@
this.role = Role.Tab
}
.onFocusChanged {
+ isFocused = it.isFocused
if (it.isFocused) {
onFocus()
}
@@ -114,12 +120,16 @@
* - See [TabDefaults.underlinedIndicatorTabColors] for the default colors used in a [Tab] when
* using an Underlined indicator
*/
+@ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
class TabColors
internal constructor(
private val activeContentColor: Color,
+ private val contentColor: Color = activeContentColor.copy(alpha = 0.4f),
private val selectedContentColor: Color,
private val focusedContentColor: Color,
+ private val focusedSelectedContentColor: Color,
private val disabledActiveContentColor: Color,
+ private val disabledContentColor: Color = disabledActiveContentColor.copy(alpha = 0.4f),
private val disabledSelectedContentColor: Color,
) {
/**
@@ -130,8 +140,7 @@
* @param enabled whether the button is enabled
*/
internal fun inactiveContentColor(enabled: Boolean): Color {
- return if (enabled) activeContentColor.copy(alpha = 0.4f)
- else disabledActiveContentColor.copy(alpha = 0.4f)
+ return if (enabled) contentColor else disabledContentColor
}
/**
@@ -160,87 +169,133 @@
* Represents the content color for this tab, depending on whether it is focused
*
* * [Tab] is focused when the current [Tab] is selected and focused
+ *
+ * @param selected whether the tab is selected
*/
- internal fun focusedContentColor(): Color {
- return focusedContentColor
+ internal fun focusedContentColor(selected: Boolean): Color {
+ return if (selected) focusedSelectedContentColor else focusedContentColor
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || other !is TabColors) return false
- if (activeContentColor != other.activeContentColor(true)) return false
- if (selectedContentColor != other.selectedContentColor(true)) return false
- if (focusedContentColor != other.focusedContentColor()) return false
-
- if (disabledActiveContentColor != other.activeContentColor(false)) return false
- if (disabledSelectedContentColor != other.selectedContentColor(false)) return false
+ if (activeContentColor != other.activeContentColor) return false
+ if (contentColor != other.contentColor) return false
+ if (selectedContentColor != other.selectedContentColor) return false
+ if (focusedContentColor != other.focusedContentColor) return false
+ if (focusedSelectedContentColor != other.focusedSelectedContentColor) return false
+ if (disabledActiveContentColor != other.disabledActiveContentColor) return false
+ if (disabledContentColor != other.disabledContentColor) return false
+ if (disabledSelectedContentColor != other.disabledSelectedContentColor) return false
return true
}
override fun hashCode(): Int {
var result = activeContentColor.hashCode()
+ result = 31 * result + contentColor.hashCode()
result = 31 * result + selectedContentColor.hashCode()
result = 31 * result + focusedContentColor.hashCode()
+ result = 31 * result + focusedSelectedContentColor.hashCode()
result = 31 * result + disabledActiveContentColor.hashCode()
+ result = 31 * result + disabledContentColor.hashCode()
result = 31 * result + disabledSelectedContentColor.hashCode()
return result
}
}
+@ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
object TabDefaults {
/**
* [Tab]'s content colors to in conjunction with underlined indicator
+ *
+ * @param activeContentColor applied when the any of the other tabs is focused
+ * @param contentColor the default color of the tab's content when none of the tabs are focused
+ * @param selectedContentColor applied when the current tab is selected
+ * @param focusedContentColor applied when the current tab is focused
+ * @param focusedSelectedContentColor applied when the current tab is both focused and selected
+ * @param disabledActiveContentColor applied when any of the other tabs is focused and the
+ * current tab is disabled
+ * @param disabledContentColor applied when the current tab is disabled and none of the tabs are
+ * focused
+ * @param disabledSelectedContentColor applied when the current tab is disabled and selected
*/
- // TODO: get selected & focused values from theme
+ // TODO: get selectedContentColor & focusedContentColor values from theme
+ @OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun underlinedIndicatorTabColors(
activeContentColor: Color = LocalContentColor.current,
+ contentColor: Color = activeContentColor.copy(alpha = 0.4f),
selectedContentColor: Color = Color(0xFFC9C2E8),
focusedContentColor: Color = Color(0xFFC9BFFF),
+ focusedSelectedContentColor: Color = focusedContentColor,
disabledActiveContentColor: Color = activeContentColor,
+ disabledContentColor: Color = disabledActiveContentColor.copy(alpha = 0.4f),
disabledSelectedContentColor: Color = selectedContentColor,
): TabColors =
TabColors(
activeContentColor = activeContentColor,
+ contentColor = contentColor,
selectedContentColor = selectedContentColor,
focusedContentColor = focusedContentColor,
+ focusedSelectedContentColor = focusedSelectedContentColor,
disabledActiveContentColor = disabledActiveContentColor,
+ disabledContentColor = disabledContentColor,
disabledSelectedContentColor = disabledSelectedContentColor,
)
/**
* [Tab]'s content colors to in conjunction with pill indicator
+ *
+ * @param activeContentColor applied when the any of the other tabs is focused
+ * @param contentColor the default color of the tab's content when none of the tabs are focused
+ * @param selectedContentColor applied when the current tab is selected
+ * @param focusedContentColor applied when the current tab is focused
+ * @param focusedSelectedContentColor applied when the current tab is both focused and selected
+ * @param disabledActiveContentColor applied when any of the other tabs is focused and the
+ * current tab is disabled
+ * @param disabledContentColor applied when the current tab is disabled and none of the tabs are
+ * focused
+ * @param disabledSelectedContentColor applied when the current tab is disabled and selected
*/
// TODO: get selected & focused values from theme
+ @OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun pillIndicatorTabColors(
activeContentColor: Color = LocalContentColor.current,
+ contentColor: Color = activeContentColor.copy(alpha = 0.4f),
selectedContentColor: Color = Color(0xFFE5DEFF),
focusedContentColor: Color = Color(0xFF313033),
+ focusedSelectedContentColor: Color = focusedContentColor,
disabledActiveContentColor: Color = activeContentColor,
+ disabledContentColor: Color = disabledActiveContentColor.copy(alpha = 0.4f),
disabledSelectedContentColor: Color = selectedContentColor,
): TabColors =
TabColors(
activeContentColor = activeContentColor,
+ contentColor = contentColor,
selectedContentColor = selectedContentColor,
focusedContentColor = focusedContentColor,
+ focusedSelectedContentColor = focusedSelectedContentColor,
disabledActiveContentColor = disabledActiveContentColor,
+ disabledContentColor = disabledContentColor,
disabledSelectedContentColor = disabledSelectedContentColor,
)
}
/** Returns the [Tab]'s content color based on focused/selected state */
+@OptIn(ExperimentalTvMaterial3Api::class)
private fun getTabContentColor(
colors: TabColors,
- anyTabFocused: Boolean,
+ isTabRowActive: Boolean,
+ focused: Boolean,
selected: Boolean,
enabled: Boolean,
): Color =
when {
- anyTabFocused && selected -> colors.focusedContentColor()
+ focused -> colors.focusedContentColor(selected)
selected -> colors.selectedContentColor(enabled)
- anyTabFocused -> colors.activeContentColor(enabled)
+ isTabRowActive -> colors.activeContentColor(enabled)
else -> colors.inactiveContentColor(enabled)
}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/TabRow.kt b/tv/tv-material/src/main/java/androidx/tv/material3/TabRow.kt
index d07a6a7..a1af7e7 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/TabRow.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/TabRow.kt
@@ -78,6 +78,7 @@
* @param indicator used to indicate which tab is currently selected and/or focused
* @param tabs a composable which will render all the tabs
*/
+@ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
@Composable
fun TabRow(
selectedTabIndex: Int,
@@ -161,6 +162,7 @@
}
}
+@ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
object TabRowDefaults {
/** Color of the background of a tab */
val ContainerColor = Color.Transparent