Introduce Border indication for TV Compose
Border indication will add a border around the composable (independent from `Modifier.border`) that can react to different indication states like `focused`, `pressed`, etc.
Test: Added UI test
Relnote: "Introduce Border indication for TV Compose
Border indication will add a border around the composable (independent from `Modifier.border`) that can react to different indication states like `focused`, `pressed`, etc."
Change-Id: I4a6d8a4ae15d9d038fa27f4c8ffbba90bb494b49
diff --git a/tv/tv-material/api/public_plus_experimental_current.txt b/tv/tv-material/api/public_plus_experimental_current.txt
index 8a14d13..853801b 100644
--- a/tv/tv-material/api/public_plus_experimental_current.txt
+++ b/tv/tv-material/api/public_plus_experimental_current.txt
@@ -1,6 +1,29 @@
// Signature format: 4.0
package androidx.tv.material3 {
+ @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class Border {
+ ctor public Border(androidx.compose.foundation.BorderStroke border, optional float inset, optional androidx.compose.ui.graphics.Shape shape);
+ method public androidx.tv.material3.Border copy(optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.ui.unit.Dp? inset, optional androidx.compose.ui.graphics.Shape? shape);
+ method public androidx.compose.foundation.BorderStroke getBorder();
+ method public float getInset();
+ method public androidx.compose.ui.graphics.Shape getShape();
+ property public final androidx.compose.foundation.BorderStroke border;
+ property public final float inset;
+ property public final androidx.compose.ui.graphics.Shape shape;
+ field public static final androidx.tv.material3.Border.Companion Companion;
+ }
+
+ public static final class Border.Companion {
+ method public androidx.tv.material3.Border getNone();
+ property public final androidx.tv.material3.Border None;
+ }
+
+ @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class BorderIndication implements androidx.compose.foundation.Indication {
+ ctor public BorderIndication(androidx.compose.ui.graphics.Brush brush, float width, androidx.compose.ui.graphics.Shape shape, optional float inset);
+ ctor public BorderIndication(androidx.tv.material3.Border border);
+ method @androidx.compose.runtime.Composable public androidx.compose.foundation.IndicationInstance rememberUpdatedInstance(androidx.compose.foundation.interaction.InteractionSource interactionSource);
+ }
+
@androidx.tv.material3.ExperimentalTvMaterial3Api public final class CarouselDefaults {
method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public void IndicatorRow(int slideCount, int activeSlideIndex, optional androidx.compose.ui.Modifier modifier, optional float spacing, optional kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> indicator);
method @androidx.compose.runtime.Composable public androidx.compose.animation.ContentTransform getContentTransform();
@@ -36,10 +59,14 @@
property public final int activeSlideIndex;
}
+ @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ClickableSurfaceBorder {
+ }
+
@androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ClickableSurfaceColor {
}
@androidx.tv.material3.ExperimentalTvMaterial3Api public final class ClickableSurfaceDefaults {
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ClickableSurfaceBorder border(optional androidx.tv.material3.Border border, optional androidx.tv.material3.Border focusedBorder, optional androidx.tv.material3.Border pressedBorder, optional androidx.tv.material3.Border disabledBorder, optional androidx.tv.material3.Border focusedDisabledBorder);
method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ClickableSurfaceColor color(optional long color, optional long focusedColor, optional long pressedColor, optional long disabledColor);
method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ClickableSurfaceColor contentColor(optional long color, optional long focusedColor, optional long pressedColor, optional long disabledColor);
method public androidx.tv.material3.ClickableSurfaceGlow glow(optional androidx.tv.material3.Glow glow, optional androidx.tv.material3.Glow focusedGlow, optional androidx.tv.material3.Glow pressedGlow);
@@ -64,9 +91,11 @@
}
@androidx.compose.runtime.Stable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ColorScheme {
- ctor public ColorScheme(long primary, long onPrimary, long primaryContainer, long onPrimaryContainer, long inversePrimary, long secondary, long onSecondary, long secondaryContainer, long onSecondaryContainer, long tertiary, long onTertiary, long tertiaryContainer, long onTertiaryContainer, long background, long onBackground, long surface, long onSurface, long surfaceVariant, long onSurfaceVariant, long surfaceTint, long inverseSurface, long inverseOnSurface, long error, long onError, long errorContainer, long onErrorContainer, long outline, long outlineVariant, long scrim);
- method public androidx.tv.material3.ColorScheme copy(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline, optional long outlineVariant, optional long scrim);
+ ctor public ColorScheme(long primary, long onPrimary, long primaryContainer, long onPrimaryContainer, long inversePrimary, long secondary, long onSecondary, long secondaryContainer, long onSecondaryContainer, long tertiary, long onTertiary, long tertiaryContainer, long onTertiaryContainer, long background, long onBackground, long surface, long onSurface, long surfaceVariant, long onSurfaceVariant, long surfaceTint, long inverseSurface, long inverseOnSurface, long error, long onError, long errorContainer, long onErrorContainer, long border, long borderVariant, long scrim);
+ method public androidx.tv.material3.ColorScheme copy(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long border, optional long borderVariant, optional long scrim);
method public long getBackground();
+ method public long getBorder();
+ method public long getBorderVariant();
method public long getError();
method public long getErrorContainer();
method public long getInverseOnSurface();
@@ -83,8 +112,6 @@
method public long getOnSurfaceVariant();
method public long getOnTertiary();
method public long getOnTertiaryContainer();
- method public long getOutline();
- method public long getOutlineVariant();
method public long getPrimary();
method public long getPrimaryContainer();
method public long getScrim();
@@ -96,6 +123,8 @@
method public long getTertiary();
method public long getTertiaryContainer();
property public final long background;
+ property public final long border;
+ property public final long borderVariant;
property public final long error;
property public final long errorContainer;
property public final long inverseOnSurface;
@@ -112,8 +141,6 @@
property public final long onSurfaceVariant;
property public final long onTertiary;
property public final long onTertiaryContainer;
- property public final long outline;
- property public final long outlineVariant;
property public final long primary;
property public final long primaryContainer;
property public final long scrim;
@@ -129,8 +156,8 @@
public final class ColorSchemeKt {
method @androidx.tv.material3.ExperimentalTvMaterial3Api public static long contentColorFor(androidx.tv.material3.ColorScheme, long backgroundColor);
method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static long contentColorFor(long backgroundColor);
- method @androidx.tv.material3.ExperimentalTvMaterial3Api public static androidx.tv.material3.ColorScheme darkColorScheme(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline, optional long outlineVariant, optional long scrim);
- method @androidx.tv.material3.ExperimentalTvMaterial3Api public static androidx.tv.material3.ColorScheme lightColorScheme(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline, optional long outlineVariant, optional long scrim);
+ method @androidx.tv.material3.ExperimentalTvMaterial3Api public static androidx.tv.material3.ColorScheme darkColorScheme(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long border, optional long borderVariant, optional long scrim);
+ method @androidx.tv.material3.ExperimentalTvMaterial3Api public static androidx.tv.material3.ColorScheme lightColorScheme(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long border, optional long borderVariant, optional long scrim);
method @androidx.tv.material3.ExperimentalTvMaterial3Api public static long surfaceColorAtElevation(androidx.tv.material3.ColorScheme, float elevation);
}
@@ -240,7 +267,7 @@
}
public final class SurfaceKt {
- method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Surface(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional float tonalElevation, optional androidx.tv.material3.ClickableSurfaceShape shape, optional androidx.tv.material3.ClickableSurfaceColor color, optional androidx.tv.material3.ClickableSurfaceColor contentColor, optional androidx.tv.material3.ClickableSurfaceScale scale, optional androidx.tv.material3.ClickableSurfaceGlow glow, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Surface(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional float tonalElevation, optional androidx.tv.material3.ClickableSurfaceShape shape, optional androidx.tv.material3.ClickableSurfaceColor color, optional androidx.tv.material3.ClickableSurfaceColor contentColor, optional androidx.tv.material3.ClickableSurfaceScale scale, optional androidx.tv.material3.ClickableSurfaceBorder border, optional androidx.tv.material3.ClickableSurfaceGlow glow, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.Dp> getLocalAbsoluteTonalElevation();
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.Dp> LocalAbsoluteTonalElevation;
}
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/SurfaceTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/SurfaceTest.kt
index b630f44..7e93eea 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/SurfaceTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/SurfaceTest.kt
@@ -18,6 +18,7 @@
import android.os.Build
import androidx.annotation.RequiresApi
+import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.interaction.FocusInteraction
@@ -461,4 +462,34 @@
rule.onRoot().captureToImage().assertDoesNotContainColor(Color.Blue)
}
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ @Test
+ fun clickableSurface_onFocus_showsBorder() {
+ rule.setContent {
+ Surface(
+ onClick = { /* Do something */ },
+ modifier = Modifier
+ .size(100.toDp())
+ .testTag("surface"),
+ border = ClickableSurfaceDefaults.border(
+ focusedBorder = Border(
+ border = BorderStroke(width = 5.toDp(), color = Color.Magenta)
+ )
+ ),
+ color = ClickableSurfaceDefaults.color(
+ color = Color.Transparent,
+ focusedColor = Color.Transparent
+ )
+ ) {}
+ }
+
+ val surface = rule.onNodeWithTag("surface")
+
+ surface.captureToImage().assertDoesNotContainColor(Color.Magenta)
+
+ surface.performSemanticsAction(SemanticsActions.RequestFocus)
+
+ surface.captureToImage().assertContainsColor(Color.Magenta)
+ }
}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Border.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Border.kt
new file mode 100644
index 0000000..4b43641
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Border.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.
+ */
+
+package androidx.tv.material3
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+/**
+ * Defines the border for a TV component.
+ * @param border configures the width and brush for the border
+ * @param inset defines how far (in dp) should the border be from the component it's applied to
+ * @param shape defines the [Shape] of the border
+ */
+@ExperimentalTvMaterial3Api
+@Immutable
+class Border(
+ val border: BorderStroke,
+ val inset: Dp = 0.dp,
+ val shape: Shape = ShapeDefaults.Medium
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || this::class != other::class) return false
+
+ other as Border
+
+ if (border != other.border) return false
+ if (inset != other.inset) return false
+ if (shape != other.shape) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = border.hashCode()
+ result = 31 * result + inset.hashCode()
+ result = 31 * result + shape.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "Border(border=$border, inset=$inset, shape=$shape)"
+ }
+
+ fun copy(
+ border: BorderStroke? = null,
+ inset: Dp? = null,
+ shape: Shape? = null
+ ): Border = Border(
+ border = border ?: this.border,
+ inset = inset ?: this.inset,
+ shape = shape ?: this.shape
+ )
+
+ companion object {
+ /**
+ * Signifies the absence of a border. Use this if you do not want to display a border
+ * indication in any of the TV Components.
+ */
+ val None = Border(
+ border = BorderStroke(width = 0.dp, color = Color.Transparent),
+ inset = 0.dp,
+ shape = RectangleShape
+ )
+ }
+}
\ No newline at end of file
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/ColorScheme.kt b/tv/tv-material/src/main/java/androidx/tv/material3/ColorScheme.kt
index 257754a..f6903d5 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/ColorScheme.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/ColorScheme.kt
@@ -65,8 +65,8 @@
onError: Color = ColorLightTokens.OnError,
errorContainer: Color = ColorLightTokens.ErrorContainer,
onErrorContainer: Color = ColorLightTokens.OnErrorContainer,
- outline: Color = ColorLightTokens.Outline,
- outlineVariant: Color = ColorLightTokens.OutlineVariant,
+ border: Color = ColorLightTokens.Border,
+ borderVariant: Color = ColorLightTokens.BorderVariant,
scrim: Color = ColorLightTokens.Scrim
): ColorScheme =
ColorScheme(
@@ -96,8 +96,8 @@
onError = onError,
errorContainer = errorContainer,
onErrorContainer = onErrorContainer,
- outline = outline,
- outlineVariant = outlineVariant,
+ border = border,
+ borderVariant = borderVariant,
scrim = scrim
)
@@ -157,9 +157,9 @@
* @property errorContainer The preferred tonal color of error containers.
* @property onErrorContainer The color (and state variants) that should be used for content on
* top of [errorContainer].
- * @property outline Subtle color used for boundaries. Outline color role adds contrast for
+ * @property border Subtle color used for boundaries. Border color role adds contrast for
* accessibility purposes.
- * @property outlineVariant Utility color used for boundaries for decorative elements when strong
+ * @property borderVariant Utility color used for boundaries for decorative elements when strong
* contrast is not required.
* @property scrim Color of a scrim that obscures content.
*/
@@ -192,8 +192,8 @@
onError: Color,
errorContainer: Color,
onErrorContainer: Color,
- outline: Color,
- outlineVariant: Color,
+ border: Color,
+ borderVariant: Color,
scrim: Color
) {
var primary by mutableStateOf(primary, structuralEqualityPolicy())
@@ -248,9 +248,9 @@
internal set
var onErrorContainer by mutableStateOf(onErrorContainer, structuralEqualityPolicy())
internal set
- var outline by mutableStateOf(outline, structuralEqualityPolicy())
+ var border by mutableStateOf(border, structuralEqualityPolicy())
internal set
- var outlineVariant by mutableStateOf(outlineVariant, structuralEqualityPolicy())
+ var borderVariant by mutableStateOf(borderVariant, structuralEqualityPolicy())
internal set
var scrim by mutableStateOf(scrim, structuralEqualityPolicy())
internal set
@@ -283,8 +283,8 @@
onError: Color = this.onError,
errorContainer: Color = this.errorContainer,
onErrorContainer: Color = this.onErrorContainer,
- outline: Color = this.outline,
- outlineVariant: Color = this.outlineVariant,
+ border: Color = this.border,
+ borderVariant: Color = this.borderVariant,
scrim: Color = this.scrim
): ColorScheme =
ColorScheme(
@@ -314,8 +314,8 @@
onError = onError,
errorContainer = errorContainer,
onErrorContainer = onErrorContainer,
- outline = outline,
- outlineVariant = outlineVariant,
+ border = border,
+ borderVariant = borderVariant,
scrim = scrim
)
@@ -347,8 +347,8 @@
"onError=$onError" +
"errorContainer=$errorContainer" +
"onErrorContainer=$onErrorContainer" +
- "outline=$outline" +
- "outlineVariant=$outlineVariant" +
+ "border=$border" +
+ "borderVariant=$borderVariant" +
"scrim=$scrim" +
")"
}
@@ -385,8 +385,8 @@
onError: Color = ColorDarkTokens.OnError,
errorContainer: Color = ColorDarkTokens.ErrorContainer,
onErrorContainer: Color = ColorDarkTokens.OnErrorContainer,
- outline: Color = ColorDarkTokens.Outline,
- outlineVariant: Color = ColorDarkTokens.OutlineVariant,
+ border: Color = ColorDarkTokens.Border,
+ borderVariant: Color = ColorDarkTokens.BorderVariant,
scrim: Color = ColorDarkTokens.Scrim
): ColorScheme =
ColorScheme(
@@ -416,8 +416,8 @@
onError = onError,
errorContainer = errorContainer,
onErrorContainer = onErrorContainer,
- outline = outline,
- outlineVariant = outlineVariant,
+ border = border,
+ borderVariant = borderVariant,
scrim = scrim
)
@@ -555,8 +555,8 @@
onError = other.onError
errorContainer = other.errorContainer
onErrorContainer = other.onErrorContainer
- outline = other.outline
- outlineVariant = other.outlineVariant
+ border = other.border
+ borderVariant = other.borderVariant
scrim = other.scrim
}
@@ -586,8 +586,8 @@
ColorSchemeKeyTokens.SurfaceTint -> surfaceTint
ColorSchemeKeyTokens.OnTertiary -> onTertiary
ColorSchemeKeyTokens.OnTertiaryContainer -> onTertiaryContainer
- ColorSchemeKeyTokens.Outline -> outline
- ColorSchemeKeyTokens.OutlineVariant -> outlineVariant
+ ColorSchemeKeyTokens.Border -> border
+ ColorSchemeKeyTokens.BorderVariant -> borderVariant
ColorSchemeKeyTokens.Primary -> primary
ColorSchemeKeyTokens.PrimaryContainer -> primaryContainer
ColorSchemeKeyTokens.Scrim -> scrim
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Glow.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Glow.kt
new file mode 100644
index 0000000..a246c10
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Glow.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.
+ */
+
+package androidx.tv.material3
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+/**
+ * Defines the shadow for a TV component.
+ * @param elevationColor [Color] to be applied on the shadow
+ * @param elevation defines how strong should be the shadow. Larger its value, further the
+ * shadow goes from the center of the component.
+ */
+@ExperimentalTvMaterial3Api
+@Immutable
+class Glow(
+ val elevationColor: Color,
+ val elevation: Dp
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || this::class != other::class) return false
+
+ other as Glow
+
+ if (elevationColor != other.elevationColor) return false
+ if (elevation != other.elevation) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = elevationColor.hashCode()
+ result = 31 * result + elevation.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "Glow(elevationColor=$elevationColor, elevation=$elevation)"
+ }
+
+ fun copy(
+ glowColor: Color? = null,
+ glowElevation: Dp? = null
+ ): Glow = Glow(
+ elevationColor = glowColor ?: this.elevationColor,
+ elevation = glowElevation ?: this.elevation
+ )
+
+ companion object {
+ /**
+ * Signifies the absence of a glow in TV Components. Use this if you do not want to display
+ * a glow indication in any of the Leanback TV Components.
+ */
+ val None = Glow(
+ elevationColor = Color.Transparent,
+ elevation = 0.dp
+ )
+ }
+}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Indications.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Indications.kt
index 0eb0b30..0b0626d 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Indications.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Indications.kt
@@ -23,22 +23,32 @@
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Indication
import androidx.compose.foundation.IndicationInstance
+import androidx.compose.foundation.border
import androidx.compose.foundation.interaction.FocusInteraction
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
+import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.graphics.drawOutline
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
+import androidx.compose.ui.graphics.drawscope.inset
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalDensity
@@ -162,6 +172,84 @@
)
}
+/**
+ * Border indication will add a border around the composable (independent from [Modifier.border])
+ * that can react to different [Indication] states like focused, pressed, etc.
+ * @param brush describes the color/gradient of the border.
+ * @param width describes the width/thickness of the border.
+ * @param shape describes the shape of the border. It is generally kept the same as the composable
+ * it's being applied to.
+ * @param inset describes the offset of the border from the composable it's being applied to.
+ */
+@ExperimentalTvMaterial3Api
+@Immutable
+class BorderIndication(
+ private val brush: Brush,
+ private val width: Dp,
+ private val shape: Shape,
+ private val inset: Dp = 0.dp
+) : Indication {
+
+ /**
+ * Creates an instance of [BorderIndication] from [androidx.tv.material3.Border].
+ * @param border the [androidx.tv.material3.Border] instance that is used to create and return
+ * an [BorderIndication] instance
+ */
+ constructor(border: Border) :
+ this(
+ brush = border.border.brush,
+ width = border.border.width,
+ shape = border.shape,
+ inset = border.inset
+ )
+ @Composable
+ override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance {
+ val density = LocalDensity.current
+ val brushState = rememberUpdatedState(brush)
+ val widthState = rememberUpdatedState(width)
+ val shapeState = rememberUpdatedState(shape)
+ val insetState = rememberUpdatedState(inset)
+
+ return remember(interactionSource, density) {
+ BorderIndicationInstance(
+ brush = brushState,
+ width = widthState,
+ shape = shapeState,
+ density = density,
+ inset = insetState
+ )
+ }
+ }
+}
+
+internal class BorderIndicationInstance(
+ private val brush: State<Brush>,
+ private val width: State<Dp>,
+ private val shape: State<Shape>,
+ private val density: Density,
+ private val inset: State<Dp>
+) : IndicationInstance {
+ override fun ContentDrawScope.drawIndication() {
+ drawContent()
+
+ inset(inset = -inset.value.toPx()) {
+ drawOutline(
+ outline = shape.value.createOutline(
+ size = size,
+ layoutDirection = layoutDirection,
+ density = [email protected]
+ ),
+ brush = brush.value,
+ alpha = 1f,
+ style = Stroke(
+ width = width.value.toPx(),
+ cap = StrokeCap.Round
+ )
+ )
+ }
+ }
+}
+
internal object ScaleIndicationTokens {
const val focusDuration: Int = 300
const val unFocusDuration: Int = 500
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Surface.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Surface.kt
index 7d2555f..c446450 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Surface.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Surface.kt
@@ -73,6 +73,7 @@
* @param color Color to be used on background of the Surface
* @param contentColor The preferred content color provided by this Surface to its children.
* @param scale Defines size of the Surface relative to its original size.
+ * @param border Defines a border around the Surface.
* @param glow Diffused shadow to be shown behind the Surface.
* @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
* for this Surface. You can create and pass in your own remembered [MutableInteractionSource] if
@@ -92,6 +93,7 @@
color: ClickableSurfaceColor = ClickableSurfaceDefaults.color(),
contentColor: ClickableSurfaceColor = ClickableSurfaceDefaults.contentColor(),
scale: ClickableSurfaceScale = ClickableSurfaceDefaults.scale(),
+ border: ClickableSurfaceBorder = ClickableSurfaceDefaults.border(),
glow: ClickableSurfaceGlow = ClickableSurfaceDefaults.glow(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
content: @Composable (BoxScope.() -> Unit)
@@ -133,6 +135,12 @@
pressed = pressed,
scale = scale
),
+ border = ClickableSurfaceDefaults.border(
+ enabled = enabled,
+ focused = focused,
+ pressed = pressed,
+ border = border
+ ),
glow = ClickableSurfaceDefaults.glow(
enabled = enabled,
focused = focused,
@@ -154,6 +162,7 @@
color: Color,
contentColor: Color,
scale: Float,
+ border: Border,
glow: Glow,
tonalElevation: Dp,
interactionSource: MutableInteractionSource,
@@ -210,6 +219,14 @@
placeable.place(0, 0, zIndex = zIndex)
}
}
+ .then(
+ if (border != Border.None) {
+ Modifier.indication(
+ interactionSource = interactionSource,
+ indication = remember { BorderIndication(border = border) }
+ )
+ } else Modifier
+ )
.drawWithCache {
onDrawBehind {
drawOutline(
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/SurfaceDefaults.kt b/tv/tv-material/src/main/java/androidx/tv/material3/SurfaceDefaults.kt
index 67f0697..35afd2f 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/SurfaceDefaults.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/SurfaceDefaults.kt
@@ -17,11 +17,13 @@
package androidx.tv.material3
import androidx.annotation.FloatRange
+import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.unit.dp
/**
* Contains the default values used by clickable Surface.
@@ -174,6 +176,55 @@
focusedDisabledScale = focusedDisabledScale
)
+ internal fun border(
+ enabled: Boolean,
+ focused: Boolean,
+ pressed: Boolean,
+ border: ClickableSurfaceBorder
+ ): Border {
+ return when {
+ pressed && enabled -> border.pressedBorder
+ focused && enabled -> border.focusedBorder
+ focused && !enabled -> border.focusedDisabledBorder
+ enabled -> border.border
+ else -> border.disabledBorder
+ }
+ }
+
+ /**
+ * Creates a [ClickableSurfaceBorder] that represents the default [Border]s applied on a
+ * Surface in different [Interaction] states.
+ *
+ * @param border the [Border] to be used for this Surface when enabled
+ * @param focusedBorder the [Border] to be used for this Surface when focused
+ * @param pressedBorder the [Border] to be used for this Surface when pressed
+ * @param disabledBorder the [Border] to be used for this Surface when disabled
+ * @param focusedDisabledBorder the [Border] to be used for this Surface when disabled and
+ * focused
+ */
+ @ReadOnlyComposable
+ @Composable
+ fun border(
+ border: Border = Border.None,
+ focusedBorder: Border = border,
+ pressedBorder: Border = focusedBorder,
+ disabledBorder: Border = border,
+ focusedDisabledBorder: Border = Border(
+ border = BorderStroke(
+ width = 2.dp,
+ color = MaterialTheme.colorScheme.border
+ ),
+ inset = 0.dp,
+ shape = ShapeDefaults.Small
+ )
+ ) = ClickableSurfaceBorder(
+ border = border,
+ focusedBorder = focusedBorder,
+ pressedBorder = pressedBorder,
+ disabledBorder = disabledBorder,
+ focusedDisabledBorder = focusedDisabledBorder
+ )
+
internal fun glow(
enabled: Boolean,
focused: Boolean,
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/SurfaceStyles.kt b/tv/tv-material/src/main/java/androidx/tv/material3/SurfaceStyles.kt
index 3cc05a5..3ea0875 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/SurfaceStyles.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/SurfaceStyles.kt
@@ -21,8 +21,6 @@
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
/**
* Defines [Shape] for all TV [Interaction] states of a Clickable Surface.
@@ -168,6 +166,50 @@
}
/**
+ * Defines [Border] for all TV states of [Surface].
+ */
+@ExperimentalTvMaterial3Api
+@Immutable
+class ClickableSurfaceBorder internal constructor(
+ internal val border: Border,
+ internal val focusedBorder: Border,
+ internal val pressedBorder: Border,
+ internal val disabledBorder: Border,
+ internal val focusedDisabledBorder: Border
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || this::class != other::class) return false
+
+ other as ClickableSurfaceBorder
+
+ if (border != other.border) return false
+ if (focusedBorder != other.focusedBorder) return false
+ if (pressedBorder != other.pressedBorder) return false
+ if (disabledBorder != other.disabledBorder) return false
+ if (focusedDisabledBorder != other.focusedDisabledBorder) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = border.hashCode()
+ result = 31 * result + focusedBorder.hashCode()
+ result = 31 * result + pressedBorder.hashCode()
+ result = 31 * result + disabledBorder.hashCode()
+ result = 31 * result + focusedDisabledBorder.hashCode()
+
+ return result
+ }
+
+ override fun toString(): String {
+ return "${this.javaClass.simpleName}(border=$border, focusedBorder=$focusedBorder, " +
+ "pressedBorder=$pressedBorder, disabledBorder=$disabledBorder, " +
+ "focusedDisabledBorder=$focusedDisabledBorder)"
+ }
+}
+
+/**
* Defines [Glow] for all TV states of [Surface].
*/
@ExperimentalTvMaterial3Api
@@ -203,57 +245,3 @@
"pressedGlow=$pressedGlow)"
}
}
-
-/**
- * Defines the shadow for a TV component.
- * @param elevationColor [Color] to be applied on the shadow
- * @param elevation defines how strong should be the shadow. Larger its value, further the
- * shadow goes from the center of the component.
- */
-@ExperimentalTvMaterial3Api
-@Immutable
-class Glow(
- val elevationColor: Color,
- val elevation: Dp
-) {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other == null || this::class != other::class) return false
-
- other as Glow
-
- if (elevationColor != other.elevationColor) return false
- if (elevation != other.elevation) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- var result = elevationColor.hashCode()
- result = 31 * result + elevation.hashCode()
- return result
- }
-
- override fun toString(): String {
- return "Glow(elevationColor=$elevationColor, elevation=$elevation)"
- }
-
- fun copy(
- glowColor: Color? = null,
- glowElevation: Dp? = null
- ): Glow = Glow(
- elevationColor = glowColor ?: this.elevationColor,
- elevation = glowElevation ?: this.elevation
- )
-
- companion object {
- /**
- * Signifies the absence of a glow in TV Components. Use this if you do not want to display
- * a glow indication in any of the Leanback TV Components.
- */
- val None = Glow(
- elevationColor = Color.Transparent,
- elevation = 0.dp
- )
- }
-}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/tokens/ColorDarkTokens.kt b/tv/tv-material/src/main/java/androidx/tv/material3/tokens/ColorDarkTokens.kt
index 460f41e..5c52011 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/tokens/ColorDarkTokens.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/tokens/ColorDarkTokens.kt
@@ -35,8 +35,8 @@
val OnSurfaceVariant = PaletteTokens.NeutralVariant80
val OnTertiary = PaletteTokens.Tertiary20
val OnTertiaryContainer = PaletteTokens.Tertiary90
- val Outline = PaletteTokens.NeutralVariant60
- val OutlineVariant = PaletteTokens.NeutralVariant30
+ val Border = PaletteTokens.NeutralVariant60
+ val BorderVariant = PaletteTokens.NeutralVariant30
val Primary = PaletteTokens.Primary80
val PrimaryContainer = PaletteTokens.Primary30
val Scrim = PaletteTokens.Neutral0
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/tokens/ColorLightTokens.kt b/tv/tv-material/src/main/java/androidx/tv/material3/tokens/ColorLightTokens.kt
index 261c8d8..bdabae5 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/tokens/ColorLightTokens.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/tokens/ColorLightTokens.kt
@@ -35,8 +35,8 @@
val OnSurfaceVariant = PaletteTokens.NeutralVariant30
val OnTertiary = PaletteTokens.Tertiary100
val OnTertiaryContainer = PaletteTokens.Tertiary10
- val Outline = PaletteTokens.NeutralVariant50
- val OutlineVariant = PaletteTokens.NeutralVariant80
+ val Border = PaletteTokens.NeutralVariant50
+ val BorderVariant = PaletteTokens.NeutralVariant80
val Primary = PaletteTokens.Primary40
val PrimaryContainer = PaletteTokens.Primary90
val Scrim = PaletteTokens.Neutral0
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/tokens/ColorSchemeKeyTokens.kt b/tv/tv-material/src/main/java/androidx/tv/material3/tokens/ColorSchemeKeyTokens.kt
index ccc622a..932ad7c 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/tokens/ColorSchemeKeyTokens.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/tokens/ColorSchemeKeyTokens.kt
@@ -35,8 +35,8 @@
OnSurfaceVariant,
OnTertiary,
OnTertiaryContainer,
- Outline,
- OutlineVariant,
+ Border,
+ BorderVariant,
Primary,
PrimaryContainer,
Scrim,