Merge "Introduce TV opinionated material cards" into androidx-main
diff --git a/tv/tv-material/api/public_plus_experimental_current.txt b/tv/tv-material/api/public_plus_experimental_current.txt
index 72025d1..26b688f 100644
--- a/tv/tv-material/api/public_plus_experimental_current.txt
+++ b/tv/tv-material/api/public_plus_experimental_current.txt
@@ -70,10 +70,16 @@
   @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CardDefaults {
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.CardBorder border(optional androidx.tv.material3.Border border, optional androidx.tv.material3.Border focusedBorder, optional androidx.tv.material3.Border pressedBorder);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.CardColors colors(optional long containerColor, optional long contentColor, optional long focusedContainerColor, optional long focusedContentColor, optional long pressedContainerColor, optional long pressedContentColor);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.CardColors compactCardColors(optional long containerColor, optional long contentColor, optional long focusedContainerColor, optional long focusedContentColor, optional long pressedContainerColor, optional long pressedContentColor);
+    method public androidx.compose.ui.graphics.Brush getContainerGradient();
     method public androidx.tv.material3.CardGlow glow(optional androidx.tv.material3.Glow glow, optional androidx.tv.material3.Glow focusedGlow, optional androidx.tv.material3.Glow pressedGlow);
     method public androidx.tv.material3.CardScale scale(optional @FloatRange(from=0.0) float scale, optional @FloatRange(from=0.0) float focusedScale, optional @FloatRange(from=0.0) float pressedScale);
     method public androidx.tv.material3.CardShape shape(optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.ui.graphics.Shape focusedShape, optional androidx.compose.ui.graphics.Shape pressedShape);
+    property public final androidx.compose.ui.graphics.Brush ContainerGradient;
+    field public static final float HorizontalImageAspectRatio = 1.7777778f;
     field public static final androidx.tv.material3.CardDefaults INSTANCE;
+    field public static final float SquareImageAspectRatio = 1.0f;
+    field public static final float VerticalImageAspectRatio = 0.6666667f;
   }
 
   @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CardGlow {
@@ -81,6 +87,9 @@
 
   public final class CardKt {
     method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Card(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.CardShape shape, optional androidx.tv.material3.CardColors colors, optional androidx.tv.material3.CardScale scale, optional androidx.tv.material3.CardBorder border, optional androidx.tv.material3.CardGlow glow, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void ClassicCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> image, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> subtitle, optional kotlin.jvm.functions.Function0<kotlin.Unit> description, optional androidx.tv.material3.CardShape shape, optional androidx.tv.material3.CardColors colors, optional androidx.tv.material3.CardScale scale, optional androidx.tv.material3.CardBorder border, optional androidx.tv.material3.CardGlow glow, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
+    method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void CompactCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> image, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> subtitle, optional kotlin.jvm.functions.Function0<kotlin.Unit> description, optional androidx.tv.material3.CardShape shape, optional androidx.tv.material3.CardColors colors, optional androidx.tv.material3.CardScale scale, optional androidx.tv.material3.CardBorder border, optional androidx.tv.material3.CardGlow glow, optional androidx.compose.ui.graphics.Brush scrimBrush, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
+    method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void WideClassicCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> image, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> subtitle, optional kotlin.jvm.functions.Function0<kotlin.Unit> description, optional androidx.tv.material3.CardShape shape, optional androidx.tv.material3.CardColors colors, optional androidx.tv.material3.CardScale scale, optional androidx.tv.material3.CardBorder border, optional androidx.tv.material3.CardGlow glow, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
   }
 
   @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CardScale {
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CardScreenshotTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CardScreenshotTest.kt
new file mode 100644
index 0000000..f613158
--- /dev/null
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CardScreenshotTest.kt
@@ -0,0 +1,393 @@
+/*
+ * 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 android.os.Build
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Alignment.Companion.Center
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onChild
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performSemanticsAction
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+class CardScreenshotTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @get:Rule
+    val screenshotRule = AndroidXScreenshotTestRule(TV_GOLDEN_MATERIAL3)
+
+    private val boxSizeModifier = Modifier.size(220.dp, 180.dp)
+    private val verticalCardSizeModifier = Modifier.size(150.dp, 120.dp)
+    private val horizontalCardSizeModifier = Modifier.size(180.dp, 100.dp)
+
+    @Test
+    fun card_lightTheme() {
+        rule.setContent {
+            LightMaterialTheme {
+                Box(
+                    modifier = boxSizeModifier.testTag(CardWrapperTag),
+                    contentAlignment = Center
+                ) {
+                    Card(
+                        modifier = verticalCardSizeModifier,
+                        onClick = { }
+                    ) {
+                        Box(Modifier.fillMaxSize()) {
+                            Text("Card", Modifier.align(Center))
+                        }
+                    }
+                }
+            }
+        }
+
+        assertAgainstGolden("card_lightTheme")
+    }
+
+    @Test
+    fun card_darkTheme() {
+        rule.setContent {
+            DarkMaterialTheme {
+                Box(
+                    modifier = boxSizeModifier.testTag(CardWrapperTag),
+                    contentAlignment = Center
+                ) {
+                    Card(
+                        modifier = verticalCardSizeModifier,
+                        onClick = { }
+                    ) {
+                        Box(Modifier.fillMaxSize()) {
+                            Text("Card", Modifier.align(Center))
+                        }
+                    }
+                }
+            }
+        }
+
+        assertAgainstGolden("card_darkTheme")
+    }
+
+    @Test
+    fun card_focused() {
+        rule.setContent {
+            Box(
+                modifier = boxSizeModifier.testTag(CardWrapperTag),
+                contentAlignment = Center
+            ) {
+                Card(
+                    modifier = verticalCardSizeModifier,
+                    onClick = { }
+                ) {
+                    Box(Modifier.fillMaxSize()) {
+                        Text("Card", Modifier.align(Center))
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag(CardWrapperTag)
+            .onChild()
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+        rule.waitForIdle()
+
+        assertAgainstGolden("card_focused")
+    }
+
+    @Test
+    fun classicCard_lightTheme() {
+        rule.setContent {
+            LightMaterialTheme {
+                Box(
+                    modifier = boxSizeModifier.testTag(CardWrapperTag),
+                    contentAlignment = Center
+                ) {
+                    ClassicCard(
+                        modifier = verticalCardSizeModifier,
+                        image = {
+                            SampleImage(
+                                Modifier
+                                    .fillMaxWidth()
+                                    .height(80.dp)
+                            )
+                        },
+                        title = { Text("Classic Card") },
+                        contentPadding = PaddingValues(8.dp),
+                        onClick = { }
+                    )
+                }
+            }
+        }
+
+        assertAgainstGolden("classicCard_lightTheme")
+    }
+
+    @Test
+    fun classicCard_darkTheme() {
+        rule.setContent {
+            DarkMaterialTheme {
+                Box(
+                    modifier = boxSizeModifier.testTag(CardWrapperTag),
+                    contentAlignment = Center
+                ) {
+                    ClassicCard(
+                        modifier = verticalCardSizeModifier,
+                        image = {
+                            SampleImage(
+                                Modifier
+                                    .fillMaxWidth()
+                                    .height(80.dp)
+                            )
+                        },
+                        title = { Text("Classic Card") },
+                        contentPadding = PaddingValues(8.dp),
+                        onClick = { }
+                    )
+                }
+            }
+        }
+
+        assertAgainstGolden("classicCard_darkTheme")
+    }
+
+    @Test
+    fun classicCard_focused() {
+        rule.setContent {
+            Box(
+                modifier = boxSizeModifier.testTag(CardWrapperTag),
+                contentAlignment = Center
+            ) {
+                ClassicCard(
+                    modifier = verticalCardSizeModifier,
+                    image = {
+                        SampleImage(
+                            Modifier
+                                .fillMaxWidth()
+                                .height(80.dp)
+                        )
+                    },
+                    title = { Text("Classic Card") },
+                    contentPadding = PaddingValues(8.dp),
+                    onClick = { }
+                )
+            }
+        }
+
+        rule.onNodeWithTag(CardWrapperTag)
+            .onChild()
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+        rule.waitForIdle()
+
+        assertAgainstGolden("classicCard_focused")
+    }
+
+    @Test
+    fun compactCard_lightTheme() {
+        rule.setContent {
+            LightMaterialTheme {
+                Box(
+                    modifier = boxSizeModifier.testTag(CardWrapperTag),
+                    contentAlignment = Center
+                ) {
+                    CompactCard(
+                        modifier = verticalCardSizeModifier,
+                        image = { SampleImage(Modifier.fillMaxSize()) },
+                        title = { Text("Compact Card", Modifier.padding(8.dp)) },
+                        onClick = { }
+                    )
+                }
+            }
+        }
+
+        assertAgainstGolden("compactCard_lightTheme")
+    }
+
+    @Test
+    fun compactCard_darkTheme() {
+        rule.setContent {
+            DarkMaterialTheme {
+                Box(
+                    modifier = boxSizeModifier.testTag(CardWrapperTag),
+                    contentAlignment = Center
+                ) {
+                    CompactCard(
+                        modifier = verticalCardSizeModifier,
+                        image = { SampleImage(Modifier.fillMaxSize()) },
+                        title = { Text("Compact Card", Modifier.padding(8.dp)) },
+                        onClick = { }
+                    )
+                }
+            }
+        }
+
+        assertAgainstGolden("compactCard_darkTheme")
+    }
+
+    @Test
+    fun compactCard_focused() {
+        rule.setContent {
+            Box(
+                modifier = boxSizeModifier.testTag(CardWrapperTag),
+                contentAlignment = Center
+            ) {
+                CompactCard(
+                    modifier = verticalCardSizeModifier,
+                    image = { SampleImage(Modifier.fillMaxSize()) },
+                    title = { Text("Compact Card", Modifier.padding(8.dp)) },
+                    onClick = { }
+                )
+            }
+        }
+
+        rule.onNodeWithTag(CardWrapperTag)
+            .onChild()
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+        rule.waitForIdle()
+
+        assertAgainstGolden("compactCard_focused")
+    }
+
+    @Test
+    fun wideClassicCard_lightTheme() {
+        rule.setContent {
+            LightMaterialTheme {
+                Box(
+                    modifier = boxSizeModifier.testTag(CardWrapperTag),
+                    contentAlignment = Center
+                ) {
+                    WideClassicCard(
+                        modifier = horizontalCardSizeModifier,
+                        image = {
+                            SampleImage(
+                                Modifier
+                                    .fillMaxHeight()
+                                    .width(80.dp)
+                            )
+                        },
+                        title = { Text("Wide Classic Card", Modifier.padding(start = 8.dp)) },
+                        contentPadding = PaddingValues(8.dp),
+                        onClick = { }
+                    )
+                }
+            }
+        }
+
+        assertAgainstGolden("wideClassicCard_lightTheme")
+    }
+
+    @Test
+    fun wideClassicCard_darkTheme() {
+        rule.setContent {
+            DarkMaterialTheme {
+                Box(
+                    modifier = boxSizeModifier.testTag(CardWrapperTag),
+                    contentAlignment = Center
+                ) {
+                    WideClassicCard(
+                        modifier = horizontalCardSizeModifier,
+                        image = {
+                            SampleImage(
+                                Modifier
+                                    .fillMaxHeight()
+                                    .width(80.dp)
+                            )
+                        },
+                        title = { Text("Wide Classic Card", Modifier.padding(start = 8.dp)) },
+                        contentPadding = PaddingValues(8.dp),
+                        onClick = { }
+                    )
+                }
+            }
+        }
+
+        assertAgainstGolden("wideClassicCard_darkTheme")
+    }
+
+    @Test
+    fun wideClassicCard_focused() {
+        rule.setContent {
+            Box(
+                modifier = boxSizeModifier.testTag(CardWrapperTag),
+                contentAlignment = Center
+            ) {
+                WideClassicCard(
+                    modifier = horizontalCardSizeModifier,
+                    image = {
+                        SampleImage(
+                            Modifier
+                                .fillMaxHeight()
+                                .width(80.dp)
+                        )
+                    },
+                    title = { Text("Wide Classic Card", Modifier.padding(start = 8.dp)) },
+                    contentPadding = PaddingValues(8.dp),
+                    onClick = { }
+                )
+            }
+        }
+
+        rule.onNodeWithTag(CardWrapperTag)
+            .onChild()
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+        rule.waitForIdle()
+
+        assertAgainstGolden("wideClassicCard_focused")
+    }
+
+    @Composable
+    fun SampleImage(modifier: Modifier = Modifier) {
+        Box(
+            modifier = modifier
+                .background(Color.Blue)
+        )
+    }
+
+    private fun assertAgainstGolden(goldenName: String) {
+        rule.onNodeWithTag(CardWrapperTag)
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, goldenName)
+    }
+}
+
+private const val CardWrapperTag = "card_wrapper"
\ No newline at end of file
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CardTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CardTest.kt
index d4437c7..3ea23a8 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CardTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CardTest.kt
@@ -23,11 +23,14 @@
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.interaction.PressInteraction
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.shape.CutCornerShape
+import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.testutils.assertIsEqualTo
 import androidx.compose.testutils.assertShape
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
@@ -44,6 +47,7 @@
 import androidx.compose.ui.test.assertIsEnabled
 import androidx.compose.ui.test.assertTextEquals
 import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.performKeyInput
@@ -51,7 +55,7 @@
 import androidx.compose.ui.test.pressKey
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
+import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
 import com.google.common.truth.Truth
 import kotlinx.coroutines.CoroutineScope
@@ -65,7 +69,7 @@
     ExperimentalComposeUiApi::class,
     ExperimentalTvMaterial3Api::class
 )
-@MediumTest
+@LargeTest
 @RunWith(AndroidJUnit4::class)
 class CardTest {
     @get:Rule
@@ -82,7 +86,7 @@
                 Card(
                     modifier = Modifier
                         .semantics(mergeDescendants = true) {}
-                        .testTag("card"),
+                        .testTag(CardTag),
                     shape = CardDefaults.shape(shape = shape),
                     colors = CardDefaults.colors(containerColor = cardColor),
                     onClick = {}
@@ -93,7 +97,7 @@
         }
 
         rule
-            .onNodeWithTag("card")
+            .onNodeWithTag(CardTag)
             .captureToImage()
             .assertShape(
                 density = rule.density,
@@ -109,14 +113,15 @@
         val count = mutableStateOf(0)
         rule.setContent {
             Card(
-                modifier = Modifier.testTag("card"),
+                modifier = Modifier.testTag(CardTag),
                 onClick = { count.value += 1 },
             ) {
                 Text("${count.value}")
                 Spacer(Modifier.size(30.dp))
             }
         }
-        rule.onNodeWithTag("card")
+
+        rule.onNodeWithTag(CardTag)
             .assertHasClickAction()
             .assert(SemanticsMatcher.keyNotDefined(SemanticsProperties.Role))
             .performSemanticsAction(SemanticsActions.RequestFocus)
@@ -131,7 +136,7 @@
         val count = mutableStateOf(0f)
         rule.setContent {
             Card(
-                modifier = Modifier.testTag("card"),
+                modifier = Modifier.testTag(CardTag),
                 onClick = { count.value += 1 },
             ) {
                 Text("${count.value}")
@@ -139,12 +144,12 @@
             }
         }
 
-        rule.onNodeWithTag("card")
+        rule.onNodeWithTag(CardTag)
             .performSemanticsAction(SemanticsActions.RequestFocus)
             .performKeyInput { pressKey(Key.DirectionCenter) }
         Truth.assertThat(count.value).isEqualTo(1)
 
-        rule.onNodeWithTag("card")
+        rule.onNodeWithTag(CardTag)
             .performSemanticsAction(SemanticsActions.RequestFocus)
             .performKeyInput { pressKey(Key.DirectionCenter) }
             .performKeyInput { pressKey(Key.DirectionCenter) }
@@ -161,7 +166,7 @@
             scope = rememberCoroutineScope()
             Card(
                 onClick = {},
-                modifier = Modifier.testTag("card"),
+                modifier = Modifier.testTag(CardTag),
                 interactionSource = interactionSource
             ) {
                 Spacer(Modifier.size(30.dp))
@@ -174,14 +179,14 @@
 
         rule.runOnIdle { Truth.assertThat(interactions).isEmpty() }
 
-        rule.onNodeWithTag("card").performSemanticsAction(SemanticsActions.RequestFocus)
+        rule.onNodeWithTag(CardTag).performSemanticsAction(SemanticsActions.RequestFocus)
 
         rule.runOnIdle {
             Truth.assertThat(interactions).hasSize(1)
             Truth.assertThat(interactions.first()).isInstanceOf(FocusInteraction.Focus::class.java)
         }
 
-        rule.onNodeWithTag("card").performKeyInput { pressKey(Key.DirectionCenter) }
+        rule.onNodeWithTag(CardTag).performKeyInput { pressKey(Key.DirectionCenter) }
 
         rule.runOnIdle {
             Truth.assertThat(interactions).hasSize(3)
@@ -190,4 +195,286 @@
             Truth.assertThat(interactions[2]).isInstanceOf(PressInteraction.Release::class.java)
         }
     }
-}
\ No newline at end of file
+
+    @Test
+    fun classicCard_semantics() {
+        val count = mutableStateOf(0)
+        rule.setContent {
+            ClassicCard(
+                modifier = Modifier
+                    .semantics(mergeDescendants = true) {}
+                    .testTag(ClassicCardTag),
+                image = { SampleImage() },
+                title = { Text("${count.value}") },
+                onClick = { count.value += 1 }
+            )
+        }
+
+        rule.onNodeWithTag(ClassicCardTag)
+            .assertHasClickAction()
+            .assert(SemanticsMatcher.keyNotDefined(SemanticsProperties.Role))
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+            .assertIsEnabled()
+            .assertTextEquals("0")
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+            .assertTextEquals("1")
+    }
+
+    @Test
+    fun classicCard_clickAction() {
+        val count = mutableStateOf(0f)
+        rule.setContent {
+            ClassicCard(
+                modifier = Modifier
+                    .semantics(mergeDescendants = true) {}
+                    .testTag(ClassicCardTag),
+                image = { SampleImage() },
+                title = { Text("${count.value}") },
+                onClick = { count.value += 1 }
+            )
+        }
+
+        rule.onNodeWithTag(ClassicCardTag)
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+        Truth.assertThat(count.value).isEqualTo(1)
+
+        rule.onNodeWithTag(ClassicCardTag)
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+        Truth.assertThat(count.value).isEqualTo(3)
+    }
+
+    @Test
+    fun classicCard_contentPadding() {
+        val contentPadding = PaddingValues(8.dp, 10.dp, 12.dp, 14.dp)
+        val cardTitleTag = "classic_card_title"
+
+        rule.setContent {
+            ClassicCard(
+                modifier = Modifier.testTag(ClassicCardTag),
+                image = { SampleImage() },
+                title = {
+                    Text(
+                        text = "Classic Card",
+                        modifier = Modifier.testTag(cardTitleTag)
+                    )
+                },
+                onClick = {},
+                contentPadding = contentPadding
+            )
+        }
+
+        val cardBounds = rule
+            .onNodeWithTag(ClassicCardTag)
+            .getUnclippedBoundsInRoot()
+
+        val imageBounds = rule
+            .onNodeWithTag(SampleImageTag, true)
+            .getUnclippedBoundsInRoot()
+
+        val titleBounds = rule
+            .onNodeWithTag(cardTitleTag, true)
+            .getUnclippedBoundsInRoot()
+
+        // Check top padding
+        (imageBounds.top - cardBounds.top).assertIsEqualTo(
+            10.dp,
+            "padding between top of the image and top of the card."
+        )
+
+        // Check bottom padding
+        (cardBounds.bottom - titleBounds.bottom).assertIsEqualTo(
+            14.dp,
+            "padding between bottom of the text and bottom of the card."
+        )
+
+        // Check start padding
+        (imageBounds.left - cardBounds.left).assertIsEqualTo(
+            8.dp,
+            "padding between left of the image and left of the card."
+        )
+
+        // Check end padding
+        (cardBounds.right - imageBounds.right).assertIsEqualTo(
+            12.dp,
+            "padding between right of the text and right of the card."
+        )
+    }
+
+    @Test
+    fun compactCard_semantics() {
+        val count = mutableStateOf(0)
+        rule.setContent {
+            CompactCard(
+                modifier = Modifier
+                    .semantics(mergeDescendants = true) {}
+                    .testTag(CompactCardTag),
+                image = { SampleImage() },
+                title = { Text("${count.value}") },
+                onClick = { count.value += 1 }
+            )
+        }
+
+        rule.onNodeWithTag(CompactCardTag)
+            .assertHasClickAction()
+            .assert(SemanticsMatcher.keyNotDefined(SemanticsProperties.Role))
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+            .assertIsEnabled()
+            .assertTextEquals("0")
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+            .assertTextEquals("1")
+    }
+
+    @Test
+    fun compactCard_clickAction() {
+        val count = mutableStateOf(0f)
+        rule.setContent {
+            CompactCard(
+                modifier = Modifier
+                    .semantics(mergeDescendants = true) {}
+                    .testTag(CompactCardTag),
+                image = { SampleImage() },
+                title = { Text("${count.value}") },
+                onClick = { count.value += 1 }
+            )
+        }
+
+        rule.onNodeWithTag(CompactCardTag)
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+        Truth.assertThat(count.value).isEqualTo(1)
+
+        rule.onNodeWithTag(CompactCardTag)
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+        Truth.assertThat(count.value).isEqualTo(3)
+    }
+
+    @Test
+    fun wideClassicCard_semantics() {
+        val count = mutableStateOf(0)
+        rule.setContent {
+            WideClassicCard(
+                modifier = Modifier
+                    .semantics(mergeDescendants = true) {}
+                    .testTag(WideClassicCardTag),
+                image = { SampleImage() },
+                title = { Text("${count.value}") },
+                onClick = { count.value += 1 }
+            )
+        }
+
+        rule.onNodeWithTag(WideClassicCardTag)
+            .assertHasClickAction()
+            .assert(SemanticsMatcher.keyNotDefined(SemanticsProperties.Role))
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+            .assertIsEnabled()
+            .assertTextEquals("0")
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+            .assertTextEquals("1")
+    }
+
+    @Test
+    fun wideClassicCard_clickAction() {
+        val count = mutableStateOf(0f)
+        rule.setContent {
+            WideClassicCard(
+                modifier = Modifier
+                    .semantics(mergeDescendants = true) {}
+                    .testTag(WideClassicCardTag),
+                image = { SampleImage() },
+                title = { Text("${count.value}") },
+                onClick = { count.value += 1 }
+            )
+        }
+
+        rule.onNodeWithTag(WideClassicCardTag)
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+        Truth.assertThat(count.value).isEqualTo(1)
+
+        rule.onNodeWithTag(WideClassicCardTag)
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+        Truth.assertThat(count.value).isEqualTo(3)
+    }
+
+    @Test
+    fun wideClassicCard_contentPadding() {
+        val contentPadding = PaddingValues(8.dp, 10.dp, 12.dp, 14.dp)
+        val cardTitleTag = "wide_classic_card_title"
+
+        rule.setContent {
+            WideClassicCard(
+                modifier = Modifier.testTag(WideClassicCardTag),
+                image = { SampleImage() },
+                title = {
+                    Text(
+                        text = "Wide Classic Card",
+                        modifier = Modifier.testTag(cardTitleTag)
+                    )
+                },
+                onClick = {},
+                contentPadding = contentPadding
+            )
+        }
+
+        val cardBounds = rule
+            .onNodeWithTag(WideClassicCardTag)
+            .getUnclippedBoundsInRoot()
+
+        val imageBounds = rule
+            .onNodeWithTag(SampleImageTag, true)
+            .getUnclippedBoundsInRoot()
+
+        val titleBounds = rule
+            .onNodeWithTag(cardTitleTag, true)
+            .getUnclippedBoundsInRoot()
+
+        // Check top padding
+        (imageBounds.top - cardBounds.top).assertIsEqualTo(
+            10.dp,
+            "padding between top of the image and top of the card."
+        )
+
+        // Check bottom padding
+        (cardBounds.bottom - imageBounds.bottom).assertIsEqualTo(
+            14.dp,
+            "padding between bottom of the text and bottom of the card."
+        )
+
+        // Check start padding
+        (imageBounds.left - cardBounds.left).assertIsEqualTo(
+            8.dp,
+            "padding between left of the image and left of the card."
+        )
+
+        // Check end padding
+        (cardBounds.right - titleBounds.right).assertIsEqualTo(
+            12.dp,
+            "padding between right of the text and right of the card."
+        )
+    }
+
+    @Composable
+    fun SampleImage() {
+        Box(
+            Modifier
+                .size(180.dp, 150.dp)
+                .testTag(SampleImageTag)
+        )
+    }
+}
+
+private const val CardTag = "card"
+private const val StandardCardTag = "standard-card"
+private const val CompactCardTag = "compact-card"
+private const val ClassicCardTag = "classic-card"
+private const val WideCardTag = "wide-card"
+private const val WideClassicCardTag = "wide-classic-card"
+
+private const val SampleImageTag = "sample-image"
\ No newline at end of file
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Card.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Card.kt
index 1a5d6c7..53add8d 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Card.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Card.kt
@@ -20,15 +20,26 @@
 import androidx.compose.foundation.BorderStroke
 import androidx.compose.foundation.interaction.Interaction
 import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.ReadOnlyComposable
 import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithCache
+import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.unit.dp
 
 /**
@@ -82,16 +93,328 @@
 }
 
 /**
+ * [ClassicCard] is an opinionated TV Material card that offers a 4 slot layout to show
+ * information about a subject.
+ *
+ * This card has a vertical layout with the interactive surface [Surface], which provides the image
+ * slot at the top, followed by the title, subtitle, and description slots.
+ *
+ * This Card handles click events, calling its [onClick] lambda.
+ *
+ * @param onClick called when this card is clicked
+ * @param image defines the [Composable] image to be displayed on top of the Card.
+ * @param title defines the [Composable] title placed below the image in the Card.
+ * @param modifier the [Modifier] to be applied to this card.
+ * @param subtitle defines the [Composable] supporting text placed below the title of the Card.
+ * @param description defines the [Composable] description placed below the subtitle of the Card.
+ * @param shape [CardShape] defines the shape of this card's container in different interaction
+ * states. See [CardDefaults.shape].
+ * @param colors [CardColors] defines the background & content colors used in this card for
+ * different interaction states. See [CardDefaults.colors].
+ * @param scale [CardScale] defines size of the card relative to its original size for different
+ * interaction states. See [CardDefaults.scale].
+ * @param border [CardBorder] defines a border around the card for different interaction states.
+ * See [CardDefaults.border].
+ * @param glow [CardGlow] defines a shadow to be shown behind the card for different interaction
+ * states. See [CardDefaults.glow].
+ * @param contentPadding [PaddingValues] defines the inner padding applied to the card's content.
+ * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
+ * for this card. You can create and pass in your own `remember`ed instance to observe
+ * [Interaction]s and customize the appearance / behavior of this card in different states.
+ */
+@ExperimentalTvMaterial3Api
+@Composable
+fun ClassicCard(
+    onClick: () -> Unit,
+    image: @Composable BoxScope.() -> Unit,
+    title: @Composable () -> Unit,
+    modifier: Modifier = Modifier,
+    subtitle: @Composable () -> Unit = {},
+    description: @Composable () -> Unit = {},
+    shape: CardShape = CardDefaults.shape(),
+    colors: CardColors = CardDefaults.colors(),
+    scale: CardScale = CardDefaults.scale(),
+    border: CardBorder = CardDefaults.border(),
+    glow: CardGlow = CardDefaults.glow(),
+    contentPadding: PaddingValues = PaddingValues(),
+    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
+) {
+    Card(
+        onClick = onClick,
+        modifier = modifier,
+        interactionSource = interactionSource,
+        shape = shape,
+        colors = colors,
+        scale = scale,
+        border = border,
+        glow = glow
+    ) {
+        Column(
+            modifier = Modifier.padding(contentPadding)
+        ) {
+            Box(
+                contentAlignment = CardDefaults.ContentImageAlignment,
+                content = image
+            )
+            Column {
+                CardContent(
+                    title = title,
+                    subtitle = subtitle,
+                    description = description
+                )
+            }
+        }
+    }
+}
+
+/**
+ * [CompactCard] is an opinionated TV Material card that offers a 4 slot layout to show
+ * information about a subject.
+ *
+ * This card provides the interactive surface [Surface] with the image slot as the background
+ * (with an overlay scrim gradient). Other slots for the title, subtitle, and description are
+ * placed over it.
+ *
+ * This Card handles click events, calling its [onClick] lambda.
+ *
+ * @param onClick called when this card is clicked
+ * @param image defines the [Composable] image to be displayed on top of the Card.
+ * @param title defines the [Composable] title placed below the image in the Card.
+ * @param modifier the [Modifier] to be applied to this card.
+ * @param subtitle defines the [Composable] supporting text placed below the title of the Card.
+ * @param description defines the [Composable] description placed below the subtitle of the Card.
+ * @param shape [CardShape] defines the shape of this card's container in different interaction
+ * states. See [CardDefaults.shape].
+ * @param colors [CardColors] defines the background & content colors used in this card for
+ * different interaction states. See [CardDefaults.compactCardColors].
+ * @param scale [CardScale] defines size of the card relative to its original size for different
+ * interaction states. See [CardDefaults.scale].
+ * @param border [CardBorder] defines a border around the card for different interaction states.
+ * See [CardDefaults.border].
+ * @param glow [CardGlow] defines a shadow to be shown behind the card for different interaction
+ * states. See [CardDefaults.glow].
+ * @param scrimBrush [Brush] defines a brush/gradient to be used to draw the scrim over the image
+ * in the background. See [CardDefaults.ContainerGradient].
+ * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
+ * for this card. You can create and pass in your own `remember`ed instance to observe
+ * [Interaction]s and customize the appearance / behavior of this card in different states.
+ */
+@ExperimentalTvMaterial3Api
+@Composable
+fun CompactCard(
+    onClick: () -> Unit,
+    image: @Composable BoxScope.() -> Unit,
+    title: @Composable () -> Unit,
+    modifier: Modifier = Modifier,
+    subtitle: @Composable () -> Unit = {},
+    description: @Composable () -> Unit = {},
+    shape: CardShape = CardDefaults.shape(),
+    colors: CardColors = CardDefaults.compactCardColors(),
+    scale: CardScale = CardDefaults.scale(),
+    border: CardBorder = CardDefaults.border(),
+    glow: CardGlow = CardDefaults.glow(),
+    scrimBrush: Brush = CardDefaults.ContainerGradient,
+    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
+) {
+    Card(
+        onClick = onClick,
+        modifier = modifier,
+        interactionSource = interactionSource,
+        shape = shape,
+        colors = colors,
+        scale = scale,
+        border = border,
+        glow = glow
+    ) {
+        Box(contentAlignment = Alignment.BottomStart) {
+            Box(
+                modifier = Modifier
+                    .drawWithCache {
+                        onDrawWithContent {
+                            drawContent()
+                            drawRect(brush = scrimBrush)
+                        }
+                    },
+                contentAlignment = CardDefaults.ContentImageAlignment,
+                content = image
+            )
+            Column {
+                CardContent(
+                    title = title,
+                    subtitle = subtitle,
+                    description = description
+                )
+            }
+        }
+    }
+}
+
+/**
+ * [WideClassicCard] is an opinionated TV Material card that offers a 4 slot layout to show
+ * information about a subject.
+ *
+ * This card has a horizontal layout with the interactive surface [Surface], which provides the
+ * image slot at the start, followed by the title, subtitle, and description slots at the end.
+ *
+ * This Card handles click events, calling its [onClick] lambda.
+ *
+ * @param onClick called when this card is clicked
+ * @param image defines the [Composable] image to be displayed on top of the Card.
+ * @param title defines the [Composable] title placed below the image in the Card.
+ * @param modifier the [Modifier] to be applied to this card.
+ * @param subtitle defines the [Composable] supporting text placed below the title of the Card.
+ * @param description defines the [Composable] description placed below the subtitle of the Card.
+ * @param shape [CardShape] defines the shape of this card's container in different interaction
+ * states. See [CardDefaults.shape].
+ * @param colors [CardColors] defines the background & content colors used in this card for
+ * different interaction states. See [CardDefaults.colors].
+ * @param scale [CardScale] defines size of the card relative to its original size for different
+ * interaction states. See [CardDefaults.scale].
+ * @param border [CardBorder] defines a border around the card for different interaction states.
+ * See [CardDefaults.border].
+ * @param glow [CardGlow] defines a shadow to be shown behind the card for different interaction
+ * states. See [CardDefaults.glow].
+ * @param contentPadding [PaddingValues] defines the inner padding applied to the card's content.
+ * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
+ * for this card. You can create and pass in your own `remember`ed instance to observe
+ * [Interaction]s and customize the appearance / behavior of this card in different states.
+ */
+@ExperimentalTvMaterial3Api
+@Composable
+fun WideClassicCard(
+    onClick: () -> Unit,
+    image: @Composable BoxScope.() -> Unit,
+    title: @Composable () -> Unit,
+    modifier: Modifier = Modifier,
+    subtitle: @Composable () -> Unit = {},
+    description: @Composable () -> Unit = {},
+    shape: CardShape = CardDefaults.shape(),
+    colors: CardColors = CardDefaults.colors(),
+    scale: CardScale = CardDefaults.scale(),
+    border: CardBorder = CardDefaults.border(),
+    glow: CardGlow = CardDefaults.glow(),
+    contentPadding: PaddingValues = PaddingValues(),
+    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
+) {
+    Card(
+        onClick = onClick,
+        modifier = modifier,
+        interactionSource = interactionSource,
+        shape = shape,
+        colors = colors,
+        scale = scale,
+        border = border,
+        glow = glow
+    ) {
+        Row(
+            modifier = Modifier.padding(contentPadding)
+        ) {
+            Box(
+                contentAlignment = CardDefaults.ContentImageAlignment,
+                content = image
+            )
+            Column {
+                CardContent(
+                    title = title,
+                    subtitle = subtitle,
+                    description = description
+                )
+            }
+        }
+    }
+}
+
+@Composable
+private fun CardContent(
+    title: @Composable () -> Unit,
+    subtitle: @Composable () -> Unit = {},
+    description: @Composable () -> Unit = {},
+    contentColor: Color
+) {
+    CompositionLocalProvider(LocalContentColor provides contentColor) {
+        CardContent(title, subtitle, description)
+    }
+}
+
+@Composable
+private fun CardContent(
+    title: @Composable () -> Unit,
+    subtitle: @Composable () -> Unit = {},
+    description: @Composable () -> Unit = {}
+) {
+    ProvideTextStyle(MaterialTheme.typography.titleMedium) {
+        title.invoke()
+    }
+    ProvideTextStyle(MaterialTheme.typography.bodySmall) {
+        Box(Modifier.graphicsLayer { alpha = SubtitleAlpha }) {
+            subtitle.invoke()
+        }
+    }
+    ProvideTextStyle(MaterialTheme.typography.bodySmall) {
+        Box(Modifier.graphicsLayer { alpha = DescriptionAlpha }) {
+            description.invoke()
+        }
+    }
+}
+
+/**
  * Contains the default values used by all card types.
  */
 @ExperimentalTvMaterial3Api
 object CardDefaults {
+    internal val ContentImageAlignment = Alignment.Center
+
     /**
-    * The default [Shape] used by Cards.
-    */
+     * The default [Shape] used by Cards.
+     */
     private val ContainerShape = RoundedCornerShape(8.dp)
 
     /**
+     * Recommended aspect ratio [Float] to get square images, can be applied using the modifier
+     * [Modifier.aspectRatio].
+     */
+    const val SquareImageAspectRatio = 1f
+
+    /**
+     * Recommended aspect ratio [Float] for vertical images, can be applied using the modifier
+     * [Modifier.aspectRatio].
+     */
+    const val VerticalImageAspectRatio = 2f / 3
+
+    /**
+     * Recommended aspect ratio [Float] for horizontal images, can be applied using the modifier
+     * [Modifier.aspectRatio].
+     */
+    const val HorizontalImageAspectRatio = 16f / 9
+
+    /**
+     * Gradient used in cards to give more emphasis to the textual content that is generally
+     * displayed above an image.
+     */
+    val ContainerGradient = Brush.verticalGradient(
+        listOf(
+            Color(red = 28, green = 27, blue = 31, alpha = 0),
+            Color(red = 28, green = 27, blue = 31, alpha = 204)
+        )
+    )
+
+    /**
+     * Returns the content color [Color] from the colors [CardColors] for different
+     * interaction states.
+     */
+    internal fun contentColor(
+        focused: Boolean,
+        pressed: Boolean,
+        colors: CardColors
+    ): Color {
+        return when {
+            focused -> colors.focusedContentColor
+            pressed -> colors.pressedContentColor
+            else -> colors.contentColor
+        }
+    }
+
+    /**
      * Creates a [CardShape] that represents the default container shapes used in a Card.
      *
      * @param shape the default shape used when the Card has no other [Interaction]s.
@@ -121,7 +444,7 @@
     @ReadOnlyComposable
     @Composable
     fun colors(
-        containerColor: Color = MaterialTheme.colorScheme.surface,
+        containerColor: Color = MaterialTheme.colorScheme.surfaceVariant,
         contentColor: Color = contentColorFor(containerColor),
         focusedContainerColor: Color = containerColor,
         focusedContentColor: Color = contentColorFor(focusedContainerColor),
@@ -137,6 +460,34 @@
     )
 
     /**
+     * Creates [CardColors] that represents the default colors used in a Compact Card.
+     *
+     * @param containerColor the default container color of this Card.
+     * @param contentColor the default content color of this Card.
+     * @param focusedContainerColor the container color of this Card when focused.
+     * @param focusedContentColor the content color of this Card when focused.
+     * @param pressedContainerColor the container color of this Card when pressed.
+     * @param pressedContentColor the content color of this Card when pressed.
+     */
+    @ReadOnlyComposable
+    @Composable
+    fun compactCardColors(
+        containerColor: Color = MaterialTheme.colorScheme.surfaceVariant,
+        contentColor: Color = Color.White,
+        focusedContainerColor: Color = containerColor,
+        focusedContentColor: Color = contentColor,
+        pressedContainerColor: Color = focusedContainerColor,
+        pressedContentColor: Color = focusedContentColor
+    ) = CardColors(
+        containerColor = containerColor,
+        contentColor = contentColor,
+        focusedContainerColor = focusedContainerColor,
+        focusedContentColor = focusedContentColor,
+        pressedContainerColor = pressedContainerColor,
+        pressedContentColor = pressedContentColor
+    )
+
+    /**
      * Creates a [CardScale] that represents the default scales used in a Card.
      * Scales are used to modify the size of a composable in different [Interaction] states
      * e.g. 1f (original) in default state, 1.1f (scaled up) in focused state, 0.8f (scaled down)
@@ -200,6 +551,9 @@
     )
 }
 
+private const val SubtitleAlpha = 0.6f
+private const val DescriptionAlpha = 0.8f
+
 @OptIn(ExperimentalTvMaterial3Api::class)
 private fun CardColors.toClickableSurfaceContainerColor() =
     ClickableSurfaceColor(