[go: nahoru, domu]

Adding Immersive List composable

Test: Tested manually on sample app and added an integration test

Relnote: Adding ImmersiveList

Change-Id: Ica405d7086a069ad213512fae9994a53a9f82a5c
diff --git a/tv/tv-material/api/current.txt b/tv/tv-material/api/current.txt
index 0ed6747..24ca3bd 100644
--- a/tv/tv-material/api/current.txt
+++ b/tv/tv-material/api/current.txt
@@ -9,9 +9,9 @@
 
 }
 
-package androidx.tv.material.pager {
+package androidx.tv.material.immersivelist {
 
-  public final class PagerKt {
+  public final class ImmersiveListKt {
   }
 
 }
diff --git a/tv/tv-material/api/public_plus_experimental_current.txt b/tv/tv-material/api/public_plus_experimental_current.txt
index 4e063ddc4..8712654 100644
--- a/tv/tv-material/api/public_plus_experimental_current.txt
+++ b/tv/tv-material/api/public_plus_experimental_current.txt
@@ -50,9 +50,27 @@
 
 }
 
-package androidx.tv.material.pager {
+package androidx.tv.material.immersivelist {
 
-  public final class PagerKt {
+  @androidx.compose.runtime.Immutable @androidx.tv.material.ExperimentalTvMaterialApi public final class ImmersiveListBackgroundScope implements androidx.compose.foundation.layout.BoxScope {
+    method @androidx.compose.animation.ExperimentalAnimationApi @androidx.compose.runtime.Composable public void AnimatedContent(int targetState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentScope<java.lang.Integer>,androidx.compose.animation.ContentTransform> transitionSpec, optional androidx.compose.ui.Alignment contentAlignment, kotlin.jvm.functions.Function2<? super androidx.compose.animation.AnimatedVisibilityScope,? super java.lang.Integer,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public void AnimatedVisibility(boolean visible, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.animation.EnterTransition enter, optional androidx.compose.animation.ExitTransition exit, optional String label, kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedVisibilityScope,kotlin.Unit> content);
+  }
+
+  @androidx.tv.material.ExperimentalTvMaterialApi public final class ImmersiveListDefaults {
+    method public androidx.compose.animation.EnterTransition getEnterTransition();
+    method public androidx.compose.animation.ExitTransition getExitTransition();
+    property public final androidx.compose.animation.EnterTransition EnterTransition;
+    property public final androidx.compose.animation.ExitTransition ExitTransition;
+    field public static final androidx.tv.material.immersivelist.ImmersiveListDefaults INSTANCE;
+  }
+
+  public final class ImmersiveListKt {
+    method @androidx.compose.runtime.Composable @androidx.tv.material.ExperimentalTvMaterialApi public static void ImmersiveList(kotlin.jvm.functions.Function3<? super androidx.tv.material.immersivelist.ImmersiveListBackgroundScope,? super java.lang.Integer,? super java.lang.Boolean,kotlin.Unit> background, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment listAlignment, kotlin.jvm.functions.Function1<? super androidx.tv.material.immersivelist.ImmersiveListScope,kotlin.Unit> list);
+  }
+
+  @androidx.compose.runtime.Immutable @androidx.tv.material.ExperimentalTvMaterialApi public final class ImmersiveListScope {
+    method public androidx.compose.ui.Modifier focusableItem(androidx.compose.ui.Modifier, int index);
   }
 
 }
diff --git a/tv/tv-material/api/restricted_current.txt b/tv/tv-material/api/restricted_current.txt
index 0ed6747..24ca3bd 100644
--- a/tv/tv-material/api/restricted_current.txt
+++ b/tv/tv-material/api/restricted_current.txt
@@ -9,9 +9,9 @@
 
 }
 
-package androidx.tv.material.pager {
+package androidx.tv.material.immersivelist {
 
-  public final class PagerKt {
+  public final class ImmersiveListKt {
   }
 
 }
diff --git a/tv/tv-material/build.gradle b/tv/tv-material/build.gradle
index 3e5244d..19f04632 100644
--- a/tv/tv-material/build.gradle
+++ b/tv/tv-material/build.gradle
@@ -41,6 +41,7 @@
     implementation("androidx.compose.ui:ui-text:$composeVersion")
     implementation("androidx.compose.ui:ui-util:$composeVersion")
     implementation("androidx.profileinstaller:profileinstaller:1.2.0")
+    implementation(project(":tv:tv-foundation"))
 
     androidTestImplementation(libs.truth)
 
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/FeaturedCarousel.kt b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/FeaturedCarousel.kt
index 0a19b0c..971069d 100644
--- a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/FeaturedCarousel.kt
+++ b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/FeaturedCarousel.kt
@@ -24,27 +24,17 @@
 import androidx.compose.animation.core.tween
 import androidx.compose.foundation.background
 import androidx.compose.foundation.border
-import androidx.compose.foundation.focusable
 import androidx.compose.foundation.horizontalScroll
-import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
 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.width
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.LazyRow
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material3.Button
-import androidx.compose.material3.Card
-import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
@@ -59,7 +49,6 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Color.Companion.Cyan
 import androidx.compose.ui.graphics.Color.Companion.Gray
-import androidx.compose.ui.graphics.Color.Companion.Red
 import androidx.compose.ui.graphics.Color.Companion.Yellow
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.dp
@@ -72,19 +61,11 @@
 @Composable
 fun FeaturedCarousel() {
     val carouselState = remember { CarouselState(0) }
-    LazyColumn {
-        item {
-            Carousel(
-                modifier = Modifier
-                    .height(400.dp)
-                    .width(950.dp),
-                carouselState = carouselState,
-                slideCount = 3
-            ) { SampleFrame(it) }
-        }
-
-        items(7) { SampleLazyRow() }
-    }
+    Carousel(
+        modifier = Modifier.height(130.dp).width(950.dp).border(1.dp, Color.Black),
+        carouselState = carouselState,
+        slideCount = mediaItems.size
+    ) { SampleFrame(it) }
 }
 
 @OptIn(ExperimentalTvMaterialApi::class)
@@ -162,61 +143,6 @@
     }
 }
 
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun SampleLazyRow() {
-    LazyRow(
-        state = rememberLazyListState(),
-        contentPadding = PaddingValues(2.dp),
-        horizontalArrangement = Arrangement.spacedBy(4.dp),
-        modifier = Modifier
-            .fillMaxWidth()
-            .height(100.dp)) {
-        items((1..10).map { it.toString() }) {
-            var cardScale by remember { mutableStateOf(0.5f) }
-            val borderGlowColorTransition = rememberInfiniteTransition()
-            var initialValue by remember { mutableStateOf(Color.Transparent) }
-            val glowingColor by borderGlowColorTransition.animateColor(
-                initialValue = initialValue,
-                targetValue = Color.Transparent,
-                animationSpec = infiniteRepeatable(
-                    animation = tween(1000, easing = LinearEasing),
-                    repeatMode = RepeatMode.Reverse
-                )
-            )
-
-            Card(
-                modifier = Modifier
-                    .width(100.dp)
-                    .height(100.dp)
-                    .scale(cardScale)
-                    .border(2.dp, glowingColor, RoundedCornerShape(12.dp))
-                    .onFocusChanged { focusState ->
-                        if (focusState.isFocused) {
-                            cardScale = 1.0f
-                            initialValue = Color.White
-                        } else {
-                            cardScale = 0.5f
-                            initialValue = Color.Transparent
-                        }
-                    }
-                    .focusable()
-            ) {
-                Text(
-                    text = it,
-                    modifier = Modifier
-                        .fillMaxWidth()
-                        .height(100.dp)
-                        .padding(12.dp),
-                    color = Red,
-                    fontWeight = FontWeight.Bold
-
-                )
-            }
-        }
-    }
-}
-
 val mediaItems = listOf(
     Media(id = "1", title = "Title 1", description = "Description 1", backgroundColor = Gray),
     Media(id = "2", title = "Title 2", description = "Description 2", backgroundColor = Yellow),
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/MainActivity.kt b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/MainActivity.kt
index c329df0..b3f2c76 100644
--- a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/MainActivity.kt
+++ b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/MainActivity.kt
@@ -19,8 +19,40 @@
 import android.os.Bundle
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
+import androidx.compose.animation.animateColor
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.border
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Card
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.scale
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
 
 class MainActivity : ComponentActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
@@ -28,8 +60,70 @@
         setContent {
             // A surface container using the 'background' color from the theme
             Surface(color = MaterialTheme.colorScheme.background) {
-                FeaturedCarousel()
+                LazyColumn {
+                    item { FeaturedCarousel() }
+                    item { SampleImmersiveList() }
+
+                    items(7) { SampleLazyRow() }
+                }
             }
         }
     }
+
+    @Composable
+    fun SampleLazyRow() {
+        LazyRow(
+            state = rememberLazyListState(),
+            contentPadding = PaddingValues(2.dp),
+            horizontalArrangement = Arrangement.spacedBy(4.dp),
+            modifier = Modifier
+                .fillMaxWidth()
+                .height(100.dp)) {
+            items((1..10).map { it.toString() }) { SampleCard(it) }
+        }
+    }
+
+    @Composable
+    private fun SampleCard(it: String) {
+        var cardScale by remember { mutableStateOf(0.5f) }
+        val borderGlowColorTransition = rememberInfiniteTransition()
+        var initialValue by remember { mutableStateOf(Color.Transparent) }
+        val glowingColor by borderGlowColorTransition.animateColor(
+            initialValue = initialValue,
+            targetValue = Color.Transparent,
+            animationSpec = infiniteRepeatable(
+                animation = tween(1000, easing = LinearEasing),
+                repeatMode = RepeatMode.Reverse
+            )
+        )
+
+        Card(
+            modifier = Modifier
+                .width(100.dp)
+                .height(100.dp)
+                .scale(cardScale)
+                .border(2.dp, glowingColor, RoundedCornerShape(12.dp))
+                .onFocusChanged { focusState ->
+                    if (focusState.isFocused) {
+                        cardScale = 1.0f
+                        initialValue = Color.White
+                    } else {
+                        cardScale = 0.5f
+                        initialValue = Color.Transparent
+                    }
+                }
+                .focusable()
+        ) {
+            Text(
+                text = it,
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .height(100.dp)
+                    .padding(12.dp),
+                color = Color.Red,
+                fontWeight = FontWeight.Bold
+
+            )
+        }
+    }
 }
\ No newline at end of file
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/SampleImmersiveList.kt b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/SampleImmersiveList.kt
new file mode 100644
index 0000000..45dffab
--- /dev/null
+++ b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/SampleImmersiveList.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2022 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.tvmaterial.samples
+
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.animation.animateColor
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Card
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.scale
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.lazy.list.TvLazyRow
+import androidx.tv.material.ExperimentalTvMaterialApi
+import androidx.tv.material.immersivelist.ImmersiveList
+
+@OptIn(ExperimentalTvMaterialApi::class, ExperimentalAnimationApi::class)
+@Composable
+fun SampleImmersiveList() {
+    ImmersiveList(
+        modifier = Modifier
+            .height(130.dp)
+            .width(950.dp)
+            .border(1.dp, Color.Black),
+        background = { index, _ ->
+            AnimatedContent(targetState = index) { SampleBackground(it) } },
+    ) {
+        TvLazyRow {
+            items(immersiveClusterMediaItems.size) {
+                SampleCard(Modifier.focusableItem(it), (it + 1).toString())
+            }
+        }
+    }
+}
+
+@Composable
+fun SampleBackground(idx: Int) {
+    val item = immersiveClusterMediaItems[idx]
+
+    Box(
+        Modifier
+            .background(item.backgroundColor)
+            .fillMaxWidth()
+            .height(90.dp)) {
+        Text(
+            text = item.title,
+            style = MaterialTheme.typography.headlineSmall,
+            color = Color.Black,
+            fontWeight = FontWeight.Bold
+        )
+    }
+}
+
+@Composable
+private fun SampleCard(modifier: Modifier, cardText: String) {
+    var cardScale by remember { mutableStateOf(0.5f) }
+    val borderGlowColorTransition = rememberInfiniteTransition()
+    var initialValue by remember { mutableStateOf(Color.Transparent) }
+    val glowingColor by borderGlowColorTransition.animateColor(
+        initialValue = initialValue,
+        targetValue = Color.Transparent,
+        animationSpec = infiniteRepeatable(
+            animation = tween(1000, easing = LinearEasing),
+            repeatMode = RepeatMode.Reverse
+        )
+    )
+
+    Card(
+        modifier = modifier
+            .width(100.dp)
+            .height(100.dp)
+            .scale(cardScale)
+            .border(2.dp, glowingColor, RoundedCornerShape(12.dp))
+            .onFocusChanged { focusState ->
+                if (focusState.isFocused) {
+                    cardScale = 1.0f
+                    initialValue = Color.White
+                } else {
+                    cardScale = 0.5f
+                    initialValue = Color.Transparent
+                }
+            }
+            .focusable()
+    ) {
+        Text(
+            text = cardText,
+            modifier = Modifier
+                .fillMaxWidth()
+                .height(100.dp)
+                .padding(12.dp),
+            color = Color.Red,
+            fontWeight = FontWeight.Bold
+
+        )
+    }
+}
+
+val immersiveClusterMediaItems = listOf(
+    Media(id = "1", title = "Title 1", description = "Description 1", backgroundColor = Color.Gray),
+    Media(id = "2", title = "Title 2", description = "Description 2", backgroundColor = Color.Blue),
+    Media(id = "3", title = "Title 3", description = "Description 3", backgroundColor = Color.Cyan)
+)
\ No newline at end of file
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material/immersivelist/ImmersiveListTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material/immersivelist/ImmersiveListTest.kt
new file mode 100644
index 0000000..5b22bdd
--- /dev/null
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material/immersivelist/ImmersiveListTest.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2022 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.material.immersivelist
+
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.input.key.NativeKeyEvent
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsFocused
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.tv.foundation.lazy.list.TvLazyRow
+import androidx.tv.material.ExperimentalTvMaterialApi
+import org.junit.Rule
+import org.junit.Test
+
+class ImmersiveListTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @OptIn(ExperimentalTvMaterialApi::class, ExperimentalAnimationApi::class)
+    @Test
+    fun immersiveList_scroll_backgroundChanges() {
+        val firstCard = FocusRequester()
+        val secondCard = FocusRequester()
+
+        rule.setContent {
+            ImmersiveList(
+                background = { index, _ ->
+                    AnimatedContent(targetState = index) {
+                        Box(
+                            Modifier
+                                .testTag("background-$it")
+                                .size(200.dp)) {
+                            BasicText("background-$it")
+                        }
+                    }
+                }) {
+                    TvLazyRow {
+                        items(3) { index ->
+                            var modifier = Modifier
+                                .testTag("card-$index")
+                                .size(100.dp)
+                            when (index) {
+                                0 -> modifier = modifier
+                                    .focusRequester(firstCard)
+                                1 -> modifier = modifier
+                                    .focusRequester(secondCard)
+                            }
+
+                            Box(modifier.focusableItem(index)) { BasicText("card-$index") }
+                        }
+                    }
+            }
+        }
+
+        rule.runOnIdle { firstCard.requestFocus() }
+
+        rule.onNodeWithTag("card-0").assertIsFocused()
+        rule.onNodeWithTag("background-0").assertIsDisplayed()
+        rule.onNodeWithTag("background-1").assertDoesNotExist()
+        rule.onNodeWithTag("background-2").assertDoesNotExist()
+
+        rule.waitForIdle()
+        keyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
+
+        rule.onNodeWithTag("card-1").assertIsFocused()
+        rule.onNodeWithTag("background-1").assertIsDisplayed()
+        rule.onNodeWithTag("background-0").assertDoesNotExist()
+        rule.onNodeWithTag("background-2").assertDoesNotExist()
+
+        rule.waitForIdle()
+        keyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
+
+        rule.onNodeWithTag("card-0").assertIsFocused()
+        rule.onNodeWithTag("background-0").assertIsDisplayed()
+        rule.onNodeWithTag("background-1").assertDoesNotExist()
+        rule.onNodeWithTag("background-2").assertDoesNotExist()
+    }
+
+    private fun keyPress(keyCode: Int, numberOfPresses: Int = 1) {
+        for (index in 0 until numberOfPresses)
+            InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keyCode)
+    }
+}
\ No newline at end of file
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material/pager/PagerTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material/pager/PagerTest.kt
deleted file mode 100644
index 23df054..0000000
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material/pager/PagerTest.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright 2022 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.material.pager
-
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.tv.material.ExperimentalTvMaterialApi
-import org.junit.Rule
-import org.junit.Test
-
-class PagerTest {
-    @get:Rule
-    val rule = createComposeRule()
-
-    @OptIn(ExperimentalTvMaterialApi::class)
-    @Test
-    fun pager_zeroSlideCount_drawsSomething() {
-        val testTag = "pager"
-        rule.setContent {
-            Pager(slideCount = 0, modifier = Modifier.testTag(testTag)) {}
-        }
-
-        rule.onNodeWithTag(testTag).assertExists()
-    }
-}
\ No newline at end of file
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt b/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt
index ebd71f6..d579e94a 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt
@@ -16,11 +16,14 @@
 
 package androidx.tv.material.carousel
 
+import androidx.compose.animation.AnimatedContent
 import androidx.compose.animation.EnterTransition
 import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.ExperimentalAnimationApi
 import androidx.compose.animation.core.tween
 import androidx.compose.animation.fadeIn
 import androidx.compose.animation.fadeOut
+import androidx.compose.animation.with
 import androidx.compose.foundation.background
 import androidx.compose.foundation.focusable
 import androidx.compose.foundation.layout.Arrangement
@@ -49,7 +52,6 @@
 import androidx.compose.ui.platform.LocalFocusManager
 import androidx.compose.ui.unit.dp
 import androidx.tv.material.ExperimentalTvMaterialApi
-import androidx.tv.material.pager.Pager
 import java.lang.Math.floorMod
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.first
@@ -69,7 +71,7 @@
  */
 
 @Suppress("IllegalExperimentalApiUsage")
-@OptIn(ExperimentalComposeUiApi::class)
+@OptIn(ExperimentalComposeUiApi::class, ExperimentalAnimationApi::class)
 @ExperimentalTvMaterialApi
 @Composable
 fun Carousel(
@@ -82,7 +84,9 @@
     carouselIndicator:
     @Composable BoxScope.() -> Unit = {
         CarouselDefaults.Indicator(
-            modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp),
+            modifier = Modifier
+                .align(Alignment.BottomEnd)
+                .padding(16.dp),
             carouselState = carouselState,
             slideCount = slideCount)
     },
@@ -105,13 +109,10 @@
             }
         }
         .focusable()) {
-        Pager(
-            enterTransition = enterTransition,
-            exitTransition = exitTransition,
-            currentSlide = carouselState.slideIndex,
-            slideCount = slideCount
+        AnimatedContent(
+            targetState = carouselState.slideIndex,
+            transitionSpec = { enterTransition.with(exitTransition) }
         ) { content.invoke(it) }
-
         this.carouselIndicator()
     }
 }
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/immersivelist/ImmersiveList.kt b/tv/tv-material/src/main/java/androidx/tv/material/immersivelist/ImmersiveList.kt
new file mode 100644
index 0000000..091c16e
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material/immersivelist/ImmersiveList.kt
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2022 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.material.immersivelist
+
+import androidx.compose.animation.AnimatedContentScope
+import androidx.compose.animation.AnimatedVisibilityScope
+import androidx.compose.animation.ContentTransform
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.with
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusDirection
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.tv.material.ExperimentalTvMaterialApi
+
+/**
+ * Immersive List consists of a list with multiple items and a background that displays content
+ * based on the item in focus.
+ * To animate the background's entry and exit, use [ImmersiveListBackgroundScope.AnimatedContent].
+ * To display the background only when the list is in focus, use
+ * [ImmersiveListBackgroundScope.AnimatedVisibility].
+ *
+ * @param background Composable defining the background to be displayed for a given item's
+ * index.
+ * @param modifier applied to Immersive List.
+ * @param listAlignment Alignment of the List with respect to the Immersive List.
+ * @param list composable defining the list of items that has to be rendered.
+ */
+@Suppress("IllegalExperimentalApiUsage")
+@OptIn(ExperimentalComposeUiApi::class)
+@ExperimentalTvMaterialApi
+@Composable
+fun ImmersiveList(
+    background:
+    @Composable ImmersiveListBackgroundScope.(index: Int, listHasFocus: Boolean) -> Unit,
+    modifier: Modifier = Modifier,
+    listAlignment: Alignment = Alignment.BottomEnd,
+    list: @Composable ImmersiveListScope.() -> Unit,
+) {
+    var currentItemIndex by remember { mutableStateOf(0) }
+    var listHasFocus by remember { mutableStateOf(false) }
+
+    Box(modifier) {
+        ImmersiveListBackgroundScope(this).background(currentItemIndex, listHasFocus)
+
+        val focusManager = LocalFocusManager.current
+
+        Box(Modifier.align(listAlignment).onFocusChanged { listHasFocus = it.hasFocus }) {
+            ImmersiveListScope {
+                currentItemIndex = it
+                focusManager.moveFocus(FocusDirection.Enter)
+            }.list()
+        }
+    }
+}
+
+@ExperimentalTvMaterialApi
+object ImmersiveListDefaults {
+    /**
+     * Default transition used to bring the background content into view
+     */
+    val EnterTransition: EnterTransition = fadeIn(animationSpec = tween(300))
+
+    /**
+     * Default transition used to remove the background content from view
+     */
+    val ExitTransition: ExitTransition = fadeOut(animationSpec = tween(300))
+}
+
+@Immutable
+@ExperimentalTvMaterialApi
+public class ImmersiveListBackgroundScope internal constructor(boxScope: BoxScope) : BoxScope
+by boxScope {
+
+    /**
+     * [ImmersiveListBackgroundScope.AnimatedVisibility] composable animates the appearance and
+     * disappearance of its content, as [visible] value changes. Different [EnterTransition]s and
+     * [ExitTransition]s can be defined in [enter] and [exit] for the appearance and disappearance
+     * animation.
+     *
+     * @param visible defines whether the content should be visible
+     * @param modifier modifier for the Layout created to contain the [content]
+     * @param enter EnterTransition(s) used for the appearing animation, fading in by default
+     * @param exit ExitTransition(s) used for the disappearing animation, fading out by default
+     * @param content Content to appear or disappear based on the value of [visible]
+     *
+     * @link androidx.compose.animation.AnimatedVisibility
+     * @see androidx.compose.animation.AnimatedVisibility
+     * @see EnterTransition
+     * @see ExitTransition
+     * @see AnimatedVisibilityScope
+     */
+    @Composable
+    fun AnimatedVisibility(
+        visible: Boolean,
+        modifier: Modifier = Modifier,
+        enter: EnterTransition = ImmersiveListDefaults.EnterTransition,
+        exit: ExitTransition = ImmersiveListDefaults.ExitTransition,
+        label: String = "AnimatedVisibility",
+        content: @Composable AnimatedVisibilityScope.() -> Unit
+    ) {
+        androidx.compose.animation.AnimatedVisibility(
+            visible,
+            modifier,
+            enter,
+            exit,
+            label,
+            content)
+    }
+
+    /**
+     * [ImmersiveListBackgroundScope.AnimatedContent] is a container that automatically animates its
+     * content when [targetState] changes. Its [content] for different target states is defined in a
+     * mapping between a target state and a composable function.
+     *
+     * @param targetState defines the key to choose the content to be displayed
+     * @param modifier modifier for the Layout created to contain the [content]
+     * @param transitionSpec defines the EnterTransition(s) and ExitTransition(s) used to display
+     * and remove the content, fading in and fading out by default
+     * @param content Content to appear or disappear based on the value of [targetState]
+     *
+     * @link androidx.compose.animation.AnimatedContent
+     * @see androidx.compose.animation.AnimatedContent
+     * @see ContentTransform
+     * @see AnimatedContentScope
+     */
+    @Suppress("IllegalExperimentalApiUsage")
+    @ExperimentalAnimationApi
+    @Composable
+    fun AnimatedContent(
+        targetState: Int,
+        modifier: Modifier = Modifier,
+        transitionSpec: AnimatedContentScope<Int>.() -> ContentTransform = {
+            ImmersiveListDefaults.EnterTransition.with(ImmersiveListDefaults.ExitTransition)
+        },
+        contentAlignment: Alignment = Alignment.TopStart,
+        content: @Composable AnimatedVisibilityScope.(targetState: Int) -> Unit
+    ) {
+        androidx.compose.animation.AnimatedContent(
+            targetState,
+            modifier,
+            transitionSpec,
+            contentAlignment,
+            content)
+    }
+}
+
+@Immutable
+@ExperimentalTvMaterialApi
+public class ImmersiveListScope internal constructor(private val onFocused: (Int) -> Unit) {
+    /**
+     * Modifier to be added to each of the items of the list within ImmersiveList to inform the
+     * ImmersiveList of the index of the item in focus.
+     *
+     * @param index index of the item within the list.
+     */
+    fun Modifier.focusableItem(index: Int): Modifier {
+        return onFocusChanged { if (it.hasFocus || it.isFocused) { onFocused(index) } }
+            .focusable()
+    }
+}
\ No newline at end of file
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/pager/Pager.kt b/tv/tv-material/src/main/java/androidx/tv/material/pager/Pager.kt
deleted file mode 100644
index 24f88ed..0000000
--- a/tv/tv-material/src/main/java/androidx/tv/material/pager/Pager.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright 2022 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.material.pager
-
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.EnterTransition
-import androidx.compose.animation.ExitTransition
-import androidx.compose.animation.ExperimentalAnimationApi
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.with
-import androidx.compose.foundation.layout.Box
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.tv.material.ExperimentalTvMaterialApi
-
-/**
- * Composable that accepts a lambda that generates the slides based on the index provided and
- * displays the slide associated with the index [currentSlide].
- *
- * @param modifier the modifier to apply to this component.
- * @param enterTransition defines how the slide is animated into view.
- * @param exitTransition defines how the slide is animated out of view.
- * @param currentSlide the slide that is currently displayed by the pager.
- * @param slideCount the total number of slides.
- * @param content defines the slide composable for a given index.
- */
-@Suppress("IllegalExperimentalApiUsage")
-@OptIn(ExperimentalAnimationApi::class)
-@ExperimentalTvMaterialApi
-@Composable
-internal fun Pager(
-    slideCount: Int,
-    modifier: Modifier = Modifier,
-    enterTransition: EnterTransition = PagerDefaults.EnterTransition,
-    exitTransition: ExitTransition = PagerDefaults.ExitTransition,
-    currentSlide: Int = 0,
-    content: @Composable (index: Int) -> Unit
-) {
-    if (slideCount <= 0) {
-        Box(modifier)
-    } else {
-        AnimatedContent(
-            modifier = modifier,
-            targetState = currentSlide.coerceIn(0, slideCount - 1),
-            transitionSpec = { enterTransition.with(exitTransition) }
-        ) {
-            content(it)
-        }
-    }
-}
-
-@ExperimentalTvMaterialApi
-private object PagerDefaults {
-    /**
-     * Default transition used to bring a slide into view
-     */
-    val EnterTransition: EnterTransition = fadeIn(animationSpec = tween(900))
-
-    /**
-     * Default transition used to remove a slide from view
-     */
-    val ExitTransition: ExitTransition = fadeOut(animationSpec = tween(900))
-}